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

ICanBoogie / ActiveRecord / 11881074700

17 Nov 2024 06:07PM UTC coverage: 86.108%. Remained the same
11881074700

push

github

olvlvl
Use PHPStan 2.0

5 of 10 new or added lines in 6 files covered. (50.0%)

108 existing lines in 5 files now uncovered.

1376 of 1598 relevant lines covered (86.11%)

24.51 hits per line

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

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

3
namespace ICanBoogie;
4

5
use ICanBoogie\ActiveRecord\Model;
6
use ICanBoogie\ActiveRecord\Query;
7
use ICanBoogie\ActiveRecord\RecordNotValid;
8
use ICanBoogie\ActiveRecord\Schema;
9
use ICanBoogie\ActiveRecord\Schema\Serial;
10
use ICanBoogie\ActiveRecord\StaticModelProvider;
11
use ICanBoogie\Validate\ValidationErrors;
12
use LogicException;
13
use ReflectionException;
14
use Throwable;
15

16
use function array_keys;
17
use function is_array;
18

19
/**
20
 * Active Record facilitates the creation and use of business objects whose data require persistent
21
 * storage via a database.
22
 *
23
 * @method ValidationErrors validate() Validate the active record, returns an array of errors.
24
 *
25
 * @property-read Model<static> $model
26
 *     The model managing the active record.
27
 *     {@see self::get_model()}
28
 * @property-read bool $is_new
29
 *     Whether the record is new or not.
30
 *     {@see self::get_is_new()}
31
 * @property-read scalar|scalar[] $primary_key_value
32
 *     The value of the primary key.
33
 *     {@see self::get_primary_key_value()}
34
 */
35
abstract class ActiveRecord extends Prototyped
36
{
37
    /**
38
     * Returns a new query.
39
     *
40
     * @return Query<static>
41
     *
42
     * @see Model::query()
43
     */
44
    final public static function query(): Query
45
    {
46
        return StaticModelProvider::model_for_record(static::class)->query();
5✔
47
    }
48

49
    /**
50
     * Returns a new query with the WHERE clause initialized with the provided conditions and arguments.
51
     *
52
     * @param mixed ...$conditions_and_args
53
     *
54
     * @return Query<static>
55
     *
56
     * @see Query::where()
57
     */
58
    final public static function where(...$conditions_and_args): Query
59
    {
60
        return self::query()->where(...$conditions_and_args);
2✔
61
    }
62

63
    /**
64
     * Model managing the active record.
65
     *
66
     * @var Model<static>
67
     */
68
    private Model $model;
69

70
    /**
71
     * @return Model<static>
72
     */
73
    protected function get_model(): Model
74
    {
75
        /** @var Model<static> */
76
        return $this->model
10✔
77
            ??= StaticModelProvider::model_for_record($this::class);
10✔
78
    }
79

80
    /**
81
     * @return scalar|scalar[]
82
     */
83
    protected function get_primary_key_value(): mixed
84
    {
85
        $model = $this->get_model();
1✔
86
        $primary = $model->extended_schema->primary;
1✔
87

88
        if (is_array($primary)) {
1✔
89
            $actual = [];
×
90

91
            foreach ($primary as $property) {
×
92
                $actual[] = $this->$property;
×
93
            }
94

95
            // @phpstan-ignore-next-line
96
            return $actual;
×
97
        }
98

99
        // @phpstan-ignore-next-line
100
        return $this->$primary;
1✔
101
    }
102

103
    /**
104
     * @param ?Model<static> $model
105
     *     The model managing the active record. A {@see Model} instance can be specified as well as a model
106
     *     identifier. If `$model` is null, the model will be resolved with {@see StaticModelProvider} when required.
107
     */
108
    public function __construct(?Model $model = null)
109
    {
110
        if ($model) {
32✔
111
            $this->model = $model;
22✔
112
        }
113
    }
114

115
    /**
116
     * Removes the {@see $model} property.
117
     *
118
     * Properties whose values are instances of the {@see ActiveRecord} class are removed from the
119
     * exported properties.
120
     *
121
     * @return array<non-empty-string, mixed>
122
     *
123
     * @throws ReflectionException
124
     */
125
    public function __sleep() // @phpstan-ignore-line
126
    {
127
        $properties = parent::__sleep();
2✔
128

129
        /** @phpstan-ignore-next-line */
130
        unset($properties['model']);
2✔
131

132
        foreach (array_keys($properties) as $property) {
2✔
UNCOV
133
            if ($this->$property instanceof self) {
×
UNCOV
134
                unset($properties[$property]);
×
135
            }
136
        }
137

138
        return $properties;
2✔
139
    }
140

141
    /**
142
     * Removes `model` from the output.
143
     *
144
     * @return array<non-empty-string, mixed>
145
     */
146
    public function __debugInfo(): array
147
    {
148
        $array = (array)$this;
1✔
149

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

152
        return $array;
1✔
153
    }
154

155
    /**
156
     * Whether the record is new or not.
157
     */
158
    protected function get_is_new(): bool
159
    {
UNCOV
160
        $primary = $this->get_model()->primary;
×
161

162
        if (is_array($primary)) {
×
UNCOV
163
            foreach ($primary as $property) {
×
164
                if (empty($this->$property)) {
×
165
                    return true;
×
166
                }
167
            }
UNCOV
168
        } elseif (empty($this->$primary)) {
×
UNCOV
169
            return true;
×
170
        }
171

UNCOV
172
        return false;
×
173
    }
174

175
    /**
176
     * Saves the active record using its model.
177
     *
178
     * @throws Throwable
179
     */
180
    public function save(bool $skip_validation = false): void
181
    {
182
        if (!$skip_validation) {
6✔
183
            $this->assert_is_valid();
6✔
184
        }
185

186
        $model = $this->get_model();
5✔
187
        $schema = $model->extended_schema;
5✔
188
        // @phpstan-ignore-next-line
189
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
5✔
190

191
        if (count($properties) == 0) {
5✔
UNCOV
192
            throw new LogicException("No properties to save");
×
193
        }
194

195
        #
196
        # Multi-column primary key
197
        #
198

199
        $primary = $model->primary;
5✔
200

201
        if (is_array($primary)) {
5✔
UNCOV
202
            $model->insert($properties, upsert: true);
×
203

204
            return;
×
205
        }
206

207
        #
208
        # Non auto-increment primary key, unless the key is inherited from parent model.
209
        #
210

211
        if (
212
            !$model->parent && $primary && isset($properties[$primary])
5✔
213
            && !$model->extended_schema->columns[$primary] instanceof Serial
5✔
214
        ) {
UNCOV
215
            $model->insert($properties, upsert: true);
×
216

217
            return;
×
218
        }
219

220
        #
221
        # Serial primary key
222
        #
223

224
        $id = null;
5✔
225

226
        if (isset($properties[$primary])) {
5✔
227
            $id = $properties[$primary];
1✔
228
            unset($properties[$primary]);
1✔
229
            assert(is_numeric($id));
230
        }
231

232
        // @phpstan-ignore-next-line
233
        $rc = $model->save($properties, $id);
5✔
234

235
        if ($id === null) {
5✔
236
            $this->$primary = $rc;
5✔
237
        }
238
    }
239

240
    /**
241
     * Assert that a record is valid.
242
     *
243
     * @throws RecordNotValid if the record is not valid.
244
     */
245
    public function assert_is_valid(): void
246
    {
247
        $errors = $this->validate();
6✔
248

249
        if (count($errors)) {
6✔
250
            throw new RecordNotValid($this, $errors);
1✔
251
        }
252
    }
253

254
    /**
255
     * Creates validation rules.
256
     *
257
     * @return array<string, mixed>
258
     */
259
    public function create_validation_rules(): array
260
    {
261
        return [];
9✔
262
    }
263

264
    /**
265
     * Unless it's an acceptable value for a column, columns with `null` values are discarded.
266
     * This way, we don't have to define every property before saving our active record.
267
     *
268
     * @param array<non-empty-string, mixed> $properties
269
     * @param Schema $schema The model's extended schema.
270
     *
271
     * @return array<non-empty-string, mixed> The altered persistent properties
272
     */
273
    protected function alter_persistent_properties(array $properties, Schema $schema): array
274
    {
275
        foreach ($properties as $identifier => $value) {
5✔
276
            if ($value !== null || ($schema->has_column($identifier) && $schema->columns[$identifier]->null)) {
5✔
277
                continue;
5✔
278
            }
279

UNCOV
280
            unset($properties[$identifier]);
×
281
        }
282

283
        return $properties;
5✔
284
    }
285

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

300
        // @phpstan-ignore-next-line
301
        $model->delete($key);
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

© 2025 Coveralls, Inc