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

ICanBoogie / ActiveRecord / 11645748547

02 Nov 2024 08:54PM UTC coverage: 86.318%. Remained the same
11645748547

push

github

olvlvl
Tidy Query

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

93 existing lines in 3 files now uncovered.

1369 of 1586 relevant lines covered (86.32%)

24.43 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
 * @see self::get_model()
26
 * @property-read Model $model The model managing the active record.
27
 * @see self::get_is_new()
28
 * @property-read bool $is_new Whether the record is new or not.
29
 * @see self::get_primary_key_value()
30
 * @property-read TKey $primary_key_value The value of the primary key.
31
 *
32
 * @template TKey of int|non-empty-string|non-empty-array<non-empty-string>
33
 */
34
abstract class ActiveRecord extends Prototyped
35
{
36
    /**
37
     * Returns a new query.
38
     *
39
     * @return Query<static>
40
     *
41
     * @see Model::query()
42
     */
43
    final public static function query(): Query
44
    {
45
        return StaticModelProvider::model_for_record(static::class)->query();
5✔
46
    }
47

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

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

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

78
    /**
79
     * @return mixed&TKey
80
     */
81
    protected function get_primary_key_value(): mixed
82
    {
83
        $model = $this->get_model();
1✔
84
        $primary = $model->extended_schema->primary;
1✔
85

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

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

93
            return $actual;
×
94
        }
95

96
        return $this->$primary;
1✔
97
    }
98

99
    /**
100
     * @param ?Model<TKey, static> $model
101
     *     The model managing the active record. A {@link Model} instance can be specified as well as a model
102
     *     identifier. If `$model` is null, the model will be resolved with {@link StaticModelProvider} when required.
103
     */
104
    public function __construct(Model $model = null)
105
    {
106
        if ($model) {
31✔
107
            $this->model = $model;
21✔
108
        }
109
    }
110

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

125
        /** @phpstan-ignore-next-line */
126
        unset($properties['model']);
2✔
127

128
        foreach (array_keys($properties) as $property) {
2✔
129
            if ($this->$property instanceof self) {
×
130
                unset($properties[$property]);
×
131
            }
132
        }
133

134
        return $properties;
2✔
135
    }
136

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

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

148
        return $array;
1✔
149
    }
150

151
    /**
152
     * Whether the record is new or not.
153
     */
154
    protected function get_is_new(): bool
155
    {
156
        $primary = $this->get_model()->primary;
×
157

158
        if (is_array($primary)) {
×
159
            foreach ($primary as $property) {
×
160
                if (empty($this->$property)) {
×
161
                    return true;
×
162
                }
163
            }
164
        } elseif (empty($this->$primary)) {
×
165
            return true;
×
166
        }
167

168
        return false;
×
169
    }
170

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

182
        $model = $this->get_model();
5✔
183
        $schema = $model->extended_schema;
5✔
184
        // @phpstan-ignore-next-line
185
        $properties = $this->alter_persistent_properties($this->to_array(), $schema);
5✔
186

187
        if (count($properties) == 0) {
5✔
188
            throw new LogicException("No properties to save");
×
189
        }
190

191
        #
192
        # Multi-column primary key
193
        #
194

195
        $primary = $model->primary;
5✔
196

197
        if (is_array($primary)) {
5✔
198
            $model->insert($properties, upsert: true);
×
199

200
            return;
×
201
        }
202

203
        #
204
        # Non auto-increment primary key, unless the key is inherited from parent model.
205
        #
206

207
        if (
208
            !$model->parent && $primary && isset($properties[$primary])
5✔
209
            && !$model->extended_schema->columns[$primary] instanceof Serial
5✔
210
        ) {
211
            $model->insert($properties, upsert: true);
×
212

213
            return;
×
214
        }
215

216
        #
217
        # Serial primary key
218
        #
219

220
        $id = null;
5✔
221

222
        if (isset($properties[$primary])) {
5✔
223
            $id = $properties[$primary];
1✔
224
            unset($properties[$primary]);
1✔
225
            assert(is_numeric($id));
226
        }
227

228
        // @phpstan-ignore-next-line
229
        $rc = $model->save($properties, $id);
5✔
230

231
        if ($id === null) {
5✔
232
            $this->$primary = $rc;
5✔
233
        }
234
    }
235

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

245
        if (count($errors)) {
6✔
246
            throw new RecordNotValid($this, $errors);
1✔
247
        }
248
    }
249

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

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

UNCOV
276
            unset($properties[$identifier]);
×
277
        }
278

279
        return $properties;
5✔
280
    }
281

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

296
        $model->delete($key);
297
    }
298
}
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