Passed
Pull Request — master (#209)
by Aleksei
03:39
created

Select   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 70
c 7
b 0
f 0
dl 0
loc 402
rs 10
wmc 30

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __call() 0 15 3
A __clone() 0 4 1
A __destruct() 0 5 1
A __construct() 0 5 1
A sqlStatement() 0 3 1
A scope() 0 5 1
A limit() 0 5 1
A buildQuery() 0 3 1
A fetchData() 0 6 1
A count() 0 8 2
A wherePK() 0 3 1
A load() 0 19 4
A with() 0 19 4
A getBuilder() 0 3 1
A fetchAll() 0 3 1
A getLoader() 0 3 1
A offset() 0 5 1
A getIterator() 0 6 1
A fetchOne() 0 12 2
A constrain() 0 3 1
1
<?php
2
3
/**
4
 * Cycle DataMapper ORM
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\ORM;
13
14
use Countable;
15
use Cycle\ORM\Heap\Node;
16
use Cycle\ORM\Select\ConstrainInterface;
17
use Cycle\ORM\Select\JoinableLoader;
18
use Cycle\ORM\Select\QueryBuilder;
19
use Cycle\ORM\Select\RootLoader;
20
use Cycle\ORM\Select\ScopeInterface;
21
use IteratorAggregate;
22
use Spiral\Database\Query\SelectQuery;
23
use Spiral\Pagination\PaginableInterface;
24
25
/**
26
 * Query builder and entity selector. Mocks SelectQuery. Attention, Selector does not mount RootLoader scope by default.
27
 *
28
 * Trait provides the ability to transparently configure underlying loader query.
29
 *
30
 * @method Select distinct()
31
 * @method Select where(...$args);
32
 * @method Select andWhere(...$args);
33
 * @method Select orWhere(...$args);
34
 * @method Select having(...$args);
35
 * @method Select andHaving(...$args);
36
 * @method Select orHaving(...$args);
37
 * @method Select orderBy($expression, $direction = 'ASC');
38
 *
39
 * @method mixed avg($identifier) Perform aggregation (AVG) based on column or expression value.
40
 * @method mixed min($identifier) Perform aggregation (MIN) based on column or expression value.
41
 * @method mixed max($identifier) Perform aggregation (MAX) based on column or expression value.
42
 * @method mixed sum($identifier) Perform aggregation (SUM) based on column or expression value.
43
 */
44
final class Select implements IteratorAggregate, Countable, PaginableInterface
45
{
46
    // load relation data within same query
47
    public const SINGLE_QUERY = JoinableLoader::INLOAD;
48
49
    // load related data after the query
50
    public const OUTER_QUERY = JoinableLoader::POSTLOAD;
51
52
    /** @var ORMInterface @internal */
53
    private $orm;
54
55
    /** @var RootLoader */
56
    private $loader;
57
58
    /** @var QueryBuilder */
59
    private $builder;
60
61
    /**
62
     * @param ORMInterface $orm
63
     * @param string       $role
64
     */
65
    public function __construct(ORMInterface $orm, string $role)
66
    {
67
        $this->orm = $orm;
68
        $this->loader = new RootLoader($orm, $this->orm->resolveRole($role));
69
        $this->builder = new QueryBuilder($this->getLoader()->getQuery(), $this->loader);
70
    }
71
72
    /**
73
     * Remove nested loaders and clean ORM link.
74
     */
75
    public function __destruct()
76
    {
77
        $this->orm = null;
78
        $this->loader = null;
79
        $this->builder = null;
80
    }
81
82
    /**
83
     * Bypassing call to primary select query.
84
     *
85
     * @param string $name
86
     * @param array  $arguments
87
     * @return Select|mixed
88
     */
89
    public function __call(string $name, array $arguments)
90
    {
91
        if (in_array(strtoupper($name), ['AVG', 'MIN', 'MAX', 'SUM', 'COUNT'])) {
92
            // aggregations
93
            return $this->builder->withQuery(
94
                $this->loader->buildQuery()
95
            )->__call($name, $arguments);
96
        }
97
98
        $result = $this->builder->__call($name, $arguments);
99
        if ($result instanceof QueryBuilder) {
100
            return $this;
101
        }
102
103
        return $result;
104
    }
105
106
    /**
107
     * Cloning with loader tree cloning.
108
     *
109
     * @attention at this moment binded query parameters would't be cloned!
110
     */
111
    public function __clone()
112
    {
113
        $this->loader = clone $this->loader;
114
        $this->builder = new QueryBuilder($this->loader->getQuery(), $this->loader);
115
    }
116
117
    /**
118
     * @deprecated Will be dropped in next major release. Use {@see scope()} instead.
119
     */
120
    public function constrain(ConstrainInterface $constrain = null): self
121
    {
122
        return $this->scope($constrain);
123
    }
124
125
    /**
126
     * Create new Selector with applied scope. By default no scope used.
127
     */
128
    public function scope(ScopeInterface $scope = null): self
129
    {
130
        $this->loader->setScope($scope);
131
132
        return $this;
133
    }
134
135
    /**
136
     * Get Query proxy.
137
     *
138
     * @return QueryBuilder
139
     */
140
    public function getBuilder(): QueryBuilder
141
    {
142
        return $this->builder;
143
    }
144
145
    /**
146
     * Compiled SQL query, changes in this query would not affect Selector state (but binded parameters will).
147
     *
148
     * @return SelectQuery
149
     */
150
    public function buildQuery(): SelectQuery
151
    {
152
        return $this->loader->buildQuery();
153
    }
154
155
    /**
156
     * Shortcut to where method to set AND condition for entity primary key.
157
     *
158
     * @param string|int $id
159
     * @return $this|Select
160
     */
161
    public function wherePK($id): self
162
    {
163
        return $this->__call('where', [$this->loader->getPK(), $id]);
164
    }
165
166
    /**
167
     * Attention, column will be quoted by driver!
168
     *
169
     * @param string|null $column When column is null DISTINCT(PK) will be generated.
170
     * @return int
171
     */
172
    public function count(string $column = null): int
173
    {
174
        if ($column === null) {
175
            // @tuneyourserver solves the issue with counting on queries with joins.
176
            $column = sprintf('DISTINCT(%s)', $this->loader->getPK());
177
        }
178
179
        return (int) $this->__call('count', [$column]);
180
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185
    public function limit(int $limit): self
186
    {
187
        $this->loader->getQuery()->limit($limit);
188
189
        return $this;
190
    }
191
192
    /**
193
     * @inheritdoc
194
     */
195
    public function offset(int $offset): self
196
    {
197
        $this->loader->getQuery()->offset($offset);
198
199
        return $this;
200
    }
201
202
    /**
203
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
204
     * for data pre-loading. ORM loaders by default will select the most efficient way to load
205
     * related data which might include additional select query or left join. Loaded data will
206
     * automatically pre-populate record relations. You can specify nested relations using "."
207
     * separator.
208
     *
209
     * Examples:
210
     *
211
     * // Select users and load their comments (will cast 2 queries, HAS_MANY comments)
212
     * User::find()->with('comments');
213
     *
214
     * // You can load chain of relations - select user and load their comments and post related to
215
     * //comment
216
     * User::find()->with('comments.post');
217
     *
218
     * // We can also specify custom where conditions on data loading, let's load only public
219
     * // comments.
220
     * User::find()->load('comments', [
221
     *      'where' => ['{@}.status' => 'public']
222
     * ]);
223
     *
224
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
225
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
226
     *
227
     * // In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
228
     * // conditions, let's pre-load all approved user tags, we can use same placeholder for pivot
229
     * // table alias
230
     * User::find()->load('tags', [
231
     *      'wherePivot' => ['{@}.approved' => true]
232
     * ]);
233
     *
234
     * // In most of cases you don't need to worry about how data was loaded, using external query
235
     * // or left join, however if you want to change such behaviour you can force load method to
236
     * // INLOAD
237
     * User::find()->load('tags', [
238
     *      'method'     => Loader::INLOAD,
239
     *      'wherePivot' => ['{@}.approved' => true]
240
     * ]);
241
     *
242
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
243
     * support different loading types.
244
     *
245
     * You can specify multiple loaders using array as first argument.
246
     *
247
     * Example:
248
     * User::find()->load(['posts', 'comments', 'profile']);
249
     *
250
     * Attention, consider disabling entity map if you want to use recursive loading (i.e
251
     * post.tags.posts), but first think why you even need recursive relation loading.
252
     *
253
     * @param string|array $relation
254
     * @param array        $options
255
     * @return $this|self
256
     * @see with()
257
     */
258
    public function load($relation, array $options = []): self
259
    {
260
        if (is_string($relation)) {
261
            $this->loader->loadRelation($relation, $options, false, true);
262
263
            return $this;
264
        }
265
266
        foreach ($relation as $name => $subOption) {
267
            if (is_string($subOption)) {
268
                // array of relation names
269
                $this->load($subOption, $options);
270
            } else {
271
                // multiple relations or relation with addition load options
272
                $this->load($name, $subOption + $options);
273
            }
274
        }
275
276
        return $this;
277
    }
278
279
    /**
280
     * With method is very similar to load() one, except it will always include related data to
281
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
282
     * using same database as parent record.
283
     *
284
     * Method generally used to filter data based on some relation condition. Attention, with()
285
     * method WILL NOT load relation data, it will only make it accessible in query.
286
     *
287
     * By default joined tables will be available in query based on relation name, you can change
288
     * joined table alias using relation option "alias".
289
     *
290
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
291
     * other scenario you will not able to paginate data well.
292
     *
293
     * Examples:
294
     *
295
     * // Find all users who have comments comments
296
     * User::find()->with('comments');
297
     *
298
     * // Find all users who have approved comments (we can use comments table alias in where
299
     * statement). User::find()->with('comments')->where('comments.approved', true);
300
     *
301
     * // Find all users who have posts which have approved comments
302
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
303
     *
304
     * // Custom join alias for post comments relation
305
     * $user->with('posts.comments', [
306
     *      'as' => 'comments'
307
     * ])->where('comments.approved', true);
308
     *
309
     * // If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
310
     * // name plus "_pivot" postfix. Let's load all users with approved tags.
311
     * $user->with('tags')->where('tags_pivot.approved', true);
312
     *
313
     * // You can also use custom alias for pivot table as well
314
     * User::find()->with('tags', [
315
     *      'pivotAlias' => 'tags_connection'
316
     * ])
317
     * ->where('tags_connection.approved', false);
318
     *
319
     * You can safely combine with() and load() methods.
320
     *
321
     * // Load all users with approved comments and pre-load all their comments
322
     * User::find()->with('comments')->where('comments.approved', true)->load('comments');
323
     *
324
     * // You can also use custom conditions in this case, let's find all users with approved
325
     * // comments and pre-load such approved comments
326
     * User::find()->with('comments')->where('comments.approved', true)
327
     *             ->load('comments', [
328
     *                  'where' => ['{@}.approved' => true]
329
     *              ]);
330
     *
331
     * // As you might notice previous construction will create 2 queries, however we can simplify
332
     * // this construction to use already joined table as source of data for relation via "using"
333
     * // keyword
334
     * User::find()->with('comments')
335
     *             ->where('comments.approved', true)
336
     *             ->load('comments', ['using' => 'comments']);
337
     *
338
     * // You will get only one query with INNER JOIN, to better understand this example let's use
339
     * // custom alias for comments in with() method.
340
     * User::find()->with('comments', ['as' => 'commentsR'])
341
     *             ->where('commentsR.approved', true)
342
     *             ->load('comments', ['using' => 'commentsR']);
343
     *
344
     * @param string|array $relation
345
     * @param array        $options
346
     * @return $this|Select
347
     * @see load()
348
     */
349
    public function with($relation, array $options = []): self
350
    {
351
        if (is_string($relation)) {
352
            $this->loader->loadRelation($relation, $options, true, false);
353
354
            return $this;
355
        }
356
357
        foreach ($relation as $name => $subOption) {
358
            if (is_string($subOption)) {
359
                //Array of relation names
360
                $this->with($subOption, []);
361
            } else {
362
                //Multiple relations or relation with addition load options
363
                $this->with($name, $subOption);
364
            }
365
        }
366
367
        return $this;
368
    }
369
370
    /**
371
     * Find one entity or return null. Method provides the ability to configure custom query
372
     * parameters. Attention, method does not set a limit on selection (to avoid underselection of
373
     * joined tables), make sure to set the constrain in the query.
374
     *
375
     * @param array|null $query
376
     * @return object|null
377
     */
378
    public function fetchOne(array $query = null)
379
    {
380
        $data = (clone $this)->where($query)->limit(1)->fetchData();
381
382
        if (!isset($data[0])) {
383
            return null;
384
        }
385
386
        return $this->orm->make(
387
            $this->loader->getTarget(),
388
            $data[0],
389
            Node::MANAGED
390
        );
391
    }
392
393
    /**
394
     * Fetch all records in a form of array.
395
     *
396
     * @return object[]
397
     */
398
    public function fetchAll(): array
399
    {
400
        return iterator_to_array($this->getIterator());
401
    }
402
403
    /**
404
     * @return Iterator
405
     */
406
    public function getIterator(): Iterator
407
    {
408
        return new Iterator(
409
            $this->orm,
410
            $this->loader->getTarget(),
411
            $this->fetchData()
412
        );
413
    }
414
415
    /**
416
     * Load data tree from database and linked loaders in a form of array.
417
     *
418
     * @return array
419
     */
420
    public function fetchData(): array
421
    {
422
        $node = $this->loader->createNode();
423
        $this->loader->loadData($node);
424
425
        return $node->getResult();
426
    }
427
428
    /**
429
     * Compiled SQL statement.
430
     *
431
     * @return string
432
     */
433
    public function sqlStatement(): string
434
    {
435
        return $this->buildQuery()->sqlStatement();
436
    }
437
438
    /**
439
     * Return base loader associated with the selector.
440
     *
441
     * @return RootLoader
442
     */
443
    private function getLoader(): RootLoader
444
    {
445
        return $this->loader;
446
    }
447
}
448