Query   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 414
Duplicated Lines 2.9 %

Coupling/Cohesion

Components 2
Dependencies 15

Test Coverage

Coverage 54.77%

Importance

Changes 0
Metric Value
wmc 51
lcom 2
cbo 15
dl 12
loc 414
ccs 63
cts 115
cp 0.5477
rs 7.92
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A raw() 0 4 1
A has() 0 10 3
A __get() 0 8 2
A column() 0 7 1
A getMetadata() 0 4 1
A getEntityManager() 0 4 1
A getClassName() 0 4 1
A __call() 0 8 2
A callScopes() 0 17 5
A merge() 0 8 2
A getCriteria() 0 4 1
A attach() 0 8 2
A add() 0 10 2
A clone() 0 16 3
A create() 0 10 2
A scope() 0 6 1
A new() 0 4 1
A getScopes() 0 4 1
A __clone() 0 6 1
A except() 5 12 3
A only() 0 16 3
A createFilter() 7 14 4
A getIterator() 0 6 2
A isEmpty() 0 4 1
A dump() 0 4 1
A bootIfNotBooted() 0 9 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of Hydrogen package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace RDS\Hydrogen;
11
12
use Doctrine\Common\Persistence\ObjectRepository;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use Illuminate\Support\Traits\Macroable;
16
use RDS\Hydrogen\Criteria\CriterionInterface;
17
use RDS\Hydrogen\Query\AliasProvider;
18
use RDS\Hydrogen\Query\ExecutionsProvider;
19
use RDS\Hydrogen\Query\GroupByProvider;
20
use RDS\Hydrogen\Query\LimitAndOffsetProvider;
21
use RDS\Hydrogen\Query\ModeProvider;
22
use RDS\Hydrogen\Query\OrderProvider;
23
use RDS\Hydrogen\Query\RelationProvider;
24
use RDS\Hydrogen\Query\RepositoryProvider;
25
use RDS\Hydrogen\Query\SelectProvider;
26
use RDS\Hydrogen\Query\WhereProvider;
27
28
/**
29
 * A base class for all queries, contains the execution context
30
 * and a set of methods for adding criteria to this context.
31
 *
32
 * To add new methods during runtime, you can use the
33
 * `Query::macro(..)` method.
34
 */
35
class Query implements \IteratorAggregate
36
{
37
    use Macroable {
38
        Macroable::__call as __macroableCall;
39
        Macroable::__callStatic as __macroableCallStatic;
40
    }
41
42
    use ModeProvider;
43
    use AliasProvider;
44
    use WhereProvider;
45
    use OrderProvider;
46
    use SelectProvider;
47
    use GroupByProvider;
48
    use RelationProvider;
49
    use RepositoryProvider;
50
    use ExecutionsProvider;
51
    use LimitAndOffsetProvider;
52
53
    /**
54
     * Contains the status of the download. Before any request,
55
     * you need to make sure that all the runtime is loaded.
56
     *
57
     * It is this perennial one that indicates if at least one
58
     * query has already been created in order to load the
59
     * necessary functions.
60
     *
61
     * @var bool
62
     */
63
    private static $booted = false;
64
65
    /**
66
     * A set of query criteria in a given execution context.
67
     *
68
     * @var CriterionInterface[]
69
     */
70
    protected $criteria = [];
71
72
    /**
73
     * A set of scopes (classes and objects) that have access to be
74
     * able to create a query from a set of methods defined
75
     * in the specified scopes.
76
     *
77
     * @var array|ObjectRepository[]
78
     */
79
    protected $scopes = [];
80
81
    /**
82
     * @param ObjectRepository|null $repository
83
     */
84 69
    public function __construct(ObjectRepository $repository = null)
85
    {
86 69
        if ($repository !== null) {
87 14
            $this->from($repository);
88
        }
89 69
    }
90
91
    /**
92
     * Method for creating native DB queries or query parts.
93
     *
94
     * @param string $stmt
95
     * @return string
96
     */
97
    public static function raw(string $stmt): string
98
    {
99
        return \sprintf("RAW('%s')", \addcslashes($stmt, "'"));
100
    }
101
102
    /**
103
     * The method checks for the presence of the required criterion inside the query.
104
     *
105
     * TODO Add callable argument support (like filter).
106
     *
107
     * @param string $criterion
108
     * @return bool
109
     */
110 20
    public function has(string $criterion): bool
111
    {
112 20
        foreach ($this->criteria as $haystack) {
113 18
            if (\get_class($haystack) === $criterion) {
114 18
                return true;
115
            }
116
        }
117
118 20
        return false;
119
    }
120
121
    /**
122
     * Provides the ability to directly access methods without specifying parentheses.
123
     *
124
     * TODO 1) Add High Order Messaging for methods like `->field->where(23)` instead `->where('field', 23)`
125
     * TODO 2) Allow inner access `->embedded->field->where(23)` instead `->where('embedded.field', 23)`
126
     *
127
     * @param string $name
128
     * @return null
129
     */
130 19
    public function __get(string $name)
131
    {
132 19
        if (\method_exists($this, $name)) {
133 19
            return $this->$name();
134
        }
135
136
        return null;
137
    }
138
139
    /**
140
     * Creates the ability to directly access the table's column.
141
     *
142
     * @param string $name
143
     * @return string
144
     */
145
    public function column(string $name): string
146
    {
147
        $name = \addcslashes($name, "'");
148
        $table = $this->getMetadata()->getTableName();
149
150
        return \sprintf("FIELD('%s', '%s', '%s')", $table, $this->getAlias(), $name);
151
    }
152
153
    /**
154
     * @internal For internal use only
155
     * @return ClassMetadata
156
     */
157
    public function getMetadata(): ClassMetadata
158
    {
159
        return $this->getEntityManager()->getClassMetadata($this->getClassName());
160
    }
161
162
    /**
163
     * @internal For internal use only
164
     * @return EntityManagerInterface
165
     */
166
    public function getEntityManager(): EntityManagerInterface
167
    {
168
        return $this->getRepository()->getEntityManager();
0 ignored issues
show
Bug introduced by
The method getEntityManager does only exist in RDS\Hydrogen\Hydrogen, but not in Doctrine\Common\Persistence\ObjectRepository.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
169
    }
170
171
    /**
172
     * @internal For internal use only
173
     * @return string
174
     */
175
    public function getClassName(): string
176
    {
177
        return $this->getRepository()->getClassName();
0 ignored issues
show
Bug introduced by
The method getClassName does only exist in Doctrine\Common\Persistence\ObjectRepository, but not in RDS\Hydrogen\Hydrogen.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
178
    }
179
180
    /**
181
     * @param string $method
182
     * @param array $parameters
183
     * @return mixed|$this|Query
184
     */
185 8
    public function __call(string $method, array $parameters)
186
    {
187 8
        if ($result = $this->callScopes($method, $parameters)) {
188 8
            return $result;
189
        }
190
191
        return $this->__macroableCall($method, $parameters);
192
    }
193
194
    /**
195
     * @param string $method
196
     * @param array $parameters
197
     * @return null|Query|mixed
198
     */
199 8
    private function callScopes(string $method, array $parameters = [])
200
    {
201 8
        foreach ($this->scopes as $scope) {
202 8
            if (\method_exists($scope, $method)) {
203
                /** @var Query $query */
204 8
                $query = \is_object($scope) ? $scope->$method(...$parameters) : $scope::$method(...$parameters);
205
206 8
                if ($query instanceof self) {
207 4
                    return $this->merge($query->clone());
208
                }
209
210 8
                return $query;
211
            }
212
        }
213
214
        return null;
215
    }
216
217
    /**
218
     * Copies a set of Criteria from the child query to the parent.
219
     *
220
     * @param Query $query
221
     * @return Query
222
     */
223 4
    public function merge(Query $query): Query
224
    {
225 4
        foreach ($query->getCriteria() as $criterion) {
226 4
            $criterion->attach($this);
227
        }
228
229 4
        return $this->attach($query);
230
    }
231
232
    /**
233
     * Returns a list of selection criteria.
234
     *
235
     * @return \Generator|CriterionInterface[]
236
     */
237 67
    public function getCriteria(): \Generator
238
    {
239 67
        yield from $this->criteria;
240 35
    }
241
242
    /**
243
     * @param Query $query
244
     * @return Query
245
     */
246 4
    public function attach(Query $query): Query
247
    {
248 4
        foreach ($query->getCriteria() as $criterion) {
249 4
            $this->add($criterion);
250
        }
251
252 4
        return $this;
253
    }
254
255
    /**
256
     * Creates a new query (alias to the constructor).
257
     *
258
     * @param CriterionInterface $criterion
259
     * @return Query|$this
260
     */
261 65
    public function add(CriterionInterface $criterion): self
262
    {
263 65
        if (! $criterion->isAttached()) {
264
            $criterion->attach($this);
265
        }
266
267 65
        $this->criteria[] = $criterion;
268
269 65
        return $this;
270
    }
271
272
    /**
273
     * @return Query
274
     */
275 4
    public function clone(): Query
276
    {
277 4
        $clone = $this->create();
278
279 4
        foreach ($this->criteria as $criterion) {
280 4
            $criterion = clone $criterion;
281
282 4
            if ($criterion->isAttachedTo($this)) {
283 4
                $criterion->attach($clone);
284
            }
285
286 4
            $clone->add($criterion);
287
        }
288
289 4
        return $clone;
290
    }
291
292
    /**
293
     * Creates a new query using the current set of scopes.
294
     *
295
     * @return Query
296
     */
297 9
    public function create(): Query
298
    {
299 9
        $query = static::new()->scope(...$this->getScopes());
300
301 9
        if ($this->repository) {
302 2
            return $query->from($this->repository);
0 ignored issues
show
Bug introduced by
It seems like $this->repository can also be of type object<RDS\Hydrogen\Hydrogen>; however, RDS\Hydrogen\Query\RepositoryProvider::from() does only seem to accept object<Doctrine\Common\P...tence\ObjectRepository>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
303
        }
304
305 7
        return $query;
306
    }
307
308
    /**
309
     * Adds the specified set of scopes (method groups) to the query.
310
     *
311
     * @param object|string ...$scopes
312
     * @return Query|$this
313
     */
314 37
    public function scope(...$scopes): self
315
    {
316 37
        $this->scopes = \array_merge($this->scopes, $scopes);
317
318 37
        return $this;
319
    }
320
321
    /**
322
     * Creates a new query (alias to the constructor).
323
     *
324
     * @param ObjectRepository|null $repository
325
     * @return Query
326
     */
327 69
    public static function new(ObjectRepository $repository = null): Query
328
    {
329 69
        return new static($repository);
330
    }
331
332
    /**
333
     * Returns a set of scopes for the specified query.
334
     *
335
     * @return array|ObjectRepository[]
336
     */
337 9
    public function getScopes(): array
338
    {
339 9
        return $this->scopes;
340
    }
341
342
    /**
343
     * @return void
344
     * @throws \LogicException
345
     */
346
    public function __clone()
347
    {
348
        $error = '%s not allowed. Use %s::clone() instead';
349
350
        throw new \LogicException(\sprintf($error, __METHOD__, __CLASS__));
351
    }
352
353
    /**
354
     * @param string|\Closure $filter
355
     * @return Query
356
     */
357
    public function except($filter): Query
358
    {
359 View Code Duplication
        if (\is_string($filter) && ! \is_callable($filter)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360
            return $this->only(function (CriterionInterface $criterion) use ($filter): bool {
361
                return ! $criterion instanceof $filter;
362
            });
363
        }
364
365
        return $this->only(function (CriterionInterface $criterion) use ($filter): bool {
366
            return ! $filter($criterion);
367
        });
368
    }
369
370
    /**
371
     * @param string|\Closure $filter
372
     * @return Query
373
     */
374
    public function only($filter): Query
375
    {
376
        $filter = $this->createFilter($filter);
377
        $copy = $this->clone();
378
        $criteria = [];
379
380
        foreach ($copy->getCriteria() as $criterion) {
381
            if ($filter($criterion)) {
382
                $criteria[] = $criterion;
383
            }
384
        }
385
386
        $copy->criteria = $criteria;
387
388
        return $copy;
389
    }
390
391
    /**
392
     * @param string|callable $filter
393
     * @return callable
394
     */
395
    private function createFilter($filter): callable
396
    {
397
        \assert(\is_string($filter) || \is_callable($filter));
398
399 View Code Duplication
        if (\is_string($filter) && ! \is_callable($filter)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
400
            $typeOf = $filter;
401
402
            return function (CriterionInterface $criterion) use ($typeOf): bool {
403
                return $criterion instanceof $typeOf;
404
            };
405
        }
406
407
        return $filter;
408
    }
409
410
    /**
411
     * @return \Generator
412
     */
413
    public function getIterator(): \Generator
414
    {
415
        foreach ($this->get() as $result) {
416
            yield $result;
417
        }
418
    }
419
420
    /**
421
     * @return bool
422
     */
423
    public function isEmpty(): bool
424
    {
425
        return \count($this->criteria) === 0;
426
    }
427
428
    /**
429
     * @return string
430
     */
431
    public function dump(): string
432
    {
433
        return $this->getRepository()->getProcessor()->dump($this);
0 ignored issues
show
Bug introduced by
The method getProcessor does only exist in RDS\Hydrogen\Hydrogen, but not in Doctrine\Common\Persistence\ObjectRepository.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
434
    }
435
436
    /**
437
     * @return void
438
     */
439 30
    private function bootIfNotBooted(): void
440
    {
441 30
        if (self::$booted === false) {
442 1
            self::$booted = true;
443
444 1
            $bootstrap = new Bootstrap();
445 1
            $bootstrap->register($this->getRepository()->getEntityManager());
0 ignored issues
show
Bug introduced by
The method getEntityManager does only exist in RDS\Hydrogen\Hydrogen, but not in Doctrine\Common\Persistence\ObjectRepository.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
446
        }
447 30
    }
448
}
449