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

ICanBoogie / ActiveRecord / 4524183369

pending completion
4524183369

push

github

Olivier Laviale
Tidy

3 of 3 new or added lines in 1 file covered. (100.0%)

1355 of 1687 relevant lines covered (80.32%)

34.87 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\RecordNotValid;
16
use ICanBoogie\ActiveRecord\Schema;
17
use ICanBoogie\ActiveRecord\StaticModelResolver;
18
use ICanBoogie\Validate\ValidationErrors;
19
use LogicException;
20
use ReflectionException;
21
use Throwable;
22

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

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

46
    /**
47
     * Model managing the active record.
48
     *
49
     * @var Model<TKey, static>
50
     */
51
    private Model $model;
52

53
    /**
54
     * @return Model<TKey, static>
55
     */
56
    protected function get_model(): Model
57
    {
58
        return $this->model
8✔
59
            ??= StaticModelResolver::model_for_activerecord($this::class);
8✔
60
    }
61

62
    /**
63
     * Identifier of the model managing the active record.
64
     *
65
     * Note: Due to a PHP bug (or feature), the visibility of the property MUST NOT be private.
66
     * https://bugs.php.net/bug.php?id=40412
67
     */
68
    private string $model_id;
69

70
    protected function get_model_id(): string
71
    {
72
        return $this->model_id
1✔
73
            ??= $this->get_model()->id;
1✔
74
    }
75

76
    /**
77
     * @param ?Model<TKey,static> $model
78
     *     The model managing the active record. A {@link Model} instance can be specified as well as a model
79
     *     identifier. If `$model` is null, the model will be resolved with {@link StaticModelResolver} when required.
80
     */
81
    public function __construct(Model $model = null)
82
    {
83
        if ($model) {
36✔
84
            $this->model = $model;
28✔
85
            $this->model_id = $model->id;
28✔
86
        }
87
    }
88

89
    /**
90
     * Removes the {@link $model} property.
91
     *
92
     * Properties whose value are instances of the {@link ActiveRecord} class are removed from the
93
     * exported properties.
94
     *
95
     * @return array<string, mixed>
96
     *
97
     * @throws ReflectionException
98
     */
99
    public function __sleep() // @phpstan-ignore-line
100
    {
101
        $properties = parent::__sleep();
2✔
102

103
        /** @phpstan-ignore-next-line */
104
        unset($properties['model']);
2✔
105
        /** @phpstan-ignore-next-line */
106
        unset($properties['model_id']);
2✔
107

108
        foreach (array_keys($properties) as $property) {
2✔
109
            if ($this->$property instanceof self) {
×
110
                unset($properties[$property]);
×
111
            }
112
        }
113

114
        return $properties;
2✔
115
    }
116

117
    /**
118
     * Removes `model` from the output, since `model_id` is good enough to figure which model
119
     * is used.
120
     *
121
     * @return array<string, mixed>
122
     */
123
    public function __debugInfo(): array
124
    {
125
        $array = (array)$this;
1✔
126

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

129
        return $array;
1✔
130
    }
131

132
    /**
133
     * Whether the record is new or not.
134
     */
135
    protected function get_is_new(): bool
136
    {
137
        $primary = $this->get_model()->primary;
×
138

139
        if (is_array($primary)) {
×
140
            foreach ($primary as $property) {
×
141
                if (empty($this->$property)) {
×
142
                    return true;
×
143
                }
144
            }
145
        } elseif (empty($this->$primary)) {
×
146
            return true;
×
147
        }
148

149
        return false;
×
150
    }
151

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

168
        $model = $this->get_model();
3✔
169
        $schema = $model->extended_schema;
3✔
170
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
3✔
171

172
        #
173
        # Multipart primary key
174
        #
175

176
        $primary = $model->primary;
3✔
177

178
        if (is_array($primary)) {
3✔
179
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
180
        }
181

182
        #
183
        # Non auto-increment primary key, unless the key is inherited from parent model.
184
        #
185

186
        if (
187
            !$model->parent && $primary && isset($properties[$primary])
3✔
188
            && !$model->extended_schema[$primary]->auto_increment
3✔
189
        ) {
190
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
191
        }
192

193
        #
194
        # Auto-increment primary key
195
        #
196

197
        $key = null;
3✔
198

199
        if (isset($properties[$primary])) {
3✔
200
            $key = $properties[$primary];
×
201
            unset($properties[$primary]);
×
202
        }
203

204
        $rc = $model->save($properties, $key);
3✔
205

206
        if (is_numeric($rc)) {
3✔
207
            $rc = (int)$rc;
3✔
208
        }
209

210
        if ($key === null && $rc) {
3✔
211
            $this->update_primary_key($rc);
3✔
212
        }
213

214
        return $rc;
3✔
215
    }
216

217
    /**
218
     * Assert that a record is valid.
219
     *
220
     * @throws RecordNotValid if the record is not valid.
221
     */
222
    public function assert_is_valid(): void
223
    {
224
        $errors = $this->validate();
4✔
225

226
        if (count($errors)) {
4✔
227
            throw new RecordNotValid($this, $errors);
1✔
228
        }
229
    }
230

231
    /**
232
     * Creates validation rules.
233
     *
234
     * @return array<string, mixed>
235
     */
236
    public function create_validation_rules(): array
237
    {
238
        return [];
7✔
239
    }
240

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

257
            unset($properties[$identifier]);
×
258
        }
259

260
        return $properties;
3✔
261
    }
262

263
    /**
264
     * Updates primary key.
265
     *
266
     * @param int|string[]|string $primary_key
267
     */
268
    protected function update_primary_key(int|array|string $primary_key): void
269
    {
270
        $model = $this->get_model();
3✔
271
        $property = $model->primary
3✔
272
            ?? throw new LogicException("Unable to update primary key, model `$model->id` doesn't define one.");
×
273

274
        $this->$property = $primary_key;
3✔
275
    }
276

277
    /**
278
     * Deletes the active record using its model.
279
     *
280
     * @return bool `true` if the record was deleted, `false` otherwise.
281
     *
282
     * @throws LogicException in attempt to delete a record from a model which primary key is empty.
283
     */
284
    public function delete(): bool
285
    {
286
        $model = $this->get_model();
2✔
287
        $primary = $model->primary
2✔
288
            ?? throw new LogicException("Unable to delete record, model `$model->id` doesn't have a primary key");
×
289
        $key = $this->$primary
2✔
290
            ?? throw new LogicException("Unable to delete record, the primary key is not defined");
1✔
291

292
        return $model->delete($key);
1✔
293
    }
294
}
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