Completed
Push — master ( 8e9858...ee369b )
by Kirill
02:43
created

Query::__callStatic()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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 67
    public function __construct(ObjectRepository $repository = null)
85
    {
86 67
        if ($repository !== null) {
87 14
            $this->from($repository);
88
        }
89 67
    }
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 4
    public function __call(string $method, array $parameters = [])
186
    {
187 4
        if ($result = $this->callScopes($method, $parameters)) {
188 4
            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
198
     */
199 4
    private function callScopes(string $method, array $parameters = []): ?Query
200
    {
201 4
        foreach ($this->scopes as $scope) {
202 4
            if (\method_exists($scope, $method)) {
203
                /** @var Query $query */
204 4
                $query = \is_object($scope) ? $scope->$method(...$parameters) : $scope::$method(...$parameters);
205
206 4
                return $this->merge($query->clone());
207
            }
208
        }
209
210
        return null;
211
    }
212
213
    /**
214
     * Copies a set of Criteria from the child query to the parent.
215
     *
216
     * @param Query $query
217
     * @return Query
218
     */
219 4
    public function merge(Query $query): Query
220
    {
221 4
        foreach ($query->getCriteria() as $criterion) {
222 4
            $criterion->attach($this);
223
        }
224
225 4
        return $this->attach($query);
226
    }
227
228
    /**
229
     * Returns a list of selection criteria.
230
     *
231
     * @return \Generator|CriterionInterface[]
232
     */
233 67
    public function getCriteria(): \Generator
234
    {
235 67
        yield from $this->criteria;
236 35
    }
237
238
    /**
239
     * @param Query $query
240
     * @return Query
241
     */
242 4
    public function attach(Query $query): Query
243
    {
244 4
        foreach ($query->getCriteria() as $criterion) {
245 4
            $this->add($criterion);
246
        }
247
248 4
        return $this;
249
    }
250
251
    /**
252
     * Creates a new query (alias to the constructor).
253
     *
254
     * @param CriterionInterface $criterion
255
     * @return Query|$this
256
     */
257 65
    public function add(CriterionInterface $criterion): self
258
    {
259 65
        if (! $criterion->isAttached()) {
260
            $criterion->attach($this);
261
        }
262
263 65
        $this->criteria[] = $criterion;
264
265 65
        return $this;
266
    }
267
268
    /**
269
     * @return Query
270
     */
271 4
    public function clone(): Query
272
    {
273 4
        $clone = $this->create();
274
275 4
        foreach ($this->criteria as $criterion) {
276 4
            $criterion = clone $criterion;
277
278 4
            if ($criterion->isAttachedTo($this)) {
279 4
                $criterion->attach($clone);
280
            }
281
282 4
            $clone->add($criterion);
283
        }
284
285 4
        return $clone;
286
    }
287
288
    /**
289
     * Creates a new query using the current set of scopes.
290
     *
291
     * @return Query
292
     */
293 9
    public function create(): Query
294
    {
295 9
        $query = static::new()->scope(...$this->getScopes());
296
297 9
        if ($this->repository) {
298 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...
299
        }
300
301 7
        return $query;
302
    }
303
304
    /**
305
     * Adds the specified set of scopes (method groups) to the query.
306
     *
307
     * @param object|string ...$scopes
308
     * @return Query|$this
309
     */
310 33
    public function scope(...$scopes): self
311
    {
312 33
        $this->scopes = \array_merge($this->scopes, $scopes);
313
314 33
        return $this;
315
    }
316
317
    /**
318
     * Creates a new query (alias to the constructor).
319
     *
320
     * @param ObjectRepository|null $repository
321
     * @return Query
322
     */
323 67
    public static function new(ObjectRepository $repository = null): Query
324
    {
325 67
        return new static($repository);
326
    }
327
328
    /**
329
     * Returns a set of scopes for the specified query.
330
     *
331
     * @return array|ObjectRepository[]
332
     */
333 9
    public function getScopes(): array
334
    {
335 9
        return $this->scopes;
336
    }
337
338
    /**
339
     * @return void
340
     * @throws \LogicException
341
     */
342
    public function __clone()
343
    {
344
        $error = '%s not allowed. Use %s::clone() instead';
345
346
        throw new \LogicException(\sprintf($error, __METHOD__, __CLASS__));
347
    }
348
349
    /**
350
     * @param string|\Closure $filter
351
     * @return Query
352
     */
353
    public function except($filter): Query
354
    {
355
        if (\is_string($filter) && ! \is_callable($filter)) {
356
            return $this->only(function (CriterionInterface $criterion) use ($filter): bool {
357
                return ! $criterion instanceof $filter;
358
            });
359
        }
360
361
        return $this->only(function (CriterionInterface $criterion) use ($filter): bool {
362
            return ! $filter($criterion);
363
        });
364
    }
365
366
    /**
367
     * @param string|\Closure $filter
368
     * @return Query
369
     */
370
    public function only($filter): Query
371
    {
372
        $filter = $this->createFilter($filter);
373
        $copy = $this->clone();
374
        $criteria = [];
375
376
        foreach ($copy->getCriteria() as $criterion) {
377
            if ($filter($criterion)) {
378
                $criteria[] = $criterion;
379
            }
380
        }
381
382
        $copy->criteria = $criteria;
383
384
        return $copy;
385
    }
386
387
    /**
388
     * @param string|callable $filter
389
     * @return callable
390
     */
391
    private function createFilter($filter): callable
392
    {
393
        \assert(\is_string($filter) || \is_callable($filter));
394
395
        if (\is_string($filter) && ! \is_callable($filter)) {
396
            $typeOf = $filter;
397
398
            return function (CriterionInterface $criterion) use ($typeOf): bool {
399
                return $criterion instanceof $typeOf;
400
            };
401
        }
402
403
        return $filter;
404
    }
405
406
    /**
407
     * @return \Generator
408
     */
409
    public function getIterator(): \Generator
410
    {
411
        foreach ($this->get() as $result) {
412
            yield $result;
413
        }
414
    }
415
416
    /**
417
     * @return bool
418
     */
419
    public function isEmpty(): bool
420
    {
421
        return $this->criteria->count() === 0;
0 ignored issues
show
Bug introduced by
The method count cannot be called on $this->criteria (of type array<integer,object<RDS...ia\CriterionInterface>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
422
    }
423
424
    /**
425
     * @return string
426
     */
427
    public function dump(): string
428
    {
429
        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...
430
    }
431
432
    /**
433
     * @return void
434
     */
435 30
    private function bootIfNotBooted(): void
436
    {
437 30
        if (self::$booted === false) {
438 1
            self::$booted = true;
439
440 1
            $bootstrap = new Bootstrap();
441 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...
442
        }
443 30
    }
444
}
445