Passed
Pull Request — 2.1 (#71)
by Vincent
13:18 queued 06:40
created

QueryRepositoryExtension::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 10
ccs 4
cts 5
cp 0.8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2.032
1
<?php
2
3
namespace Bdf\Prime\Query;
4
5
use BadMethodCallException;
6
use Bdf\Prime\Collection\Indexer\EntityIndexer;
7
use Bdf\Prime\Connection\ConnectionInterface;
8
use Bdf\Prime\Connection\Result\ResultSetInterface;
9
use Bdf\Prime\Events;
10
use Bdf\Prime\Exception\EntityNotFoundException;
11
use Bdf\Prime\Exception\PrimeException;
12
use Bdf\Prime\Mapper\Mapper;
13
use Bdf\Prime\Mapper\Metadata;
14
use Bdf\Prime\Query\Closure\ClosureCompiler;
15
use Bdf\Prime\Query\Contract\Whereable;
16
use Bdf\Prime\Relations\Relation;
17
use Bdf\Prime\Repository\EntityRepository;
18
use Bdf\Prime\Repository\RepositoryInterface;
19
use Closure;
20
21
/**
22
 * QueryRepositoryExtension
23
 *
24
 * @template E as object
25
 */
26
class QueryRepositoryExtension extends QueryCompatExtension
27
{
28
    /**
29
     * @var RepositoryInterface<E>
30
     */
31
    protected $repository;
32
33
    /**
34
     * @var Metadata
35
     */
36
    protected $metadata;
37
38
    /**
39
     * @var Mapper<E>
40
     */
41
    protected $mapper;
42
43
    /**
44
     * @var ClosureCompiler<E>|null
45
     */
46
    protected $closureCompiler;
47
48
    /**
49
     * Array of relations to associate on entities
50
     * Contains relations and subrelations. Can be load
51
     * only on select queries
52
     *
53
     * @var array
54
     */
55
    protected $withRelations = [];
56
57
    /**
58
     * Array of relations to discard
59
     *
60
     * @var array
61
     */
62
    protected $withoutRelations = [];
63
64
    /**
65
     * Collect entities by attribute
66
     *
67
     * @var array
68
     */
69
    protected $byOptions;
70
71
72
    /**
73
     * QueryRepositoryExtension constructor.
74
     *
75
     * @param RepositoryInterface<E> $repository
76
     * @param ClosureCompiler<E>|null $closureCompiler
77
     */
78 353
    public function __construct(RepositoryInterface $repository, ?ClosureCompiler $closureCompiler = null)
79
    {
80 353
        $this->repository = $repository;
81 353
        $this->metadata = $repository->metadata();
82 353
        $this->mapper = $repository->mapper();
83 353
        $this->closureCompiler = $closureCompiler;
84
    }
85
86
    /**
87
     * Gets associated repository
88
     *
89
     * @param ReadCommandInterface<ConnectionInterface, E> $query
90
     * @param null|string $name
91
     *
92
     * @return RepositoryInterface|null
93
     */
94 86
    public function repository(ReadCommandInterface $query, $name = null)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

94
    public function repository(/** @scrutinizer ignore-unused */ ReadCommandInterface $query, $name = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
95
    {
96 86
        if ($name === null) {
97 86
            return $this->repository;
98
        }
99
100
        return $this->repository->repository($name);
101
    }
102
103
    /**
104
     * Get one entity by identifier
105
     *
106
     * @param ReadCommandInterface<ConnectionInterface, E>&Whereable $query
107
     * @param mixed         $id
108
     * @param null|string|array  $attributes
109
     *
110
     * @return E|null
111
     */
112 141
    public function get(ReadCommandInterface $query, $id, $attributes = null)
113
    {
114 141
        if (empty($id)) {
115 1
            return null;
116
        }
117
118 140
        if (!is_array($id)) {
119 139
            list($identifierName) = $this->metadata->primary['attributes'];
120 139
            $id = [$identifierName => $id];
121
        }
122
123 140
        return $query->where($id)->first($attributes);
0 ignored issues
show
Bug introduced by
The method where() does not exist on Bdf\Prime\Query\ReadCommandInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\ReadCommandInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
        return $query->/** @scrutinizer ignore-call */ where($id)->first($attributes);
Loading history...
124
    }
125
126
    /**
127
     * Get one entity or throws entity not found
128
     *
129
     * @param ReadCommandInterface<ConnectionInterface, E>&Whereable $query
130
     * @param mixed $id
131
     * @param null|string|array $attributes
132
     *
133
     * @return E
134
     *
135
     * @throws EntityNotFoundException  If entity is not found
136
     */
137 3
    public function getOrFail(ReadCommandInterface $query, $id, $attributes = null)
138
    {
139 3
        $entity = $this->get($query, $id, $attributes);
140
141 3
        if ($entity !== null) {
142 2
            return $entity;
143
        }
144
145 2
        throw new EntityNotFoundException('Cannot resolve entity identifier "'.implode('", "', (array)$id).'"');
146
    }
147
148
    /**
149
     * Get one entity or return a new one if not found in repository
150
     *
151
     * @param ReadCommandInterface<ConnectionInterface, E>&Whereable $query
152
     * @param mixed $id
153
     * @param null|string|array $attributes
154
     *
155
     * @return E
156
     */
157 2
    public function getOrNew(ReadCommandInterface $query, $id, $attributes = null)
158
    {
159 2
        $entity = $this->get($query, $id, $attributes);
160
161 2
        if ($entity !== null) {
162 2
            return $entity;
163
        }
164
165 2
        return $this->repository->entity();
166
    }
167
168
    /**
169
     * Filter entities by a predicate
170
     *
171
     * The predicate will be compiled to a where clause, instead of be called on each entity
172
     *
173
     * <code>
174
     * $query->filter(fn (User $user) => $user->enabled()); // WHERE enabled = 1
175
     * $query->filter(fn (User $user) => $user->enabled() && $user->age() > 18); // WHERE enabled = 1 AND age > 18
176
     * </code>
177
     *
178
     * @param ReadCommandInterface<ConnectionInterface, E> $query
179
     * @param Closure(E):bool $predicate The predicate. Must take the entity as parameter, and return a boolean.
180
     *
181
     * @return ReadCommandInterface<ConnectionInterface, E>
182
     */
183 1
    public function filter(ReadCommandInterface $query, Closure $predicate)
184
    {
185 1
        if (!$this->closureCompiler) {
186
            throw new BadMethodCallException('Closure filter is not enabled.');
187
        }
188
189 1
        if (!$query instanceof Whereable) {
190
            throw new BadMethodCallException('The query must implement ' . Whereable::class . ' to use filter with a closure.');
191
        }
192
193 1
        return $query->where($this->closureCompiler->compile($predicate));
194
    }
195
196
    /**
197
     * Relations to load.
198
     *
199
     * Relations with their sub relations
200
     * <code>
201
     * $query->with([
202
     *     'customer.packs',
203
     *     'permissions'
204
     * ]);
205
     * </code>
206
     *
207
     * Use char '#' for polymorphic sub relation
208
     * <code>
209
     * $query->with('target#customer.packs');
210
     * </code>
211
     *
212
     * @param ReadCommandInterface<ConnectionInterface, E> $query
213
     * @param string|array $relations
214
     *
215
     * @return ReadCommandInterface<ConnectionInterface, E>
216
     */
217 237
    public function with(ReadCommandInterface $query, $relations)
218
    {
219 237
        $this->withRelations = Relation::sanitizeRelations((array)$relations);
220
221 237
        return $query;
222
    }
223
224
    /**
225
     * Relations to discard
226
     *
227
     * @param ReadCommandInterface<ConnectionInterface, E> $query
228
     * @param string|array $relations
229
     *
230
     * @return ReadCommandInterface<ConnectionInterface, E>
231
     */
232 240
    public function without(ReadCommandInterface $query, $relations)
233
    {
234 240
        $this->withoutRelations = Relation::sanitizeWithoutRelations((array)$relations);
235
236 240
        return $query;
237
    }
238
239
    /**
240
     * Indexing entities by an attribute value
241
     * Use combine for multiple entities with same attribute value
242
     *
243
     * @param ReadCommandInterface<ConnectionInterface, E> $query
244
     * @param string  $attribute
245
     * @param boolean $combine
246
     *
247
     * @return ReadCommandInterface<ConnectionInterface, E>
248
     */
249 217
    public function by(ReadCommandInterface $query, $attribute, $combine = false)
250
    {
251 217
        $this->byOptions = [
252 217
            'attribute' => $attribute,
253 217
            'combine'   => $combine,
254 217
        ];
255
256 217
        return $query;
257
    }
258
259
    /**
260
     * Post processor for hydrating entities
261
     *
262
     * @param ResultSetInterface<array<string, mixed>> $data
263
     *
264
     * @return array
265
     * @throws PrimeException
266
     */
267 466
    public function processEntities(ResultSetInterface $data)
268
    {
269
        /** @var EntityRepository $repository */
270 466
        $repository = $this->repository;
271 466
        $hasLoadEvent = $repository->hasListeners(Events::POST_LOAD);
272
273
        // Save into local vars to ensure that value will not be changed during execution
274 466
        $withRelations = $this->withRelations;
275 466
        $withoutRelations = $this->withoutRelations;
276 466
        $byOptions = $this->byOptions;
277
278 466
        $entities = new EntityIndexer($this->mapper, $byOptions ? [$byOptions['attribute']] : []);
0 ignored issues
show
Bug introduced by
$byOptions ? array($byOp...'attribute']) : array() of type array is incompatible with the type Bdf\Prime\Collection\Indexer\list expected by parameter $indexes of Bdf\Prime\Collection\Ind...yIndexer::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

278
        $entities = new EntityIndexer($this->mapper, /** @scrutinizer ignore-type */ $byOptions ? [$byOptions['attribute']] : []);
Loading history...
279
280
        // Force loading of eager relations
281 466
        if (!empty($this->metadata->eagerRelations)) {
282 79
            $withRelations = array_merge($this->metadata->eagerRelations, $withRelations);
283
284
            // Skip relation that should not be loaded.
285 79
            foreach ($withoutRelations as $relationName => $nestedRelations) {
286
                // Only a leaf concerns this query.
287 45
                if (empty($nestedRelations)) {
288 45
                    unset($withRelations[$relationName]);
289
                }
290
            }
291
        }
292
293 466
        foreach ($data as $result) {
294 456
            $entities->push($entity = $this->mapper->prepareFromRepository($result, $repository->connection()->platform()));
295
296 456
            if ($hasLoadEvent) {
297 193
                $repository->notify(Events::POST_LOAD, [$entity, $repository]);
298
            }
299
        }
300
301 466
        foreach ($withRelations as $relationName => $relationInfos) {
302 159
            $repository->relation($relationName)->load(
303 159
                $entities,
304 159
                $relationInfos['relations'],
305 159
                $relationInfos['constraints'],
306 159
                $withoutRelations[$relationName] ?? []
307 159
            );
308
        }
309
310
        switch (true) {
311
            case $byOptions === null:
312 387
                return $entities->all();
313
314 239
            case $byOptions['combine']:
315 68
                return $entities->by($byOptions['attribute']);
316
317
            default:
318 211
                return $entities->byOverride($byOptions['attribute']);
319
        }
320
    }
321
322
    /**
323
     * Scope call
324
     * run a scope defined in repository
325
     *
326
     * @param string $name          Scope name
327
     * @param array  $arguments
328
     *
329
     * @return mixed
330
     */
331 1
    public function __call($name, $arguments)
332
    {
333
        /** @var EntityRepository $this->repository */
334 1
        $scopes = $this->repository->scopes();
0 ignored issues
show
Bug introduced by
The method scopes() does not exist on Bdf\Prime\Repository\RepositoryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Repository\RepositoryInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

334
        /** @scrutinizer ignore-call */ 
335
        $scopes = $this->repository->scopes();
Loading history...
335
336 1
        if (!isset($scopes[$name])) {
337
            throw new BadMethodCallException('Scope "' . get_class($this->mapper) . '::' . $name . '" not found');
338
        }
339
340 1
        return $scopes[$name](...$arguments);
341
    }
342
343
    /**
344
     * Configure the query
345
     *
346
     * @param ReadCommandInterface $query
347
     *
348
     * @return void
349
     */
350 678
    public function apply(ReadCommandInterface $query): void
351
    {
352 678
        $query->setExtension($this);
353 678
        $query->post([$this, 'processEntities'], false);
354
    }
355
}
356