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