-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathQueryTrait.php
619 lines (561 loc) · 18.7 KB
/
QueryTrait.php
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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Datasource;
use BadMethodCallException;
use Cake\Collection\Iterator\MapReduce;
use Cake\Datasource\Exception\RecordNotFoundException;
use InvalidArgumentException;
use Traversable;
/**
* Contains the characteristics for an object that is attached to a repository and
* can retrieve results based on any criteria.
*/
trait QueryTrait
{
/**
* Instance of a table object this query is bound to
*
* @var \Cake\Datasource\RepositoryInterface
*/
protected $_repository;
/**
* A ResultSet.
*
* When set, query execution will be bypassed.
*
* @var iterable|null
* @see \Cake\Datasource\QueryTrait::setResult()
*/
protected $_results;
/**
* List of map-reduce routines that should be applied over the query
* result
*
* @var array
*/
protected $_mapReduce = [];
/**
* List of formatter classes or callbacks that will post-process the
* results when fetched
*
* @var callable[]
*/
protected $_formatters = [];
/**
* A query cacher instance if this query has caching enabled.
*
* @var \Cake\Datasource\QueryCacher|null
*/
protected $_cache;
/**
* Holds any custom options passed using applyOptions that could not be processed
* by any method in this class.
*
* @var array
*/
protected $_options = [];
/**
* Whether the query is standalone or the product of an eager load operation.
*
* @var bool
*/
protected $_eagerLoaded = false;
/**
* Set the default Table object that will be used by this query
* and form the `FROM` clause.
*
* @param \Cake\Datasource\RepositoryInterface|\Cake\ORM\Table $repository The default table object to use
* @return $this
*/
public function repository(RepositoryInterface $repository)
{
$this->_repository = $repository;
return $this;
}
/**
* Returns the default table object that will be used by this query,
* that is, the table that will appear in the from clause.
*
* @return \Cake\Datasource\RepositoryInterface
*/
public function getRepository(): RepositoryInterface
{
return $this->_repository;
}
/**
* Set the result set for a query.
*
* Setting the resultset of a query will make execute() a no-op. Instead
* of executing the SQL query and fetching results, the ResultSet provided to this
* method will be returned.
*
* This method is most useful when combined with results stored in a persistent cache.
*
* @param iterable $results The results this query should return.
* @return $this
*/
public function setResult(iterable $results)
{
$this->_results = $results;
return $this;
}
/**
* Executes this query and returns a results iterator. This function is required
* for implementing the IteratorAggregate interface and allows the query to be
* iterated without having to call execute() manually, thus making it look like
* a result set instead of the query itself.
*
* @return \Cake\Datasource\ResultSetInterface
* @psalm-suppress ImplementedReturnTypeMismatch
*/
public function getIterator()
{
return $this->all();
}
/**
* Enable result caching for this query.
*
* If a query has caching enabled, it will do the following when executed:
*
* - Check the cache for $key. If there are results no SQL will be executed.
* Instead the cached results will be returned.
* - When the cached data is stale/missing the result set will be cached as the query
* is executed.
*
* ### Usage
*
* ```
* // Simple string key + config
* $query->cache('my_key', 'db_results');
*
* // Function to generate key.
* $query->cache(function ($q) {
* $key = serialize($q->clause('select'));
* $key .= serialize($q->clause('where'));
* return md5($key);
* });
*
* // Using a pre-built cache engine.
* $query->cache('my_key', $engine);
*
* // Disable caching
* $query->cache(false);
* ```
*
* @param \Closure|string|false $key Either the cache key or a function to generate the cache key.
* When using a function, this query instance will be supplied as an argument.
* @param string|\Psr\SimpleCache\CacheInterface $config Either the name of the cache config to use, or
* a cache engine instance.
* @return $this
*/
public function cache($key, $config = 'default')
{
if ($key === false) {
$this->_cache = null;
return $this;
}
$this->_cache = new QueryCacher($key, $config);
return $this;
}
/**
* Returns the current configured query `_eagerLoaded` value
*
* @return bool
*/
public function isEagerLoaded(): bool
{
return $this->_eagerLoaded;
}
/**
* Sets the query instance to be an eager loaded query. If no argument is
* passed, the current configured query `_eagerLoaded` value is returned.
*
* @param bool $value Whether or not to eager load.
* @return $this
*/
public function eagerLoaded(bool $value)
{
$this->_eagerLoaded = $value;
return $this;
}
/**
* Returns a key => value array representing a single aliased field
* that can be passed directly to the select() method.
* The key will contain the alias and the value the actual field name.
*
* If the field is already aliased, then it will not be changed.
* If no $alias is passed, the default table for this query will be used.
*
* @param string $field The field to alias
* @param string|null $alias the alias used to prefix the field
* @return array
*/
public function aliasField(string $field, ?string $alias = null): array
{
$namespaced = strpos($field, '.') !== false;
$aliasedField = $field;
if ($namespaced) {
[$alias, $field] = explode('.', $field);
}
if (!$alias) {
$alias = $this->getRepository()->getAlias();
}
$key = sprintf('%s__%s', $alias, $field);
if (!$namespaced) {
$aliasedField = $alias . '.' . $field;
}
return [$key => $aliasedField];
}
/**
* Runs `aliasField()` for each field in the provided list and returns
* the result under a single array.
*
* @param array $fields The fields to alias
* @param string|null $defaultAlias The default alias
* @return string[]
*/
public function aliasFields(array $fields, ?string $defaultAlias = null): array
{
$aliased = [];
foreach ($fields as $alias => $field) {
if (is_numeric($alias) && is_string($field)) {
$aliased += $this->aliasField($field, $defaultAlias);
continue;
}
$aliased[$alias] = $field;
}
return $aliased;
}
/**
* Fetch the results for this query.
*
* Will return either the results set through setResult(), or execute this query
* and return the ResultSetDecorator object ready for streaming of results.
*
* ResultSetDecorator is a traversable object that implements the methods found
* on Cake\Collection\Collection.
*
* @return \Cake\Datasource\ResultSetInterface
*/
public function all(): ResultSetInterface
{
if ($this->_results !== null) {
return $this->_results;
}
$results = null;
if ($this->_cache) {
$results = $this->_cache->fetch($this);
}
if ($results === null) {
$results = $this->_decorateResults($this->_execute());
if ($this->_cache) {
$this->_cache->store($this, $results);
}
}
$this->_results = $results;
return $this->_results;
}
/**
* Returns an array representation of the results after executing the query.
*
* @return array
*/
public function toArray(): array
{
return $this->all()->toArray();
}
/**
* Register a new MapReduce routine to be executed on top of the database results
* Both the mapper and caller callable should be invokable objects.
*
* The MapReduce routing will only be run when the query is executed and the first
* result is attempted to be fetched.
*
* If the third argument is set to true, it will erase previous map reducers
* and replace it with the arguments passed.
*
* @param callable|null $mapper The mapper callable.
* @param callable|null $reducer The reducing function.
* @param bool $overwrite Set to true to overwrite existing map + reduce functions.
* @return $this
* @see \Cake\Collection\Iterator\MapReduce for details on how to use emit data to the map reducer.
*/
public function mapReduce(?callable $mapper = null, ?callable $reducer = null, bool $overwrite = false)
{
if ($overwrite) {
$this->_mapReduce = [];
}
if ($mapper === null) {
if (!$overwrite) {
throw new InvalidArgumentException('$mapper can be null only when $overwrite is true.');
}
return $this;
}
$this->_mapReduce[] = compact('mapper', 'reducer');
return $this;
}
/**
* Returns the list of previously registered map reduce routines.
*
* @return array
*/
public function getMapReducers(): array
{
return $this->_mapReduce;
}
/**
* Registers a new formatter callback function that is to be executed when trying
* to fetch the results from the database.
*
* If the second argument is set to true, it will erase previous formatters
* and replace them with the passed first argument.
*
* Callbacks are required to return an iterator object, which will be used as
* the return value for this query's result. Formatter functions are applied
* after all the `MapReduce` routines for this query have been executed.
*
* Formatting callbacks will receive two arguments, the first one being an object
* implementing `\Cake\Collection\CollectionInterface`, that can be traversed and
* modified at will. The second one being the query instance on which the formatter
* callback is being applied.
*
* Usually the query instance received by the formatter callback is the same query
* instance on which the callback was attached to, except for in a joined
* association, in that case the callback will be invoked on the association source
* side query, and it will receive that query instance instead of the one on which
* the callback was originally attached to - see the examples below!
*
* ### Examples:
*
* Return all results from the table indexed by id:
*
* ```
* $query->select(['id', 'name'])->formatResults(function ($results) {
* return $results->indexBy('id');
* });
* ```
*
* Add a new column to the ResultSet:
*
* ```
* $query->select(['name', 'birth_date'])->formatResults(function ($results) {
* return $results->map(function ($row) {
* $row['age'] = $row['birth_date']->diff(new DateTime)->y;
*
* return $row;
* });
* });
* ```
*
* Add a new column to the results with respect to the query's hydration configuration:
*
* ```
* $query->formatResults(function ($results, $query) {
* return $results->map(function ($row) use ($query) {
* $data = [
* 'bar' => 'baz',
* ];
*
* if ($query->isHydrationEnabled()) {
* $row['foo'] = new Foo($data)
* } else {
* $row['foo'] = $data;
* }
*
* return $row;
* });
* });
* ```
*
* Retaining access to the association target query instance of joined associations,
* by inheriting the contain callback's query argument:
*
* ```
* // Assuming a `Articles belongsTo Authors` association that uses the join strategy
*
* $articlesQuery->contain('Authors', function ($authorsQuery) {
* return $authorsQuery->formatResults(function ($results, $query) use ($authorsQuery) {
* // Here `$authorsQuery` will always be the instance
* // where the callback was attached to.
*
* // The instance passed to the callback in the second
* // argument (`$query`), will be the one where the
* // callback is actually being applied to, in this
* // example that would be `$articlesQuery`.
*
* // ...
*
* return $results;
* });
* });
* ```
*
* @param callable|null $formatter The formatting callable.
* @param int|true $mode Whether or not to overwrite, append or prepend the formatter.
* @return $this
* @throws \InvalidArgumentException
*/
public function formatResults(?callable $formatter = null, $mode = self::APPEND)
{
if ($mode === self::OVERWRITE) {
$this->_formatters = [];
}
if ($formatter === null) {
if ($mode !== self::OVERWRITE) {
throw new InvalidArgumentException('$formatter can be null only when $mode is overwrite.');
}
return $this;
}
if ($mode === self::PREPEND) {
array_unshift($this->_formatters, $formatter);
return $this;
}
$this->_formatters[] = $formatter;
return $this;
}
/**
* Returns the list of previously registered format routines.
*
* @return callable[]
*/
public function getResultFormatters(): array
{
return $this->_formatters;
}
/**
* Returns the first result out of executing this query, if the query has not been
* executed before, it will set the limit clause to 1 for performance reasons.
*
* ### Example:
*
* ```
* $singleUser = $query->select(['id', 'username'])->first();
* ```
*
* @return \Cake\Datasource\EntityInterface|array|null The first result from the ResultSet.
*/
public function first()
{
if ($this->_dirty) {
$this->limit(1);
}
return $this->all()->first();
}
/**
* Get the first result from the executing query or raise an exception.
*
* @throws \Cake\Datasource\Exception\RecordNotFoundException When there is no first record.
* @return \Cake\Datasource\EntityInterface|array The first result from the ResultSet.
*/
public function firstOrFail()
{
$entity = $this->first();
if (!$entity) {
$table = $this->getRepository();
throw new RecordNotFoundException(sprintf(
'Record not found in table "%s"',
$table->getTable()
));
}
return $entity;
}
/**
* Returns an array with the custom options that were applied to this query
* and that were not already processed by another method in this class.
*
* ### Example:
*
* ```
* $query->applyOptions(['doABarrelRoll' => true, 'fields' => ['id', 'name']);
* $query->getOptions(); // Returns ['doABarrelRoll' => true]
* ```
*
* @see \Cake\Datasource\QueryInterface::applyOptions() to read about the options that will
* be processed by this class and not returned by this function
* @return array
* @see applyOptions()
*/
public function getOptions(): array
{
return $this->_options;
}
/**
* Enables calling methods from the result set as if they were from this class
*
* @param string $method the method to call
* @param array $arguments list of arguments for the method to call
* @return mixed
* @throws \BadMethodCallException if no such method exists in result set
*/
public function __call(string $method, array $arguments)
{
$resultSetClass = $this->_decoratorClass();
if (in_array($method, get_class_methods($resultSetClass), true)) {
$results = $this->all();
return $results->$method(...$arguments);
}
throw new BadMethodCallException(
sprintf('Unknown method "%s"', $method)
);
}
/**
* Populates or adds parts to current query clauses using an array.
* This is handy for passing all query clauses at once.
*
* @param array $options the options to be applied
* @return $this
*/
abstract public function applyOptions(array $options);
/**
* Executes this query and returns a traversable object containing the results
*
* @return \Cake\Datasource\ResultSetInterface
*/
abstract protected function _execute(): ResultSetInterface;
/**
* Decorates the results iterator with MapReduce routines and formatters
*
* @param \Traversable $result Original results
* @return \Cake\Datasource\ResultSetInterface
*/
protected function _decorateResults(Traversable $result): ResultSetInterface
{
$decorator = $this->_decoratorClass();
foreach ($this->_mapReduce as $functions) {
$result = new MapReduce($result, $functions['mapper'], $functions['reducer']);
}
if (!empty($this->_mapReduce)) {
$result = new $decorator($result);
}
foreach ($this->_formatters as $formatter) {
$result = $formatter($result, $this);
}
if (!empty($this->_formatters) && !($result instanceof $decorator)) {
$result = new $decorator($result);
}
return $result;
}
/**
* Returns the name of the class to be used for decorating results
*
* @return string
* @psalm-return class-string<\Cake\Datasource\ResultSetInterface>
*/
protected function _decoratorClass(): string
{
return ResultSetDecorator::class;
}
}