Test Failed
Pull Request — master (#21)
by Vincent
06:04
created

RepositoryQueryFactory   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 32
eloc 82
c 1
b 0
f 0
dl 0
loc 355
ccs 79
cts 79
cp 1
rs 9.84

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A builder() 0 3 1
A fromAlias() 0 3 1
A keyValue() 0 13 3
A __call() 0 7 2
A extension() 0 7 2
A isPrimaryKeyFilter() 0 15 4
A make() 0 3 1
A findById() 0 25 5
A countKeyValue() 0 17 4
A configure() 0 19 4
A entities() 0 20 4
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\Query\KeyValueQueryInterface;
14
use Bdf\Prime\Query\Contract\ReadOperation;
15
use Bdf\Prime\Query\QueryInterface;
16
use Bdf\Prime\Query\QueryRepositoryExtension;
17
use Bdf\Prime\Query\ReadCommandInterface;
18
19
/**
20
 * Factory for repository queries
21
 */
22
class RepositoryQueryFactory
23
{
24
    /**
25
     * @var RepositoryInterface
26
     */
27
    private $repository;
28
29
    /**
30
     * @var ConnectionInterface
31
     */
32
    private $connection;
33
34
    /**
35
     * @var Metadata
36
     */
37
    private $metadata;
38
39
    /**
40
     * @var callable[]
41
     */
42
    private $queries;
43
44
    /**
45
     * Query result cache
46
     *
47
     * @var CacheInterface
48
     */
49
    private $resultCache;
50
51
    /**
52
     * Check if the repository can support optimised KeyValue query
53
     * 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...
54
     *
55
     * @var bool
0 ignored issues
show
Bug introduced by
Expected "boolean" but found "bool" for @var tag in member variable comment
Loading history...
56
     */
57
    private $supportsKeyValue;
58
59
    //===============//
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// ===============//" but found "//===============//"
Loading history...
60
    // Optimisations //
61
    //===============//
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// ===============//" but found "//===============//"
Loading history...
62
63
    /**
64
     * @var KeyValueQueryInterface
65
     */
66
    private $findByIdQuery;
0 ignored issues
show
Coding Style introduced by
Expected 1 blank line(s) before member var; 5 found
Loading history...
67
68
    /**
69
     * @var KeyValueQueryInterface
70
     */
71
    private $countKeyValueQuery;
72
73
    /**
74
     * Save extension instance for optimisation
75
     *
76
     * @var QueryRepositoryExtension
77
     */
78
    private $extension;
79
80
81
    /**
82
     * RepositoryQueryFactory constructor.
83
     *
84
     * @param RepositoryInterface $repository
85
     * @param CacheInterface|null $resultCache
86
     */
87 141
    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...
88
    {
89 141
        $this->repository = $repository;
90 141
        $this->resultCache = $resultCache;
91
92 141
        $this->supportsKeyValue = empty($repository->constraints());
93
94 141
        $this->connection = $repository->connection();
95 141
        $this->queries = $repository->mapper()->queries();
96 141
        $this->metadata = $repository->metadata();
97 141
    }
98
99
    /**
100
     * Get query builder
101
     *
102
     * @return QueryInterface
103
     */
104 397
    public function builder()
105
    {
106 397
        return $this->fromAlias();
107
    }
108
109
    /**
110
     * Get query builder with a defined table alias on FROM clause
111
     *
112
     * @param string|null $alias The FROM table alias
113
     *
114
     * @return QueryInterface
115
     *
116 144
     * @throws PrimeException
0 ignored issues
show
introduced by
Comment missing for @throws tag in function comment
Loading history...
117
     */
118 144
    public function fromAlias(?string $alias = null)
119
    {
120
        return $this->configure($this->connection->builder(new OrmPreprocessor($this->repository)), $alias);
121
    }
122
123
    /**
124
     * Make a query
125
     *
126
     * @param string $query The query name or class name to make
127
     *
128
     * @return CommandInterface
129
     */
130
    public function make($query)
131
    {
132
        return $this->configure($this->connection->make($query, new OrmPreprocessor($this->repository)));
133
    }
134
135 36
    /**
136
     * Find entity by its primary key
137
     *
138 36
     * <code>
139 1
     * $queries->findById(2);
140
     * $queries->findById(['key1' => 1, 'key2' => 5]);
141 35
     * </code>
142 21
     *
143
     * @param array|string $id The entity PK. Use an array for composite PK
144
     *
145 35
     * @return mixed The entity or null if not found
146
     * @throws PrimeException When query fail
147
     */
148 36
    #[ReadOperation]
0 ignored issues
show
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...
Coding Style introduced by
Perl-style comments are not allowed. Use "// Comment." or "/* comment */" instead.
Loading history...
149 31
    public function findById($id)
0 ignored issues
show
Coding Style introduced by
You must use "/**" style comments for a function comment
Loading history...
150 2
    {
151
        // Create a new query if cache is disabled
152
        if (!$this->supportsKeyValue) {
153 30
            $query = $this->builder();
154
        } else {
155 6
            if (!$this->findByIdQuery) {
156 6
                $this->findByIdQuery = $this->keyValue();
157
            }
158
159 35
            $query = $this->findByIdQuery;
160
        }
161
162
        if (is_array($id)) {
163
            if (!$this->isPrimaryKeyFilter($id)) {
164
                throw new QueryException('Only primary keys must be passed to findById()');
165
            }
166
167
            $query->where($id);
168
        } else {
169
            list($identifierName) = $this->metadata->primary['attributes'];
170
            $query->where($identifierName, $id);
171
        }
172
173
        return $query->first();
174
    }
175
176
    /**
177
     * Create a query for perform simple key / value search on the current repository
178
     *
179
     * /!\ Key value query cannot perform join queries (condition on relation is not allowed)
180
     *     And can perform only equality comparison
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
181
     *
182
     * <code>
183
     * // Search by name
184 103
     * $queries->keyValue('name', 'myName')->first();
185
     *
186 103
     * // Get an empty key value query
187 11
     * $queries->keyValue()->where(...);
188
     *
189
     * // With criteria
190 94
     * $queries->keyValue(['name' => 'John', 'customer.id' => 5])->all();
191
     * </code>
192 94
     *
193 25
     * @param string|array|null $attribute The search attribute, or criteria
194
     * @param mixed $value The search value
195
     *
196 94
     * @return KeyValueQueryInterface|null The query, or null if not supported
197
     */
198
    public function keyValue($attribute = null, $value = null)
199
    {
200
        if (!$this->supportsKeyValue) {
201
            return null;
202
        }
203
204
        $query = $this->make(KeyValueQueryInterface::class);
205
206
        if ($attribute) {
207
            $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

207
            $query->/** @scrutinizer ignore-call */ 
208
                    where($attribute, $value);
Loading history...
208
        }
209
210
        return $query;
211
    }
212
213
    /**
214
     * Count rows on the current table with simple key / value search
215
     *
216
     * /!\ Key value query cannot perform join queries (condition on relation is not allowed)
217
     *     And can perform only equality comparison
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
218
     *
219
     * <code>
220 47
     * // Count entities with myName as name value
221
     * $queries->countKeyValue('name', 'myName');
222 47
     *
223 8
     * // With criteria
224
     * $queries->countKeyValue(['name' => 'John', 'customer.id' => 5]);
225 45
     * </code>
226 26
     *
227
     * @param string|array|null $attribute The search attribute, or criteria
228
     * @param mixed $value The search value
229 45
     *
230
     * @return int
231
     * @throws PrimeException When query fail
232 47
     */
233 46
    #[ReadOperation]
0 ignored issues
show
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...
Coding Style introduced by
Perl-style comments are not allowed. Use "// Comment." or "/* comment */" instead.
Loading history...
234
    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...
235
    {
236 47
        if (!$this->supportsKeyValue) {
237
            $query = $this->builder();
238
        } else {
239
            if (!$this->countKeyValueQuery) {
240
                $this->countKeyValueQuery = $this->keyValue();
241
            }
242
243
            $query = $this->countKeyValueQuery;
244
        }
245
246
        if ($attribute) {
247
            $query->where($attribute, $value);
248
        }
249
250
        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

250
        return $query->/** @scrutinizer ignore-call */ count();
Loading history...
251
    }
252
253
    /**
254
     * Create a query selecting all entities
255
     *
256
     * This method will configure query like :
257
     * SELECT * FROM entity WHERE pk IN (entity1.pk, entity2.pk, ...)
258
     *
259 11
     * /!\ All entities MUST have a valid primary key !
260
     *
261 11
     * <code>
262
     * // True if modifications occurs on database
263 11
     * $hasChanges = ($entities != $queries->entities($entities)->all());
264 3
     *
265 3
     * // Delete all entities
266
     * $queries->entities($entities)->delete();
267
     * </code>
268 8
     *
269 8
     * @param object[] $entities Array of entities to select
270
     *
271 8
     * @return QueryInterface
272 8
     */
273
    public function entities(array $entities)
274
    {
275 8
        $query = $this->repository->queries()->builder();
276
277
        if ($this->repository->mapper()->metadata()->isCompositePrimaryKey()) {
278 11
            foreach ($entities as $entity) {
279
                $query->orWhere($this->repository->mapper()->primaryCriteria($entity));
280
            }
281
        } else {
282
            $attribute = $this->repository->mapper()->metadata()->primary['attributes'][0];
283
            $keys = [];
284
285
            foreach ($entities as $entity) {
286
                $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

286
                /** @scrutinizer ignore-call */ 
287
                $keys[] = $this->repository->extractOne($entity, $attribute);
Loading history...
287
            }
288
289 109
            $query->where($attribute, 'in', $keys);
290
        }
291 109
292 1
        return $query;
293
    }
294
295 108
    /**
296
     * Delegates call to corresponding query
297
     *
298
     * @param string $name
299
     * @param string $arguments
300
     *
301
     * @return mixed
302
     */
303
    public function __call($name, $arguments)
304
    {
305 454
        if (isset($this->queries[$name])) {
306
            return $this->queries[$name]($this->repository, ...$arguments);
307 454
        }
308 5
309
        return $this->builder()->$name(...$arguments);
310
    }
311 454
312 454
    /**
313
     * Configure the query for the current repository
314 454
     *
315 438
     * @param CommandInterface $query
316 438
     * @param string|null $alias The FROM table alias
317
     *
318
     * @return CommandInterface
319 454
     */
320 454
    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...
321
    {
322
        if ($this->metadata->useQuoteIdentifier) {
323 454
            $query->useQuoteIdentifier();
324
        }
325
326
        $query->setCustomFilters($this->repository->mapper()->filters());
327
        $query->from($this->metadata->table, $alias);
328
329
        if ($query instanceof ReadCommandInterface) {
330
            $query->setCollectionFactory($this->repository->collectionFactory());
331 438
            $this->extension()->apply($query);
332
        }
333 438
334 107
        if ($query instanceof Cachable) {
335
            $query->setCache($this->resultCache);
336
        }
337 438
338
        return $query;
339
    }
340
341
    /**
342
     * Optimise query extension creation
343
     *
344
     * @return QueryRepositoryExtension
345
     */
346
    private function extension()
0 ignored issues
show
Coding Style introduced by
Private method name "RepositoryQueryFactory::extension" must be prefixed with an underscore
Loading history...
347 31
    {
348
        if (!$this->extension) {
349 31
            $this->extension = new QueryRepositoryExtension($this->repository);
350
        }
351 31
352 1
        return clone $this->extension;
353
    }
354
355 31
    /**
356 31
     * Check if the given filter exactly match with primary key attributes
357 31
     *
358
     * @param array $filter
359
     *
360
     * @return bool
0 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
361 30
     */
362
    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...
363
    {
364
        $pk = $this->metadata->primary['attributes'];
365
366
        if (count($filter) !== count($pk)) {
367
            return false;
368
        }
369
370
        foreach ($pk as $key) {
371
            if (!isset($filter[$key])) {
372
                return false;
373
            }
374
        }
375
376
        return true;
377
    }
378
}
379