Passed
Push — master ( c7ca8a...355930 )
by Anton
02:47
created

Select::limit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
namespace Spiral\Cycle;
11
12
use Spiral\Cycle\Heap\Node;
13
use Spiral\Cycle\Parser\OutputNode;
14
use Spiral\Cycle\Select\ConstrainInterface;
15
use Spiral\Cycle\Select\LoaderInterface;
16
use Spiral\Cycle\Select\QueryBuilder;
17
use Spiral\Cycle\Select\RootLoader;
18
use Spiral\Database\Query\SelectQuery;
19
use Spiral\Pagination\PaginableInterface;
20
21
/**
22
 * Query builder and entity selector. Mocks SelectQuery. Attention, Selector does not mount RootLoader scope by default.
23
 *
24
 * Trait provides the ability to transparently configure underlying loader query.
25
 *
26
 * @method $this distinct()
27
 * @method $this where(...$args);
28
 * @method $this andWhere(...$args);
29
 * @method $this orWhere(...$args);
30
 * @method $this having(...$args);
31
 * @method $this andHaving(...$args);
32
 * @method $this orHaving(...$args);
33
 * @method $this orderBy($expression, $direction = 'ASC');
34
 *
35
 * @method int avg($identifier) Perform aggregation (AVG) based on column or expression value.
36
 * @method int min($identifier) Perform aggregation (MIN) based on column or expression value.
37
 * @method int max($identifier) Perform aggregation (MAX) based on column or expression value.
38
 * @method int sum($identifier) Perform aggregation (SUM) based on column or expression value.
39
 */
40
class Select implements \IteratorAggregate, \Countable, PaginableInterface
41
{
42
    /** @var ORMInterface @internal */
43
    private $orm;
44
45
    /** @var LoaderInterface */
46
    private $loader;
47
48
    /** @var QueryBuilder */
49
    private $builder;
50
51
    /**
52
     * @param ORMInterface $orm
53
     * @param string       $role
54
     */
55
    public function __construct(ORMInterface $orm, string $role)
56
    {
57
        $this->orm = $orm;
58
        $this->loader = new RootLoader($orm, $this->orm->resolveRole($role));
59
        $this->builder = new QueryBuilder($this->orm, $this->getLoader()->getQuery(), $this->loader);
60
    }
61
62
    /**
63
     * Create new Selector with applied scope.
64
     *
65
     * @param ConstrainInterface|null $constrain
66
     * @return Select
67
     */
68
    public function constrain(ConstrainInterface $constrain = null): self
69
    {
70
        $this->loader->setConstrain($constrain);
0 ignored issues
show
Bug introduced by
The method setConstrain() does not exist on Spiral\Cycle\Select\LoaderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Spiral\Cycle\Select\LoaderInterface. ( Ignorable by Annotation )

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

70
        $this->loader->/** @scrutinizer ignore-call */ 
71
                       setConstrain($constrain);
Loading history...
71
72
        return $this;
73
    }
74
75
    /**
76
     * Get Query proxy.
77
     *
78
     * @return QueryBuilder
79
     */
80
    public function getBuilder(): QueryBuilder
81
    {
82
        return $this->builder;
83
    }
84
85
    /**
86
     * Compiled SQL query, changes in this query would not affect Selector state (but binded parameters will).
87
     *
88
     * @return SelectQuery
89
     */
90
    public function buildQuery(): SelectQuery
91
    {
92
        return $this->getLoader()->buildQuery();
93
    }
94
95
    /**
96
     * Shortcut to where method to set AND condition for entity primary key.
97
     *
98
     * @param string|int $id
99
     * @return $this|self
100
     */
101
    public function wherePK($id): self
102
    {
103
        return $this->__call('where', [$this->getLoader()->getPK(), $id]);
104
    }
105
106
    /**
107
     * Attention, column will be quoted by driver!
108
     *
109
     * @param string|null $column When column is null DISTINCT(PK) will be generated.
110
     * @return int
111
     */
112
    public function count(string $column = null): int
113
    {
114
        if (is_null($column)) {
115
            // @tuneyourserver solves the issue with counting on queries with joins.
116
            $column = sprintf("DISTINCT(%s)", $this->getLoader()->getPK());
117
        }
118
119
        return (int)$this->__call('count', [$column]);
120
    }
121
122
    /**
123
     * @inheritdoc
124
     */
125
    public function limit(int $limit): self
126
    {
127
        $this->loader->getQuery()->distinct();
0 ignored issues
show
Bug introduced by
The method getQuery() does not exist on Spiral\Cycle\Select\LoaderInterface. It seems like you code against a sub-type of Spiral\Cycle\Select\LoaderInterface such as Spiral\Cycle\Select\RootLoader. ( Ignorable by Annotation )

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

127
        $this->loader->/** @scrutinizer ignore-call */ 
128
                       getQuery()->distinct();
Loading history...
128
        $this->loader->getQuery()->limit($limit);
129
130
        return $this;
131
    }
132
133
    /**
134
     * @inheritdoc
135
     */
136
    public function offset(int $offset): self
137
    {
138
        $this->loader->getQuery()->offset($offset);
139
140
        return $this;
141
    }
142
143
    /**
144
     * Bypassing call to primary select query.
145
     *
146
     * @param string $name
147
     * @param array  $arguments
148
     * @return $this|mixed
149
     */
150
    public function __call(string $name, array $arguments)
151
    {
152
        if (in_array(strtoupper($name), ['AVG', 'MIN', 'MAX', 'SUM', 'COUNT'])) {
153
            $proxy = new QueryBuilder($this->orm, $this->loader->buildQuery(), $this->loader);
0 ignored issues
show
Bug introduced by
The method buildQuery() does not exist on Spiral\Cycle\Select\LoaderInterface. It seems like you code against a sub-type of Spiral\Cycle\Select\LoaderInterface such as Spiral\Cycle\Select\RootLoader. ( Ignorable by Annotation )

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

153
            $proxy = new QueryBuilder($this->orm, $this->loader->/** @scrutinizer ignore-call */ buildQuery(), $this->loader);
Loading history...
154
155
            // aggregations
156
            return $proxy->__call($name, $arguments);
157
        }
158
159
        $result = $this->getBuilder()->__call($name, $arguments);
160
        if ($result instanceof QueryBuilder) {
161
            return $this;
162
        }
163
164
        return $result;
165
    }
166
167
    /**
168
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
169
     * for data pre-loading. ORM loaders by default will select the most efficient way to load
170
     * related data which might include additional select query or left join. Loaded data will
171
     * automatically pre-populate record relations. You can specify nested relations using "."
172
     * separator.
173
     *
174
     * Examples:
175
     *
176
     * // Select users and load their comments (will cast 2 queries, HAS_MANY comments)
177
     * User::find()->with('comments');
178
     *
179
     * // You can load chain of relations - select user and load their comments and post related to
180
     * //comment
181
     * User::find()->with('comments.post');
182
     *
183
     * // We can also specify custom where conditions on data loading, let's load only public
184
     * // comments.
185
     * User::find()->load('comments', [
186
     *      'where' => ['{@}.status' => 'public']
187
     * ]);
188
     *
189
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
190
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
191
     *
192
     * // In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
193
     * // conditions, let's pre-load all approved user tags, we can use same placeholder for pivot
194
     * // table alias
195
     * User::find()->load('tags', [
196
     *      'wherePivot' => ['{@}.approved' => true]
197
     * ]);
198
     *
199
     * // In most of cases you don't need to worry about how data was loaded, using external query
200
     * // or left join, however if you want to change such behaviour you can force load method to
201
     * // INLOAD
202
     * User::find()->load('tags', [
203
     *      'method'     => Loader::INLOAD,
204
     *      'wherePivot' => ['{@}.approved' => true]
205
     * ]);
206
     *
207
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
208
     * support different loading types.
209
     *
210
     * You can specify multiple loaders using array as first argument.
211
     *
212
     * Example:
213
     * User::find()->load(['posts', 'comments', 'profile']);
214
     *
215
     * Attention, consider disabling entity map if you want to use recursive loading (i.e
216
     * post.tags.posts), but first think why you even need recursive relation loading.
217
     *
218
     * @see with()
219
     * @param string|array $relation
220
     * @param array        $options
221
     * @return $this|self
222
     */
223
    public function load($relation, array $options = []): self
224
    {
225
        if (is_string($relation)) {
226
            $this->loader->loadRelation($relation, $options);
0 ignored issues
show
Bug introduced by
The method loadRelation() does not exist on Spiral\Cycle\Select\LoaderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Spiral\Cycle\Select\LoaderInterface. ( Ignorable by Annotation )

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

226
            $this->loader->/** @scrutinizer ignore-call */ 
227
                           loadRelation($relation, $options);
Loading history...
227
228
            return $this;
229
        }
230
231
        foreach ($relation as $name => $subOption) {
232
            if (is_string($subOption)) {
233
                // array of relation names
234
                $this->load($subOption, $options);
235
            } else {
236
                // multiple relations or relation with addition load options
237
                $this->load($name, $subOption + $options);
238
            }
239
        }
240
241
        return $this;
242
    }
243
244
    /**
245
     * With method is very similar to load() one, except it will always include related data to
246
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
247
     * using same database as parent record.
248
     *
249
     * Method generally used to filter data based on some relation condition. Attention, with()
250
     * method WILL NOT load relation data, it will only make it accessible in query.
251
     *
252
     * By default joined tables will be available in query based on relation name, you can change
253
     * joined table alias using relation option "alias".
254
     *
255
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
256
     * other scenario you will not able to paginate data well.
257
     *
258
     * Examples:
259
     *
260
     * // Find all users who have comments comments
261
     * User::find()->with('comments');
262
     *
263
     * // Find all users who have approved comments (we can use comments table alias in where
264
     * statement). User::find()->with('comments')->where('comments.approved', true);
265
     *
266
     * // Find all users who have posts which have approved comments
267
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
268
     *
269
     * // Custom join alias for post comments relation
270
     * $user->with('posts.comments', [
271
     *      'as' => 'comments'
272
     * ])->where('comments.approved', true);
273
     *
274
     * // If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
275
     * // name plus "_pivot" postfix. Let's load all users with approved tags.
276
     * $user->with('tags')->where('tags_pivot.approved', true);
277
     *
278
     * // You can also use custom alias for pivot table as well
279
     * User::find()->with('tags', [
280
     *      'pivotAlias' => 'tags_connection'
281
     * ])
282
     * ->where('tags_connection.approved', false);
283
     *
284
     * You can safely combine with() and load() methods.
285
     *
286
     * // Load all users with approved comments and pre-load all their comments
287
     * User::find()->with('comments')->where('comments.approved', true)->load('comments');
288
     *
289
     * // You can also use custom conditions in this case, let's find all users with approved
290
     * // comments and pre-load such approved comments
291
     * User::find()->with('comments')->where('comments.approved', true)
292
     *             ->load('comments', [
293
     *                  'where' => ['{@}.approved' => true]
294
     *              ]);
295
     *
296
     * // As you might notice previous construction will create 2 queries, however we can simplify
297
     * // this construction to use already joined table as source of data for relation via "using"
298
     * // keyword
299
     * User::find()->with('comments')
300
     *             ->where('comments.approved', true)
301
     *             ->load('comments', ['using' => 'comments']);
302
     *
303
     * // You will get only one query with INNER JOIN, to better understand this example let's use
304
     * // custom alias for comments in with() method.
305
     * User::find()->with('comments', ['as' => 'commentsR'])
306
     *             ->where('commentsR.approved', true)
307
     *             ->load('comments', ['using' => 'commentsR']);
308
     *
309
     * @see load()
310
     * @param string|array $relation
311
     * @param array        $options
312
     * @return $this|self
313
     */
314
    public function with($relation, array $options = []): self
315
    {
316
        if (is_string($relation)) {
317
            $this->loader->loadRelation($relation, $options, true);
318
319
            return $this;
320
        }
321
322
        foreach ($relation as $name => $options) {
323
            if (is_string($options)) {
324
                //Array of relation names
325
                $this->with($options, []);
326
            } else {
327
                //Multiple relations or relation with addition load options
328
                $this->with($name, $options);
329
            }
330
        }
331
332
        return $this;
333
    }
334
335
    /**
336
     * Find one entity or return null. Method provides the ability to configure custom query
337
     * parameters. Attention, method does not set a limit on selection (to avoid underselection of
338
     * joined tables), make sure to set the constrain in the query.
339
     *
340
     * @param array|null $query
341
     * @return object|null
342
     */
343
    public function fetchOne(array $query = null)
344
    {
345
        $data = (clone $this)->where($query)->fetchData();
346
347
        if (empty($data[0])) {
348
            return null;
349
        }
350
351
        return $this->orm->make($this->loader->getTarget(), $data[0], Node::MANAGED);
352
    }
353
354
    /**
355
     * Fetch all records in a form of array.
356
     *
357
     * @return object[]
358
     */
359
    public function fetchAll(): array
360
    {
361
        return iterator_to_array($this->getIterator());
362
    }
363
364
    /**
365
     * @return Iterator
366
     */
367
    public function getIterator(): Iterator
368
    {
369
        return new Iterator($this->orm, $this->loader->getTarget(), $this->fetchData());
370
    }
371
372
    /**
373
     * Load data tree from database and linked loaders in a form of array.
374
     *
375
     * @param OutputNode $node When empty node will be created automatically by root relation
376
     *                         loader.
377
     * @return array
378
     */
379
    public function fetchData(OutputNode $node = null): array
380
    {
381
        $node = $node ?? $this->loader->createNode();
382
        $this->loader->loadData($node);
383
384
        return $node->getResult();
0 ignored issues
show
Bug introduced by
The method getResult() does not exist on Spiral\Cycle\Parser\AbstractNode. It seems like you code against a sub-type of Spiral\Cycle\Parser\AbstractNode such as Spiral\Cycle\Parser\OutputNode. ( Ignorable by Annotation )

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

384
        return $node->/** @scrutinizer ignore-call */ getResult();
Loading history...
385
    }
386
387
    /**
388
     * Compiled SQL statement.
389
     *
390
     * @return string
391
     */
392
    public function sqlStatement(): string
393
    {
394
        return $this->buildQuery()->sqlStatement();
395
    }
396
397
    /**
398
     * Cloning with loader tree cloning.
399
     *
400
     * @attention at this moment binded query parameters would't be cloned!
401
     */
402
    public function __clone()
403
    {
404
        $this->loader = clone $this->loader;
405
        $this->builder = new QueryBuilder($this->orm, $this->getLoader()->getQuery(), $this->loader);
406
    }
407
408
    /**
409
     * Remove nested loaders and clean ORM link.
410
     */
411
    public function __destruct()
412
    {
413
        $this->orm = null;
414
        $this->loader = null;
415
        $this->builder = null;
416
    }
417
418
    /**
419
     * Return base loader associated with the selector.
420
     *
421
     * @return RootLoader
422
     */
423
    protected function getLoader(): RootLoader
424
    {
425
        return $this->loader;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->loader returns the type Spiral\Cycle\Select\LoaderInterface which includes types incompatible with the type-hinted return Spiral\Cycle\Select\RootLoader.
Loading history...
426
    }
427
}