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

ICanBoogie / ActiveRecord / 6362433236

30 Sep 2023 11:14AM UTC coverage: 85.731% (+5.6%) from 80.178%
6362433236

push

github

olvlvl
Rename StaticModelProvider methods

1436 of 1675 relevant lines covered (85.73%)

29.41 hits per line

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

97.62
/lib/ActiveRecord/Model.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\ActiveRecord;
13

14
use AllowDynamicProperties;
15
use ArrayAccess;
16
use ICanBoogie\ActiveRecord;
17
use ICanBoogie\ActiveRecord\Config\ModelDefinition;
18
use ICanBoogie\OffsetNotWritable;
19

20
use function array_fill_keys;
21
use function array_keys;
22
use function array_shift;
23
use function count;
24
use function func_get_args;
25
use function get_parent_class;
26
use function implode;
27
use function is_array;
28
use function is_callable;
29
use function method_exists;
30
use function sprintf;
31

32
/**
33
 * Base class for activerecord models.
34
 *
35
 * @template TKey of int|non-empty-string|non-empty-string[]
36
 * @template TValue of ActiveRecord
37
 *
38
 * @implements ArrayAccess<TKey, TValue>
39
 *
40
 * @method Query select($expression) The method is forwarded to Query::select().
41
 * @method Query where($conditions, $conditions_args = null, $_ = null)
42
 *     The method is forwarded to {@link Query::where}.
43
 * @method Query group($group) The method is forwarded to Query::group().
44
 * @method Query order(...$order) The method is forwarded to @link Query::order().
45
 * @method Query limit($limit, $offset = null) The method is forwarded to Query::limit().
46
 * @method Query offset($offset) The method is forwarded to Query::offset().
47
 * @method bool exists($key = null) The method is forwarded to Query::exists().
48
 * @method mixed count($column = null) The method is forwarded to Query::count().
49
 * @method string average($column) The method is forwarded to Query::average().
50
 * @method string maximum($column) The method is forwarded to Query::maximum().
51
 * @method string minimum($column) The method is forwarded to Query::minimum().
52
 * @method int sum($column) The method is forwarded to Query::sum().
53
 * @method array all() The method is forwarded to Query::all().
54
 * @method ActiveRecord one() The method is forwarded to Query::one().
55
 *
56
 * @property-read Model|null $parent Parent model.
57
 * @property-read array $all Retrieve all the records from the model.
58
 * @property-read int $count The number of records of the model.
59
 * @property-read bool $exists Whether the SQL table associated with the model exists.
60
 * @property-read ActiveRecord $one Retrieve the first record from the mode.
61
 * @property ActiveRecordCache $activerecord_cache The cache use to store activerecords.
62
 */
63
#[AllowDynamicProperties]
64
class Model extends Table implements ArrayAccess
65
{
66
    /**
67
     * @var class-string<TValue>
68
     */
69
    public readonly string $activerecord_class;
70

71
    /**
72
     * @var class-string<Query<TValue>>
73
     */
74
    public readonly string $query_class;
75

76
    /**
77
     * The relations of this model to other models.
78
     */
79
    public readonly RelationCollection $relations;
80

81
    /**
82
     * Returns the records cache.
83
     *
84
     * **Note:** The method needs to be implemented through prototype bindings.
85
     */
86
    protected function lazy_get_activerecord_cache(): ActiveRecordCache
87
    {
88
        /** @phpstan-ignore-next-line */
89
        return parent::lazy_get_activerecord_cache();
15✔
90
    }
91

92
    public function __construct(
93
        Connection $connection,
94
        public readonly ModelProvider $models,
95
        private readonly ModelDefinition $definition
96
    ) {
97
        $this->activerecord_class = $this->definition->activerecord_class; // @phpstan-ignore-line
82✔
98
        $this->query_class = $this->definition->query_class;
82✔
99

100
        $parent = $this->resolve_parent($models);
82✔
101

102
        parent::__construct($connection, $definition->table, $parent);
82✔
103

104
        $this->relations = new RelationCollection($this, $this->definition->association);
82✔
105
    }
106

107
    private function resolve_parent(ModelProvider $models): ?Model
108
    {
109
        $parent_class = get_parent_class($this->activerecord_class);
82✔
110

111
        if ($parent_class === ActiveRecord::class) {
82✔
112
            return null;
82✔
113
        }
114

115
        return $models->model_for_record($parent_class); // @phpstan-ignore-line
63✔
116
    }
117

118
    /**
119
     * Handles query methods, dynamic filters, and relations.
120
     *
121
     * @inheritdoc
122
     */
123
    public function __call($method, $arguments)
124
    {
125
        if (method_exists($this->query_class, $method) || str_starts_with($method, 'filter_by_')) {
52✔
126
            return $this->query()->$method(...$arguments);
52✔
127
        }
128

129
        if (is_callable([ $this->relations, $method ])) {
15✔
130
            return $this->relations->$method(...$arguments);
×
131
        }
132

133
        return parent::__call($method, $arguments);
15✔
134
    }
135

136
    /**
137
     * Finds a record or a collection of records.
138
     *
139
     * @param mixed $key A key, multiple keys, or an array of keys.
140
     *
141
     * @return TValue|TValue[] A record or a set of records.
142
     * @throws RecordNotFound when the record, or one or more records of the records
143
     * set, could not be found.
144
     *
145
     */
146
    public function find(mixed $key)
147
    {
148
        $args = func_get_args();
14✔
149
        $n = count($args);
14✔
150

151
        if (!$n) {
14✔
152
            throw new \BadMethodCallException("Expected at least one argument.");
×
153
        }
154

155
        if (count($args) == 1) {
14✔
156
            $key = $args[0];
11✔
157

158
            if (!is_array($key)) {
11✔
159
                return $this->find_one($key);
10✔
160
            }
161

162
            $args = $key;
1✔
163
        }
164

165
        return $this->find_many($args);
4✔
166
    }
167

168
    /**
169
     * Finds one records.
170
     *
171
     * @param TKey $key
172
     *
173
     * @return TValue
174
     */
175
    private function find_one($key): ActiveRecord
176
    {
177
        $record = $this->activerecord_cache->retrieve($key);
10✔
178

179
        if ($record) {
10✔
180
            return $record;
2✔
181
        }
182

183
        $record = $this->where([ $this->primary => $key ])->one;
10✔
184

185
        if (!$record) {
10✔
186
            throw new RecordNotFound(
2✔
187
                "Record <q>{$key}</q> does not exist in model <q>{$this->activerecord_class}</q>.",
2✔
188
                [ $key => null ]
2✔
189
            );
2✔
190
        }
191

192
        $this->activerecord_cache->store($record);
9✔
193

194
        return $record;
9✔
195
    }
196

197
    /**
198
     * Finds many records.
199
     *
200
     * @param array<int|non-empty-string> $keys
201
     *
202
     * @return array<int|non-empty-string, TValue>
203
     */
204
    private function find_many(array $keys): array
205
    {
206
        $records = $missing = array_fill_keys($keys, null);
4✔
207

208
        foreach ($keys as $key) {
4✔
209
            $record = $this->activerecord_cache->retrieve($key);
4✔
210

211
            if (!$record) {
4✔
212
                continue;
4✔
213
            }
214

215
            $records[$key] = $record;
2✔
216
            unset($missing[$key]);
2✔
217
        }
218

219
        if ($missing) {
4✔
220
            $primary = $this->primary;
4✔
221

222
            assert(is_string($primary));
223

224
            $query_records = $this->where([ $primary => array_keys($missing) ])->all;
4✔
225

226
            foreach ($query_records as $record) {
4✔
227
                $key = $record->$primary;
3✔
228
                $records[$key] = $record;
3✔
229
                unset($missing[$key]);
3✔
230

231
                $this->activerecord_cache->store($record);
3✔
232
            }
233
        }
234

235
        /** @var array<int|non-empty-string, TValue> $records */
236

237
        if ($missing) {
4✔
238
            if (count($missing) > 1) {
2✔
239
                throw new RecordNotFound(
1✔
240
                    sprintf(
1✔
241
                        "Records `%s` do not exist for `%s`",
1✔
242
                        implode('`, `', array_keys($missing)),
1✔
243
                        $this->activerecord_class
1✔
244
                    ),
1✔
245
                    $records
1✔
246
                );
1✔
247
            }
248

249
            $key = array_keys($missing);
1✔
250
            $key = array_shift($key);
1✔
251

252
            throw new RecordNotFound(
1✔
253
                "Record `$key` does not exist for `$this->activerecord_class`",
1✔
254
                $records
1✔
255
            );
1✔
256
        }
257

258
        return $records;
2✔
259
    }
260

261
    /**
262
     * Returns a new query.
263
     *
264
     * @return Query<TValue>
265
     */
266
    public function query(): Query
267
    {
268
        return new $this->query_class($this);
58✔
269
    }
270

271
    /**
272
     * Because records are cached, we need to remove the record from the cache when it is saved,
273
     * so that loading the record again returns the updated record, not the one in the cache.
274
     */
275
    public function save(array $values, mixed $id = null, array $options = []): mixed
276
    {
277
        if ($id) {
58✔
278
            $this->activerecord_cache->eliminate($id);
1✔
279
        }
280

281
        return parent::save($values, $id, $options);
58✔
282
    }
283

284
    /**
285
     * Eliminates the record from the cache.
286
     *
287
     * @inheritdoc
288
     */
289
    public function delete($key)
290
    {
291
        $this->activerecord_cache->eliminate($key);
2✔
292

293
        return parent::delete($key);
2✔
294
    }
295

296
    /**
297
     * Checks that the SQL table associated with the model exists.
298
     */
299
    protected function get_exists(): bool
300
    {
301
        return $this->exists();
1✔
302
    }
303

304
    /**
305
     * Returns the number of records of the model.
306
     */
307
    protected function get_count(): int
308
    {
309
        return $this->count();
1✔
310
    }
311

312
    /**
313
     * Returns all the records of the model.
314
     *
315
     * @return TValue[]
316
     */
317
    protected function get_all(): array
318
    {
319
        return $this->all();
2✔
320
    }
321

322
    /**
323
     * Returns the first record of the model.
324
     *
325
     * @phpstan-return TValue
326
     */
327
    protected function get_one(): ActiveRecord
328
    {
329
        return $this->one();
2✔
330
    }
331

332
    /**
333
     * @inheritdoc
334
     *
335
     * @throws OffsetNotWritable when one tries to write an offset.
336
     */
337
    public function offsetSet(mixed $offset, mixed $value): void
338
    {
339
        throw new OffsetNotWritable([ $offset, $this ]);
1✔
340
    }
341

342
    /**
343
     * Alias to {@link exists()}.
344
     *
345
     * @param int $key ActiveRecord identifier.
346
     *
347
     * @return bool
348
     */
349
    public function offsetExists(mixed $key): bool
350
    {
351
        return $this->exists($key);
1✔
352
    }
353

354
    /**
355
     * Alias to {@link delete()}.
356
     *
357
     * @param int $key ActiveRecord identifier.
358
     */
359
    public function offsetUnset(mixed $key): void
360
    {
361
        $this->delete($key);
1✔
362
    }
363

364
    /**
365
     * Alias to {@link find()}.
366
     *
367
     * @param int $key ActiveRecord identifier.
368
     *
369
     * @return TValue
370
     */
371
    public function offsetGet(mixed $key): ActiveRecord
372
    {
373
        return $this->find($key);
8✔
374
    }
375

376
    /**
377
     * Creates a new ActiveRecord instance.
378
     *
379
     * The class of the instance is defined by the {@link $activerecord_class} property.
380
     *
381
     * @param array<string, mixed> $properties Optional properties to instantiate the record with.
382
     *
383
     * @retrun TValue
384
     */
385
    public function new(array $properties = []): ActiveRecord
386
    {
387
        $class = $this->activerecord_class;
2✔
388

389
        return $properties ? $class::from($properties, [ $this ]) : new $class($this);
2✔
390
    }
391
}
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