• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ICanBoogie / ActiveRecord / 6362433236

30 Sep 2023 11:14AM UTC coverage: 85.731% (+5.6%) from 80.178%
6362433236

push

github

olvlvl
Rename StaticModelProvider methods

1436 of 1675 relevant lines covered (85.73%)

29.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.85
/lib/ActiveRecord.php
1
<?php
2

3
/*
4
 * This file is part of the ICanBoogie package.
5
 *
6
 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace ICanBoogie;
13

14
use ICanBoogie\ActiveRecord\Model;
15
use ICanBoogie\ActiveRecord\Query;
16
use ICanBoogie\ActiveRecord\RecordNotValid;
17
use ICanBoogie\ActiveRecord\Schema;
18
use ICanBoogie\ActiveRecord\StaticModelProvider;
19
use ICanBoogie\Validate\ValidationErrors;
20
use LogicException;
21
use ReflectionException;
22
use Throwable;
23

24
use function array_keys;
25
use function is_array;
26
use function is_numeric;
27

28
/**
29
 * Active Record facilitates the creation and use of business objects whose data require persistent
30
 * storage via database.
31
 *
32
 * @method ValidationErrors validate() Validate the active record, returns an array of errors.
33
 *
34
 * @property-read Model $model The model managing the active record.
35
 * @uses self::get_model()
36
 * @property-read bool $is_new Whether the record is new or not.
37
 * @uses self::get_is_new()
38
 *
39
 * @template TKey of int|string|string[]
40
 */
41
abstract class ActiveRecord extends Prototyped
42
{
43
    public const SAVE_SKIP_VALIDATION = 'skip_validation';
44

45
    /**
46
     * Returns a new query.
47
     *
48
     * @return Query<static>
49
     *
50
     * @uses Model::query()
51
     */
52
    final public static function query(): Query
53
    {
54
        return StaticModelProvider::model_for_record(static::class)->query();
2✔
55
    }
56

57
    /**
58
     * Returns a new query with the WHERE clause initialized with the provided conditions and arguments.
59
     *
60
     * @param mixed ...$conditions_and_args
61
     *
62
     * @return Query<static>
63
     *
64
     * @uses Query::where()
65
     */
66
    final public static function where(...$conditions_and_args): Query
67
    {
68
        return self::query()->where(...$conditions_and_args);
1✔
69
    }
70

71
    /**
72
     * Model managing the active record.
73
     *
74
     * @var Model<TKey, static>
75
     */
76
    private Model $model;
77

78
    /**
79
     * @return Model<TKey, static>
80
     */
81
    protected function get_model(): Model
82
    {
83
        return $this->model
8✔
84
            ??= StaticModelProvider::model_for_record($this::class);
8✔
85
    }
86

87
    /**
88
     * @param ?Model<TKey,static> $model
89
     *     The model managing the active record. A {@link Model} instance can be specified as well as a model
90
     *     identifier. If `$model` is null, the model will be resolved with {@link StaticModelProvider} when required.
91
     */
92
    public function __construct(Model $model = null)
93
    {
94
        if ($model) {
34✔
95
            $this->model = $model;
26✔
96
        }
97
    }
98

99
    /**
100
     * Removes the {@link $model} property.
101
     *
102
     * Properties whose value are instances of the {@link ActiveRecord} class are removed from the
103
     * exported properties.
104
     *
105
     * @return array<non-empty-string, mixed>
106
     *
107
     * @throws ReflectionException
108
     */
109
    public function __sleep() // @phpstan-ignore-line
110
    {
111
        $properties = parent::__sleep();
2✔
112

113
        /** @phpstan-ignore-next-line */
114
        unset($properties['model']);
2✔
115

116
        foreach (array_keys($properties) as $property) {
2✔
117
            if ($this->$property instanceof self) {
×
118
                unset($properties[$property]);
×
119
            }
120
        }
121

122
        return $properties;
2✔
123
    }
124

125
    /**
126
     * Removes `model` from the output.
127
     *
128
     * @return array<non-empty-string, mixed>
129
     */
130
    public function __debugInfo(): array
131
    {
132
        $array = (array)$this;
1✔
133

134
        unset($array["\0" . __CLASS__ . "\0model"]);
1✔
135

136
        return $array;
1✔
137
    }
138

139
    /**
140
     * Whether the record is new or not.
141
     */
142
    protected function get_is_new(): bool
143
    {
144
        $primary = $this->get_model()->primary;
×
145

146
        if (is_array($primary)) {
×
147
            foreach ($primary as $property) {
×
148
                if (empty($this->$property)) {
×
149
                    return true;
×
150
                }
151
            }
152
        } elseif (empty($this->$primary)) {
×
153
            return true;
×
154
        }
155

156
        return false;
×
157
    }
158

159
    /**
160
     * Saves the active record using its model.
161
     *
162
     * @param array<string, mixed> $options Save options.
163
     *
164
     * @return bool|int|mixed Primary key value of the active record, or a boolean if the primary key
165
     * is not a serial.
166
     *
167
     * @throws Throwable
168
     */
169
    public function save(array $options = []): mixed
170
    {
171
        if (empty($options[self::SAVE_SKIP_VALIDATION])) {
4✔
172
            $this->assert_is_valid();
4✔
173
        }
174

175
        $model = $this->get_model();
3✔
176
        $schema = $model->extended_schema;
3✔
177
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
3✔
178

179
        #
180
        # Multipart primary key
181
        #
182

183
        $primary = $model->primary;
3✔
184

185
        if (is_array($primary)) {
3✔
186
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
187
        }
188

189
        #
190
        # Non auto-increment primary key, unless the key is inherited from parent model.
191
        #
192

193
        if (
194
            !$model->parent && $primary && isset($properties[$primary])
3✔
195
            && !$model->extended_schema->columns[$primary]->auto_increment
3✔
196
        ) {
197
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
198
        }
199

200
        #
201
        # Auto-increment primary key
202
        #
203

204
        $key = null;
3✔
205

206
        if (isset($properties[$primary])) {
3✔
207
            $key = $properties[$primary];
×
208
            unset($properties[$primary]);
×
209
        }
210

211
        $rc = $model->save($properties, $key);
3✔
212

213
        if (is_numeric($rc)) {
3✔
214
            $rc = (int)$rc;
3✔
215
        }
216

217
        if ($key === null && $rc) {
3✔
218
            $this->update_primary_key($rc);
3✔
219
        }
220

221
        return $rc;
3✔
222
    }
223

224
    /**
225
     * Assert that a record is valid.
226
     *
227
     * @throws RecordNotValid if the record is not valid.
228
     */
229
    public function assert_is_valid(): void
230
    {
231
        $errors = $this->validate();
4✔
232

233
        if (count($errors)) {
4✔
234
            throw new RecordNotValid($this, $errors);
1✔
235
        }
236
    }
237

238
    /**
239
     * Creates validation rules.
240
     *
241
     * @return array<string, mixed>
242
     */
243
    public function create_validation_rules(): array
244
    {
245
        return [];
7✔
246
    }
247

248
    /**
249
     * Unless it's an acceptable value for a column, columns with `null` values are discarded.
250
     * This way, we don't have to define every property before saving our active record.
251
     *
252
     * @param array<non-empty-string, mixed> $properties
253
     * @param Schema $schema The model's extended schema.
254
     *
255
     * @return array<non-empty-string, mixed> The altered persistent properties
256
     */
257
    protected function alter_persistent_properties(array $properties, Schema $schema): array
258
    {
259
        foreach ($properties as $identifier => $value) {
3✔
260
            if ($value !== null || ($schema->has_column($identifier) && $schema->columns[$identifier]->null)) {
3✔
261
                continue;
3✔
262
            }
263

264
            unset($properties[$identifier]);
×
265
        }
266

267
        return $properties;
3✔
268
    }
269

270
    /**
271
     * Updates primary key.
272
     *
273
     * @param int|non-empty-string|non-empty-array<non-empty-string> $primary_key
274
     */
275
    protected function update_primary_key(int|array|string $primary_key): void
276
    {
277
        $model = $this->get_model();
3✔
278
        $model_class = $model::class;
3✔
279
        $property = $model->primary
3✔
280
            ?? throw new LogicException("Unable to update primary key, model `$model_class` doesn't define one.");
×
281

282
        $this->$property = $primary_key;
3✔
283
    }
284

285
    /**
286
     * Deletes the active record using its model.
287
     *
288
     * @return bool `true` if the record was deleted, `false` otherwise.
289
     *
290
     * @throws LogicException in attempt to delete a record from a model which primary key is empty.
291
     */
292
    public function delete(): bool
293
    {
294
        $model = $this->get_model();
2✔
295
        $model_class = $model::class;
2✔
296
        $primary = $model->primary
2✔
297
            ?? throw new LogicException("Unable to delete record, model `$model_class` doesn't have a primary key");
×
298
        $key = $this->$primary
2✔
299
            ?? throw new LogicException("Unable to delete record, the primary key is not defined");
1✔
300

301
        return $model->delete($key);
1✔
302
    }
303
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc