Passed
Pull Request — master (#239)
by
unknown
03:30
created

Select::getLoader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
 * @method mixed avg($identifier) Perform aggregation (AVG) based on column or expression value.
39
 * @method mixed min($identifier) Perform aggregation (MIN) based on column or expression value.
40
 * @method mixed max($identifier) Perform aggregation (MAX) based on column or expression value.
41
 * @method mixed sum($identifier) Perform aggregation (SUM) based on column or expression value.
42
 */
43
final class Select implements IteratorAggregate, Countable, PaginableInterface
44
{
45
    // load relation data within same query
46
    public const SINGLE_QUERY = JoinableLoader::INLOAD;
47
48
    // load related data after the query
49
    public const OUTER_QUERY = JoinableLoader::POSTLOAD;
50
51
    /** @var ORMInterface @internal */
52
    private $orm;
53
54
    /** @var RootLoader */
55
    private $loader;
56
57
    /** @var QueryBuilder */
58
    private $builder;
59
60
    /**
61
     * @param ORMInterface $orm
62
     * @param string       $role
63
     */
64
    public function __construct(ORMInterface $orm, string $role)
65
    {
66
        $this->orm = $orm;
67
        $this->loader = new RootLoader($orm, $this->orm->resolveRole($role));
68
        $this->builder = new QueryBuilder($this->loader->getQuery(), $this->loader);
69
    }
70
71
    /**
72
     * Remove nested loaders and clean ORM link.
73
     */
74
    public function __destruct()
75
    {
76
        $this->orm = null;
77
        $this->loader = null;
78
        $this->builder = null;
79
    }
80
81
    /**
82
     * Bypassing call to primary select query.
83
     *
84
     * @param string $name
85
     * @param array  $arguments
86
     *
87
     * @return mixed|Select
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 int|string $id
159
     *
160
     * @return $this|Select
161
     */
162
    public function wherePK($id): self
163
    {
164
        return $this->__call('where', [$this->loader->getPK(), $id]);
165
    }
166
167
    /**
168
     * Attention, column will be quoted by driver!
169
     *
170
     * @param string|null $column When column is null PK or DISTINCT(PK) will be generated.
171
     *
172
     * @return int
173
     */
174
    public function count(string $column = null): int
175
    {
176
        return (int) $this->__call('count', [$column ?? $this->loader->getCountField()]);
177
    }
178
179
    /**
180
     * @inheritdoc
181
     */
182
    public function limit(int $limit): self
183
    {
184
        $this->loader->getQuery()->limit($limit);
185
186
        return $this;
187
    }
188
189
    /**
190
     * @inheritdoc
191
     */
192
    public function offset(int $offset): self
193
    {
194
        $this->loader->getQuery()->offset($offset);
195
196
        return $this;
197
    }
198
199
    /**
200
     * Request primary selector loader to pre-load relation name. Any type of loader can be used
201
     * for data pre-loading. ORM loaders by default will select the most efficient way to load
202
     * related data which might include additional select query or left join. Loaded data will
203
     * automatically pre-populate record relations. You can specify nested relations using "."
204
     * separator.
205
     *
206
     * Examples:
207
     *
208
     * // Select users and load their comments (will cast 2 queries, HAS_MANY comments)
209
     * User::find()->with('comments');
210
     *
211
     * // You can load chain of relations - select user and load their comments and post related to
212
     * //comment
213
     * User::find()->with('comments.post');
214
     *
215
     * // We can also specify custom where conditions on data loading, let's load only public
216
     * // comments.
217
     * User::find()->load('comments', [
218
     *      'where' => ['{@}.status' => 'public']
219
     * ]);
220
     *
221
     * Please note using "{@}" column name, this placeholder is required to prevent collisions and
222
     * it will be automatically replaced with valid table alias of pre-loaded comments table.
223
     *
224
     * // In case where your loaded relation is MANY_TO_MANY you can also specify pivot table
225
     * // conditions, let's pre-load all approved user tags, we can use same placeholder for pivot
226
     * // table alias
227
     * User::find()->load('tags', [
228
     *      'wherePivot' => ['{@}.approved' => true]
229
     * ]);
230
     *
231
     * // In most of cases you don't need to worry about how data was loaded, using external query
232
     * // or left join, however if you want to change such behaviour you can force load method to
233
     * // INLOAD
234
     * User::find()->load('tags', [
235
     *      'method'     => Loader::INLOAD,
236
     *      'wherePivot' => ['{@}.approved' => true]
237
     * ]);
238
     *
239
     * Attention, you will not be able to correctly paginate in this case and only ORM loaders
240
     * support different loading types.
241
     *
242
     * You can specify multiple loaders using array as first argument.
243
     *
244
     * Example:
245
     * User::find()->load(['posts', 'comments', 'profile']);
246
     *
247
     * Attention, consider disabling entity map if you want to use recursive loading (i.e
248
     * post.tags.posts), but first think why you even need recursive relation loading.
249
     *
250
     * @param array|string $relation
251
     * @param array        $options
252
     *
253
     * @return $this|self
254
     *
255
     * @see with()
256
     */
257
    public function load($relation, array $options = []): self
258
    {
259
        if (is_string($relation)) {
260
            $this->loader->loadRelation($relation, $options, false, true);
261
262
            return $this;
263
        }
264
265
        foreach ($relation as $name => $subOption) {
266
            if (is_string($subOption)) {
267
                // array of relation names
268
                $this->load($subOption, $options);
269
            } else {
270
                // multiple relations or relation with addition load options
271
                $this->load($name, $subOption + $options);
272
            }
273
        }
274
275
        return $this;
276
    }
277
278
    /**
279
     * With method is very similar to load() one, except it will always include related data to
280
     * parent query using INNER JOIN, this method can be applied only to ORM loaders and relations
281
     * using same database as parent record.
282
     *
283
     * Method generally used to filter data based on some relation condition. Attention, with()
284
     * method WILL NOT load relation data, it will only make it accessible in query.
285
     *
286
     * By default joined tables will be available in query based on relation name, you can change
287
     * joined table alias using relation option "alias".
288
     *
289
     * Do not forget to set DISTINCT flag while including HAS_MANY and MANY_TO_MANY relations. In
290
     * other scenario you will not able to paginate data well.
291
     *
292
     * Examples:
293
     *
294
     * // Find all users who have comments comments
295
     * User::find()->with('comments');
296
     *
297
     * // Find all users who have approved comments (we can use comments table alias in where
298
     * statement). User::find()->with('comments')->where('comments.approved', true);
299
     *
300
     * // Find all users who have posts which have approved comments
301
     * User::find()->with('posts.comments')->where('posts_comments.approved', true);
302
     *
303
     * // Custom join alias for post comments relation
304
     * $user->with('posts.comments', [
305
     *      'as' => 'comments'
306
     * ])->where('comments.approved', true);
307
     *
308
     * // If you joining MANY_TO_MANY relation you will be able to use pivot table used as relation
309
     * // name plus "_pivot" postfix. Let's load all users with approved tags.
310
     * $user->with('tags')->where('tags_pivot.approved', true);
311
     *
312
     * // You can also use custom alias for pivot table as well
313
     * User::find()->with('tags', [
314
     *      'pivotAlias' => 'tags_connection'
315
     * ])
316
     * ->where('tags_connection.approved', false);
317
     *
318
     * You can safely combine with() and load() methods.
319
     *
320
     * // Load all users with approved comments and pre-load all their comments
321
     * User::find()->with('comments')->where('comments.approved', true)->load('comments');
322
     *
323
     * // You can also use custom conditions in this case, let's find all users with approved
324
     * // comments and pre-load such approved comments
325
     * User::find()->with('comments')->where('comments.approved', true)
326
     *             ->load('comments', [
327
     *                  'where' => ['{@}.approved' => true]
328
     *              ]);
329
     *
330
     * // As you might notice previous construction will create 2 queries, however we can simplify
331
     * // this construction to use already joined table as source of data for relation via "using"
332
     * // keyword
333
     * User::find()->with('comments')
334
     *             ->where('comments.approved', true)
335
     *             ->load('comments', ['using' => 'comments']);
336
     *
337
     * // You will get only one query with INNER JOIN, to better understand this example let's use
338
     * // custom alias for comments in with() method.
339
     * User::find()->with('comments', ['as' => 'commentsR'])
340
     *             ->where('commentsR.approved', true)
341
     *             ->load('comments', ['using' => 'commentsR']);
342
     *
343
     * @param array|string $relation
344
     * @param array        $options
345
     *
346
     * @return $this|Select
347
     *
348
     * @see load()
349
     */
350
    public function with($relation, array $options = []): self
351
    {
352
        if (is_string($relation)) {
353
            $this->loader->loadRelation($relation, $options, true, false);
354
355
            return $this;
356
        }
357
358
        foreach ($relation as $name => $subOption) {
359
            if (is_string($subOption)) {
360
                //Array of relation names
361
                $this->with($subOption, []);
362
            } else {
363
                //Multiple relations or relation with addition load options
364
                $this->with($name, $subOption);
365
            }
366
        }
367
368
        return $this;
369
    }
370
371
    /**
372
     * Find one entity or return null. Method provides the ability to configure custom query
373
     * parameters. Attention, method does not set a limit on selection (to avoid underselection of
374
     * joined tables), make sure to set the constrain in the query.
375
     *
376
     * @param array|null $query
377
     *
378
     * @return object|null
379
     */
380
    public function fetchOne(array $query = null)
381
    {
382
        $data = (clone $this)->where($query)->limit(1)->fetchData();
383
384
        if (!isset($data[0])) {
385
            return null;
386
        }
387
388
        return $this->orm->make(
389
            $this->loader->getTarget(),
390
            $data[0],
391
            Node::MANAGED
392
        );
393
    }
394
395
    /**
396
     * Fetch all records in a form of array.
397
     *
398
     * @return object[]
399
     */
400
    public function fetchAll(): array
401
    {
402
        return iterator_to_array($this->getIterator());
403
    }
404
405
    /**
406
     * @return Iterator
407
     */
408
    public function getIterator(): Iterator
409
    {
410
        return new Iterator(
411
            $this->orm,
412
            $this->loader->getTarget(),
413
            $this->fetchData()
414
        );
415
    }
416
417
    /**
418
     * Load data tree from database and linked loaders in a form of array.
419
     *
420
     * @return array
421
     */
422
    public function fetchData(): array
423
    {
424
        $node = $this->loader->createNode();
425
        $this->loader->loadData($node);
426
427
        return $node->getResult();
428
    }
429
430
    /**
431
     * Compiled SQL statement.
432
     *
433
     * @return string
434
     */
435
    public function sqlStatement(): string
436
    {
437
        return $this->buildQuery()->sqlStatement();
438
    }
439
}
440