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

ICanBoogie / ActiveRecord / 4473815494

pending completion
4473815494

push

github

Olivier Laviale
Remove AllowDynamicProperties from ActiveRecord

1356 of 1687 relevant lines covered (80.38%)

34.87 hits per line

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

75.38
/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

21
use function is_numeric;
22

23
/**
24
 * Active Record facilitates the creation and use of business objects whose data require persistent
25
 * storage via database.
26
 *
27
 * @method ValidationErrors validate() Validate the active record, returns an array of errors.
28
 *
29
 * @property-read Model $model The model managing the active record.
30
 * @property-read string $model_id The identifier of the model managing the active record.
31
 * @property-read bool $is_new Whether the record is new or not.
32
 */
33
abstract class ActiveRecord extends Prototyped
34
{
35
    public const SAVE_SKIP_VALIDATION = 'skip_validation';
36

37
    /**
38
     * Model managing the active record.
39
     */
40
    private Model $model;
41

42
    protected function get_model(): Model
43
    {
44
        return $this->model
8✔
45
            ??= StaticModelResolver::model_for_activerecord($this::class);
8✔
46
    }
47

48
    /**
49
     * Identifier of the model managing the active record.
50
     *
51
     * Note: Due to a PHP bug (or feature), the visibility of the property MUST NOT be private.
52
     * https://bugs.php.net/bug.php?id=40412
53
     */
54
    private string $model_id;
55

56
    protected function get_model_id(): string
57
    {
58
        return $this->model_id
1✔
59
            ??= $this->get_model()->id;
1✔
60
    }
61

62
    /**
63
     * @param ?Model $model The model managing the active record. A {@link Model}
64
     * instance can be specified as well as a model identifier. If `$model` is empty, the model
65
     * will be resolved with {@link StaticModelResolver} when required.
66
     *
67
     * @throws \InvalidArgumentException if $model is neither a model identifier nor a
68
     * {@link Model} instance.
69
     */
70
    public function __construct(Model $model = null)
71
    {
72
        if ($model) {
36✔
73
            $this->model = $model;
28✔
74
            $this->model_id = $model->id;
28✔
75
        }
76
    }
77

78
    /**
79
     * Removes the {@link $model} property.
80
     *
81
     * Properties whose value are instances of the {@link ActiveRecord} class are removed from the
82
     * exported properties.
83
     */
84
    public function __sleep(): array
85
    {
86
        $properties = parent::__sleep();
2✔
87

88
        unset($properties['model']);
2✔
89
        unset($properties['model_id']);
2✔
90

91
        foreach (\array_keys($properties) as $property) {
2✔
92
            if ($this->$property instanceof self) {
1✔
93
                unset($properties[$property]);
×
94
            }
95
        }
96

97
        return $properties;
2✔
98
    }
99

100
    /**
101
     * Removes `model` from the output, since `model_id` is good enough to figure which model
102
     * is used.
103
     *
104
     * @return array
105
     */
106
    public function __debugInfo()
107
    {
108
        $array = (array)$this;
1✔
109

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

112
        return $array;
1✔
113
    }
114

115
    /**
116
     * Whether the record is new or not.
117
     *
118
     * @return bool
119
     */
120
    protected function get_is_new(): bool
121
    {
122
        $primary = $this->get_model()->primary;
×
123

124
        if (\is_array($primary)) {
×
125
            foreach ($primary as $property) {
×
126
                if (empty($this->$property)) {
×
127
                    return true;
×
128
                }
129
            }
130
        } elseif (empty($this->$primary)) {
×
131
            return true;
×
132
        }
133

134
        return false;
×
135
    }
136

137
    /**
138
     * Saves the active record using its model.
139
     *
140
     * @param array<string, mixed> $options Save options.
141
     *
142
     * @return bool|int Primary key value of the active record, or a boolean if the primary key
143
     * is not a serial.
144
     */
145
    public function save(array $options = [])
146
    {
147
        if (empty($options[self::SAVE_SKIP_VALIDATION])) {
4✔
148
            $this->assert_is_valid();
4✔
149
        }
150

151
        $model = $this->get_model();
3✔
152
        $schema = $model->extended_schema;
3✔
153
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
3✔
154

155
        #
156
        # Multipart primary key
157
        #
158

159
        $primary = $model->primary;
3✔
160

161
        if (\is_array($primary)) {
3✔
162
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
163
        }
164

165
        #
166
        # Non auto-increment primary key, unless the key is inherited from parent model.
167
        #
168

169
        if (
170
            !$model->parent && $primary && isset($properties[$primary])
3✔
171
            && !$model->extended_schema[$primary]->auto_increment
3✔
172
        ) {
173
            return $model->insert($properties, [ 'on duplicate' => true ]);
×
174
        }
175

176
        #
177
        # Auto-increment primary key
178
        #
179

180
        $key = null;
3✔
181

182
        if (isset($properties[$primary])) {
3✔
183
            $key = $properties[$primary];
×
184
            unset($properties[$primary]);
×
185
        }
186

187
        $rc = $model->save($properties, $key);
3✔
188

189
        if (is_numeric($rc)) {
3✔
190
            $rc = (int)$rc;
3✔
191
        }
192

193
        if ($key === null && $rc) {
3✔
194
            $this->update_primary_key($rc);
3✔
195
        }
196

197
        return $rc;
3✔
198
    }
199

200
    /**
201
     * Assert that a record is valid.
202
     *
203
     * @throws RecordNotValid if the record is not valid.
204
     */
205
    public function assert_is_valid(): void
206
    {
207
        $errors = $this->validate();
4✔
208

209
        if (count($errors)) {
4✔
210
            throw new RecordNotValid($this, $errors);
1✔
211
        }
212
    }
213

214
    /**
215
     * Creates validation rules.
216
     *
217
     * @return array<string, mixed>
218
     */
219
    public function create_validation_rules(): array
220
    {
221
        return [];
7✔
222
    }
223

224
    /**
225
     * Unless it's an acceptable value for a column, columns with `null` values are discarded.
226
     * This way, we don't have to define every property before saving our active record.
227
     *
228
     * @param array<string, mixed> $properties
229
     * @param Schema $schema The model's extended schema.
230
     *
231
     * @return array<string, mixed> The altered persistent properties
232
     */
233
    protected function alter_persistent_properties(array $properties, Schema $schema): array
234
    {
235
        foreach ($properties as $identifier => $value) {
3✔
236
            if ($value !== null || (isset($schema[$identifier]) && $schema[$identifier]->null)) {
3✔
237
                continue;
3✔
238
            }
239

240
            unset($properties[$identifier]);
×
241
        }
242

243
        return $properties;
3✔
244
    }
245

246
    /**
247
     * Updates primary key.
248
     *
249
     * @param int|string[]|string $primary_key
250
     */
251
    protected function update_primary_key(int|array|string $primary_key): void
252
    {
253
        $model = $this->get_model();
3✔
254
        $property = $model->primary
3✔
255
            ?? throw new LogicException("Unable to update primary key, model `$model->id` doesn't define one.");
×
256

257
        $this->$property = $primary_key;
3✔
258
    }
259

260
    /**
261
     * Deletes the active record using its model.
262
     *
263
     * @return bool `true` if the record was deleted, `false` otherwise.
264
     *
265
     * @throws LogicException in attempt to delete a record from a model which primary key is empty.
266
     */
267
    public function delete(): bool
268
    {
269
        $model = $this->get_model();
2✔
270
        $primary = $model->primary
2✔
271
            ?? throw new LogicException("Unable to delete record, model `$model->id` doesn't have a primary key");
×
272
        $key = $this->$primary
2✔
273
            ?? throw new LogicException("Unable to delete record, the primary key is not defined");
1✔
274

275
        return $model->delete($key);
1✔
276
    }
277
}
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