Completed
Pull Request — master (#25)
by Vincent
08:17
created

RepositoryQueryFactory::paginatorFactory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
namespace Bdf\Prime\Repository;
4
5
use Bdf\Prime\Cache\CacheInterface;
6
use Bdf\Prime\Connection\ConnectionInterface;
7
use Bdf\Prime\Exception\PrimeException;
8
use Bdf\Prime\Exception\QueryException;
9
use Bdf\Prime\Mapper\Metadata;
10
use Bdf\Prime\Query\CommandInterface;
11
use Bdf\Prime\Query\Compiler\Preprocessor\OrmPreprocessor;
12
use Bdf\Prime\Query\Contract\Cachable;
13
use Bdf\Prime\Query\Contract\Paginable;
14
use Bdf\Prime\Query\Contract\Query\KeyValueQueryInterface;
15
use Bdf\Prime\Query\Contract\ReadOperation;
16
use Bdf\Prime\Query\Pagination\PaginatorFactory;
17
use Bdf\Prime\Query\Pagination\Walker;
18
use Bdf\Prime\Query\Pagination\WalkStrategy\KeyWalkStrategy;
19
use Bdf\Prime\Query\Pagination\WalkStrategy\MapperPrimaryKey;
20
use Bdf\Prime\Query\QueryInterface;
21
use Bdf\Prime\Query\QueryRepositoryExtension;
22
use Bdf\Prime\Query\ReadCommandInterface;
23
24
/**
25
 * Factory for repository queries
26
 */
27
class RepositoryQueryFactory
28
{
29
    /**
30
     * @var RepositoryInterface
31
     */
32
    private $repository;
33
34
    /**
35
     * @var ConnectionInterface
36
     */
37
    private $connection;
38
39
    /**
40
     * @var Metadata
41
     */
42
    private $metadata;
43
44
    /**
45
     * @var callable[]
46
     */
47
    private $queries;
48
49
    /**
50
     * Query result cache
51
     *
52
     * @var CacheInterface
53
     */
54
    private $resultCache;
55
56
    /**
57
     * Check if the repository can support optimised KeyValue query
58
     * If this value is false, keyValue() must returns null
0 ignored issues
show
introduced by
Doc comment short description must be on a single line, further text should be a separate paragraph
Loading history...
59
     *
60
     * @var bool
0 ignored issues
show
Bug introduced by
Expected "boolean" but found "bool" for @var tag in member variable comment
Loading history...
61
     */
62
    private $supportsKeyValue;
63
64
    //===============//
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// ===============//" but found "//===============//"
Loading history...
65
    // Optimisations //
66
    //===============//
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// ===============//" but found "//===============//"
Loading history...
67
68
    /**
69
     * @var KeyValueQueryInterface
70
     */
71
    private $findByIdQuery;
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line(s) before member var; 5 found
Loading history...
72
73
    /**
74
     * @var KeyValueQueryInterface
75
     */
76
    private $countKeyValueQuery;
77
78
    /**
79
     * Save extension instance for optimisation
80
     *
81
     * @var QueryRepositoryExtension
82
     */
83
    private $extension;
84
85
    /**
86
     * Save paginator factory instance for optimisation
87
     *
88
     * @var PaginatorFactory
89
     */
90
    private $paginatorFactory;
91
92
93
    /**
94
     * RepositoryQueryFactory constructor.
95
     *
96
     * @param RepositoryInterface $repository
97
     * @param CacheInterface|null $resultCache
98
     */
99 161
    public function __construct(RepositoryInterface $repository, ?CacheInterface $resultCache = null)
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line before function; 2 found
Loading history...
100
    {
101 161
        $this->repository = $repository;
102 161
        $this->resultCache = $resultCache;
103
104 161
        $this->supportsKeyValue = empty($repository->constraints());
105
106 161
        $this->connection = $repository->connection();
107 161
        $this->queries = $repository->mapper()->queries();
108 161
        $this->metadata = $repository->metadata();
109 161
    }
110
111
    /**
112
     * Get query builder
113
     *
114
     * @return QueryInterface
115
     */
116 418
    public function builder()
117
    {
118 418
        return $this->fromAlias();
119
    }
120
121
    /**
122
     * Get query builder with a defined table alias on FROM clause
123
     *
124
     * @param string|null $alias The FROM table alias
125
     *
126
     * @return QueryInterface
127
     *
128
     * @throws PrimeException
0 ignored issues
show
introduced by
Comment missing for @throws tag in function comment
Loading history...
129
     */
130 419
    public function fromAlias(?string $alias = null)
131
    {
132 419
        return $this->configure($this->connection->builder(new OrmPreprocessor($this->repository)), $alias);
133
    }
134
135
    /**
136
     * Make a query
137
     *
138
     * @param string $query The query name or class name to make
139
     *
140
     * @return CommandInterface
141
     */
142 158
    public function make($query)
143
    {
144 158
        return $this->configure($this->connection->make($query, new OrmPreprocessor($this->repository)));
145
    }
146
147
    /**
148
     * Find entity by its primary key
149
     *
150
     * <code>
151
     * $queries->findById(2);
152
     * $queries->findById(['key1' => 1, 'key2' => 5]);
153
     * </code>
154
     *
155
     * @param array|string $id The entity PK. Use an array for composite PK
156
     *
157
     * @return mixed The entity or null if not found
158
     * @throws PrimeException When query fail
159
     */
160
    #[ReadOperation]
0 ignored issues
show
Coding Style introduced by
Perl-style comments are not allowed. Use "// Comment." or "/* comment */" instead.
Loading history...
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
Coding Style introduced by
Perl-style comments are not allowed; use "// Comment" instead
Loading history...
161 36
    public function findById($id)
0 ignored issues
show
Coding Style introduced by
You must use "/**" style comments for a function comment
Loading history...
162
    {
163
        // Create a new query if cache is disabled
164 36
        if (!$this->supportsKeyValue) {
165 1
            $query = $this->builder();
166
        } else {
167 35
            if (!$this->findByIdQuery) {
168 21
                $this->findByIdQuery = $this->keyValue();
169
            }
170
171 35
            $query = $this->findByIdQuery;
172
        }
173
174 36
        if (is_array($id)) {
175 31
            if (!$this->isPrimaryKeyFilter($id)) {
176 2
                throw new QueryException('Only primary keys must be passed to findById()');
177
            }
178
179 30
            $query->where($id);
180
        } else {
181 6
            list($identifierName) = $this->metadata->primary['attributes'];
182 6
            $query->where($identifierName, $id);
183
        }
184
185 35
        return $query->first();
186
    }
187
188
    /**
189
     * Create a query for perform simple key / value search on the current repository
190
     *
191
     * /!\ Key value query cannot perform join queries (condition on relation is not allowed)
192
     *     And can perform only equality comparison
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
193
     *
194
     * <code>
195
     * // Search by name
196
     * $queries->keyValue('name', 'myName')->first();
197
     *
198
     * // Get an empty key value query
199
     * $queries->keyValue()->where(...);
200
     *
201
     * // With criteria
202
     * $queries->keyValue(['name' => 'John', 'customer.id' => 5])->all();
203
     * </code>
204
     *
205
     * @param string|array|null $attribute The search attribute, or criteria
206
     * @param mixed $value The search value
207
     *
208
     * @return KeyValueQueryInterface|null The query, or null if not supported
209
     */
210 107
    public function keyValue($attribute = null, $value = null)
211
    {
212 107
        if (!$this->supportsKeyValue) {
213 11
            return null;
214
        }
215
216 98
        $query = $this->make(KeyValueQueryInterface::class);
217
218 98
        if ($attribute) {
219 25
            $query->where($attribute, $value);
0 ignored issues
show
Bug introduced by
The method where() does not exist on Bdf\Prime\Query\CommandInterface. It seems like you code against a sub-type of Bdf\Prime\Query\CommandInterface such as Bdf\Prime\Query\Contract...\KeyValueQueryInterface or Bdf\Prime\Query\QueryInterface or Bdf\Prime\Query\AbstractReadCommand. ( Ignorable by Annotation )

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

219
            $query->/** @scrutinizer ignore-call */ 
220
                    where($attribute, $value);
Loading history...
220
        }
221
222 98
        return $query;
223
    }
224
225
    /**
226
     * Count rows on the current table with simple key / value search
227
     *
228
     * /!\ Key value query cannot perform join queries (condition on relation is not allowed)
229
     *     And can perform only equality comparison
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
230
     *
231
     * <code>
232
     * // Count entities with myName as name value
233
     * $queries->countKeyValue('name', 'myName');
234
     *
235
     * // With criteria
236
     * $queries->countKeyValue(['name' => 'John', 'customer.id' => 5]);
237
     * </code>
238
     *
239
     * @param string|array|null $attribute The search attribute, or criteria
240
     * @param mixed $value The search value
241
     *
242
     * @return int
243
     * @throws PrimeException When query fail
244
     */
245
    #[ReadOperation]
0 ignored issues
show
Coding Style introduced by
Perl-style comments are not allowed; use "// Comment" instead
Loading history...
Coding Style introduced by
Perl-style comments are not allowed. Use "// Comment." or "/* comment */" instead.
Loading history...
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
246 47
    public function countKeyValue($attribute = null, $value = null)
0 ignored issues
show
Coding Style introduced by
You must use "/**" style comments for a function comment
Loading history...
247
    {
248 47
        if (!$this->supportsKeyValue) {
249 8
            $query = $this->builder();
250
        } else {
251 45
            if (!$this->countKeyValueQuery) {
252 26
                $this->countKeyValueQuery = $this->keyValue();
253
            }
254
255 45
            $query = $this->countKeyValueQuery;
256
        }
257
258 47
        if ($attribute) {
259 46
            $query->where($attribute, $value);
260
        }
261
262 47
        return $query->count();
0 ignored issues
show
Bug introduced by
The method count() does not exist on Bdf\Prime\Query\QueryInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Bdf\Prime\Query\QueryInterface. ( Ignorable by Annotation )

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

262
        return $query->/** @scrutinizer ignore-call */ count();
Loading history...
263
    }
264
265
    /**
266
     * Create a query selecting all entities
267
     *
268
     * This method will configure query like :
269
     * SELECT * FROM entity WHERE pk IN (entity1.pk, entity2.pk, ...)
270
     *
271
     * /!\ All entities MUST have a valid primary key !
272
     *
273
     * <code>
274
     * // True if modifications occurs on database
275
     * $hasChanges = ($entities != $queries->entities($entities)->all());
276
     *
277
     * // Delete all entities
278
     * $queries->entities($entities)->delete();
279
     * </code>
280
     *
281
     * @param object[] $entities Array of entities to select
282
     *
283
     * @return QueryInterface
284
     */
285 11
    public function entities(array $entities)
286
    {
287 11
        $query = $this->repository->queries()->builder();
288
289 11
        if ($this->repository->mapper()->metadata()->isCompositePrimaryKey()) {
290 3
            foreach ($entities as $entity) {
291 3
                $query->orWhere($this->repository->mapper()->primaryCriteria($entity));
292
            }
293
        } else {
294 8
            $attribute = $this->repository->mapper()->metadata()->primary['attributes'][0];
295 8
            $keys = [];
296
297 8
            foreach ($entities as $entity) {
298 8
                $keys[] = $this->repository->extractOne($entity, $attribute);
0 ignored issues
show
Bug introduced by
The method extractOne() 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

298
                /** @scrutinizer ignore-call */ 
299
                $keys[] = $this->repository->extractOne($entity, $attribute);
Loading history...
299
            }
300
301 8
            $query->where($attribute, 'in', $keys);
302
        }
303
304 11
        return $query;
305
    }
306
307
    /**
308
     * Delegates call to corresponding query
309
     *
310
     * @param string $name
311
     * @param string $arguments
312
     *
313
     * @return mixed
314
     */
315 113
    public function __call($name, $arguments)
316
    {
317 113
        if (isset($this->queries[$name])) {
318 1
            return $this->queries[$name]($this->repository, ...$arguments);
319
        }
320
321 112
        return $this->builder()->$name(...$arguments);
322
    }
323
324
    /**
325
     * Configure the query for the current repository
326
     *
327
     * @param CommandInterface $query
328
     * @param string|null $alias The FROM table alias
329
     *
330
     * @return CommandInterface
331
     */
332 477
    private function configure(CommandInterface $query, ?string $alias = null)
0 ignored issues
show
Coding Style introduced by
Private method name "RepositoryQueryFactory::configure" must be prefixed with an underscore
Loading history...
333
    {
334 477
        if ($this->metadata->useQuoteIdentifier) {
335 5
            $query->useQuoteIdentifier();
336
        }
337
338 477
        $query->setCustomFilters($this->repository->mapper()->filters());
339 477
        $query->from($this->metadata->table, $alias);
340
341 477
        if ($query instanceof ReadCommandInterface) {
342 461
            $query->setCollectionFactory($this->repository->collectionFactory());
343 461
            $this->extension()->apply($query);
344
        }
345
346 477
        if ($query instanceof Cachable) {
347 477
            $query->setCache($this->resultCache);
348
        }
349
350 477
        if ($query instanceof Paginable) {
351 450
            $query->setPaginatorFactory($this->paginatorFactory());
352
        }
353
354 477
        return $query;
355
    }
356
357
    /**
358
     * Optimise query extension creation
359
     *
360
     * @return QueryRepositoryExtension
361
     */
362 461
    private function extension()
0 ignored issues
show
Coding Style introduced by
Private method name "RepositoryQueryFactory::extension" must be prefixed with an underscore
Loading history...
363
    {
364 461
        if (!$this->extension) {
365 126
            $this->extension = new QueryRepositoryExtension($this->repository);
366
        }
367
368 461
        return clone $this->extension;
369
    }
370
371
    /**
372
     * Get the paginator factory instance
373
     *
374
     * @return PaginatorFactory
375
     */
376 450
    private function paginatorFactory(): PaginatorFactory
0 ignored issues
show
Coding Style introduced by
Private method name "RepositoryQueryFactory::paginatorFactory" must be prefixed with an underscore
Loading history...
377
    {
378 450
        if ($this->paginatorFactory) {
379 385
            return $this->paginatorFactory;
380
        }
381
382 115
        return $this->paginatorFactory = new RepositoryPaginatorFactory($this->repository);
0 ignored issues
show
Coding Style introduced by
Assignments must be the first block of code on a line
Loading history...
383
    }
384
385
    /**
386
     * Check if the given filter exactly match with primary key attributes
387
     *
388
     * @param array $filter
389
     *
390
     * @return bool
0 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
391
     */
392 31
    private function isPrimaryKeyFilter(array $filter): bool
0 ignored issues
show
Coding Style introduced by
Private method name "RepositoryQueryFactory::isPrimaryKeyFilter" must be prefixed with an underscore
Loading history...
393
    {
394 31
        $pk = $this->metadata->primary['attributes'];
395
396 31
        if (count($filter) !== count($pk)) {
397 1
            return false;
398
        }
399
400 31
        foreach ($pk as $key) {
401 31
            if (!isset($filter[$key])) {
402 31
                return false;
403
            }
404
        }
405
406 30
        return true;
407
    }
408
}
409