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

ICanBoogie / ActiveRecord / 8366801652

20 Mar 2024 10:35PM UTC coverage: 84.036% (-1.7%) from 85.731%
8366801652

push

github

olvlvl
Remove query method forwarding and ArrayAccessor implementation

3 of 3 new or added lines in 3 files covered. (100.0%)

93 existing lines in 5 files now uncovered.

1395 of 1660 relevant lines covered (84.04%)

21.2 hits per line

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

97.33
/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
 * @property-read Model|null $parent Parent model.
39
 * @property-read array $all Retrieve all the records from the model.
40
 * @property-read int $count The number of records of the model.
41
 * @property-read bool $exists Whether the SQL table associated with the model exists.
42
 * @property-read ActiveRecord $one Retrieve the first record from the mode.
43
 * @property ActiveRecordCache $activerecord_cache The cache use to store activerecords.
44
 */
45
#[AllowDynamicProperties]
46
class Model extends Table
47
{
48
    /**
49
     * @var class-string<TValue>
50
     */
51
    public readonly string $activerecord_class;
52

53
    /**
54
     * @var class-string<Query<TValue>>
55
     */
56
    public readonly string $query_class;
57

58
    /**
59
     * The relations of this model to other models.
60
     */
61
    public readonly RelationCollection $relations;
62

63
    /**
64
     * Returns the records cache.
65
     *
66
     * **Note:** The method needs to be implemented through prototype bindings.
67
     */
68
    protected function lazy_get_activerecord_cache(): ActiveRecordCache
69
    {
70
        /** @phpstan-ignore-next-line */
71
        return parent::lazy_get_activerecord_cache();
14✔
72
    }
73

74
    public function __construct(
75
        Connection $connection,
76
        public readonly ModelProvider $models,
77
        private readonly ModelDefinition $definition
78
    ) {
79
        $this->activerecord_class = $this->definition->activerecord_class; // @phpstan-ignore-line
58✔
80
        $this->query_class = $this->definition->query_class;
58✔
81

82
        $parent = $this->resolve_parent($models);
58✔
83

84
        parent::__construct($connection, $definition->table, $parent);
58✔
85

86
        $this->relations = new RelationCollection($this, $this->definition->association);
58✔
87
    }
88

89
    private function resolve_parent(ModelProvider $models): ?Model
90
    {
91
        $parent_class = get_parent_class($this->activerecord_class);
58✔
92

93
        if ($parent_class === ActiveRecord::class) {
58✔
94
            return null;
58✔
95
        }
96

97
        return $models->model_for_record($parent_class); // @phpstan-ignore-line
39✔
98
    }
99

100
    /**
101
     * Handles query methods, dynamic filters, and relations.
102
     *
103
     * @inheritdoc
104
     */
105
    public function __call($method, $arguments)
106
    {
107
        if (is_callable([ $this->relations, $method ])) {
14✔
UNCOV
108
            return $this->relations->$method(...$arguments);
×
109
        }
110

111
        return parent::__call($method, $arguments);
14✔
112
    }
113

114
    /**
115
     * Finds a record or a collection of records.
116
     *
117
     * @param mixed $key A key, multiple keys, or an array of keys.
118
     *
119
     * @return TValue|TValue[] A record or a set of records.
120
     * @throws RecordNotFound when the record, or one or more records of the records
121
     * set, could not be found.
122
     *
123
     */
124
    public function find(mixed $key)
125
    {
126
        $args = func_get_args();
14✔
127
        $n = count($args);
14✔
128

129
        if (!$n) {
14✔
130
            throw new \BadMethodCallException("Expected at least one argument.");
×
131
        }
132

133
        if (count($args) == 1) {
14✔
134
            $key = $args[0];
11✔
135

136
            if (!is_array($key)) {
11✔
137
                return $this->find_one($key);
10✔
138
            }
139

140
            $args = $key;
1✔
141
        }
142

143
        return $this->find_many($args);
4✔
144
    }
145

146
    /**
147
     * Finds one records.
148
     *
149
     * @param TKey $key
150
     *
151
     * @return ActiveRecord<TValue>
152
     */
153
    private function find_one($key): ActiveRecord
154
    {
155
        $record = $this->activerecord_cache->retrieve($key);
10✔
156

157
        if ($record) {
10✔
158
            return $record;
2✔
159
        }
160

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

163
        if (!$record) {
10✔
164
            throw new RecordNotFound(
2✔
165
                "Record <q>{$key}</q> does not exist in model <q>{$this->activerecord_class}</q>.",
2✔
166
                [ $key => null ]
2✔
167
            );
2✔
168
        }
169

170
        $this->activerecord_cache->store($record);
9✔
171

172
        return $record;
9✔
173
    }
174

175
    /**
176
     * Finds many records.
177
     *
178
     * @param array<int|non-empty-string> $keys
179
     *
180
     * @return array<int|non-empty-string, TValue>
181
     */
182
    private function find_many(array $keys): array
183
    {
184
        $records = $missing = array_fill_keys($keys, null);
4✔
185

186
        foreach ($keys as $key) {
4✔
187
            $record = $this->activerecord_cache->retrieve($key);
4✔
188

189
            if (!$record) {
4✔
190
                continue;
4✔
191
            }
192

193
            $records[$key] = $record;
2✔
194
            unset($missing[$key]);
2✔
195
        }
196

197
        if ($missing) {
4✔
198
            $primary = $this->primary;
4✔
199

200
            assert(is_string($primary));
201

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

204
            foreach ($query_records as $record) {
4✔
205
                $key = $record->$primary;
3✔
206
                $records[$key] = $record;
3✔
207
                unset($missing[$key]);
3✔
208

209
                $this->activerecord_cache->store($record);
3✔
210
            }
211
        }
212

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

215
        if ($missing) {
4✔
216
            if (count($missing) > 1) {
2✔
217
                throw new RecordNotFound(
1✔
218
                    sprintf(
1✔
219
                        "Records `%s` do not exist for `%s`",
1✔
220
                        implode('`, `', array_keys($missing)),
1✔
221
                        $this->activerecord_class
1✔
222
                    ),
1✔
223
                    $records
1✔
224
                );
1✔
225
            }
226

227
            $key = array_keys($missing);
1✔
228
            $key = array_shift($key);
1✔
229

230
            throw new RecordNotFound(
1✔
231
                "Record `$key` does not exist for `$this->activerecord_class`",
1✔
232
                $records
1✔
233
            );
1✔
234
        }
235

236
        return $records;
2✔
237
    }
238

239
    /**
240
     * Returns a new query.
241
     *
242
     * @return Query<TValue>
243
     */
244
    public function query(): Query
245
    {
246
        return new $this->query_class($this);
35✔
247
    }
248

249
    /**
250
     * Returns a new query with the WHERE clause initialized with the provided conditions and arguments.
251
     *
252
     * @param ...$conditions_and_args
253
     *
254
     * @return Query<TValue>
255
     */
256
    public function where(...$conditions_and_args): Query
257
    {
258
        return $this->query()->where(...$conditions_and_args);
19✔
259
    }
260

261
    /**
262
     * Because records are cached, we need to remove the record from the cache when it is saved,
263
     * so that loading the record again returns the updated record, not the one in the cache.
264
     */
265
    public function save(array $values, mixed $id = null, array $options = []): mixed
266
    {
267
        if ($id) {
34✔
268
            $this->activerecord_cache->eliminate($id);
1✔
269
        }
270

271
        return parent::save($values, $id, $options);
34✔
272
    }
273

274
    /**
275
     * Eliminates the record from the cache.
276
     *
277
     * @inheritdoc
278
     */
279
    public function delete($key)
280
    {
281
        $this->activerecord_cache->eliminate($key);
1✔
282

283
        return parent::delete($key);
1✔
284
    }
285

286
    /**
287
     * Creates a new ActiveRecord instance.
288
     *
289
     * The class of the instance is defined by the {@link $activerecord_class} property.
290
     *
291
     * @param array<string, mixed> $properties Optional properties to instantiate the record with.
292
     *
293
     * @retrun TValue
294
     */
295
    public function new(array $properties = []): ActiveRecord
296
    {
297
        $class = $this->activerecord_class;
2✔
298

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