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

ICanBoogie / ActiveRecord / 4437457574

pending completion
4437457574

push

github

Olivier Laviale
Case of a nullable belong_to

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

1356 of 1686 relevant lines covered (80.43%)

34.68 hits per line

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

76.92
/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
#[\AllowDynamicProperties]
34
abstract class ActiveRecord extends Prototyped
35
{
36
    public const SAVE_SKIP_VALIDATION = 'skip_validation';
37

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

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

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

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

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

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

89
        unset($properties['model']);
3✔
90
        unset($properties['model_id']);
3✔
91

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

98
        return $properties;
3✔
99
    }
100

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

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

113
        return $array;
1✔
114
    }
115

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

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

135
        return false;
×
136
    }
137

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

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

156
        #
157
        # Multipart primary key
158
        #
159

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

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

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

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

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

181
        $key = null;
3✔
182

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

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

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

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

198
        return $rc;
3✔
199
    }
200

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

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

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

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

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

244
        return $properties;
3✔
245
    }
246

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

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

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

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