/
Query.php
584 lines (536 loc) · 16.8 KB
/
Query.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
<?php
/**
* PHP Version 5.4
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v 3.0.0
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace Cake\ORM;
use Cake\Database\Query as DatabaseQuery;
use Cake\Database\Statement\BufferedStatement;
use Cake\Database\Statement\CallbackStatement;
/**
* Extends the base Query class to provide new methods related to association
* loading, automatic fields selection, automatic type casting and to wrap results
* into an specific iterator that will be responsible for hydrating results if
* required.
*
*/
class Query extends DatabaseQuery {
/**
* Instance of a table object this query is bound to
*
* @var \Cake\ORM\Table
*/
protected $_table;
/**
* Nested array describing the association to be fetched
* and the options to apply for each of them, if any
*
* @var \ArrayObject
*/
protected $_containments;
/**
* Contains a nested array with the compiled containments tree
* This is a normalized version of the user provided containments array.
*
* @var array
*/
protected $_normalizedContainments;
/**
* Whether the user select any fields before being executed, this is used
* to determined if any fields should be automatically be selected.
*
* @var boolean
*/
protected $_hasFields;
/**
* A list of associations that should be eagerly loaded
*
* @var array
*/
protected $_loadEagerly = [];
/**
* List of options accepted by associations in contain()
* index by key for faster access
*
* @var array
*/
protected $_containOptions = [
'associations' => 1,
'foreignKey' => 1,
'conditions' => 1,
'fields' => 1,
'sort' => 1,
'matching' => 1
];
/**
* Returns the default table object that will be used by this query,
* that is, the table that will appear in the from clause.
*
* When called with a Table argument, the default table object will be set
* and this query object will be returned for chaining.
*
* @param \Cake\ORM\Table $table The default table object to use
* @var \Cake\ORM\Table|Query
*/
public function repository(Table $table = null) {
if ($table === null) {
return $this->_table;
}
$this->_table = $table;
$this->addDefaultTypes($table);
return $this;
}
/**
* Hints this object to associate the correct types when casting conditions
* for the database. This is done by extracting the field types from the schema
* associated to the passed table object. This prevents the user from repeating
* himself when specifying conditions.
*
* This method returns the same query object for chaining.
*
* @param \Cake\ORM\Table $table
* @return Query
*/
public function addDefaultTypes(Table $table) {
$alias = $table->alias();
$schema = $table->schema();
$fields = [];
foreach ($schema->columns() as $f) {
$fields[$f] = $fields[$alias . '.' . $f] = $schema->columnType($f);
}
$this->defaultTypes($this->defaultTypes() + $fields);
return $this;
}
/**
* Sets the list of associations that should be eagerly loaded along with this
* query. The list of associated tables passed must have been previously set as
* associations using the Table API.
*
* ### Example:
*
* {{{
* // Bring articles' author information
* $query->contain('Author');
*
* // Also bring the category and tags associated to each article
* $query->contain(['Category', 'Tag']);
* }}}
*
* Associations can be arbitrarily nested using arrays, this allows this object to
* calculate joins or any additional queries that must be executed to bring the
* required associated data.
*
* ### Example:
*
* {{{
* // Eager load the product info, and for each product load other 2 associations
* $query->contain(['Product' => ['Manufacturer', 'Distributor']);
*
* // For an author query, load his region, state and country
* $query->contain(['Region' => ['State' => 'Country']);
* }}}
*
* Each association might define special options when eager loaded, the allowed
* options that can be set per association are:
*
* - foreignKey: Used to set a different field to match both tables, if set to false
* no join conditions will be generated automatically
* - conditions: An array of conditions that will be passed to either a query or
* join conditions. See `where` method for the valid format.
* - fields: An array with the fields that should be fetched from the association
* - sort: for associations that are not joined directly, the order they should
* appear in the resulting set
* - matching: A boolean indicating if the parent association records should be
* filtered by those matching the conditions in the target association.
*
* ### Example:
*
* {{{
* // Set options for the articles that will be eagerly loaded for an author
* $query->contain([
* 'Article' => [
* 'field' => ['title'],
* 'conditions' => ['read_count >' => 100],
* 'sort' => ['published' => 'DESC']
* ]
* ]);
*
* // Use special join conditions for getting an article author's 'likes'
* $query->contain([
* 'Like' => [
* 'foreignKey' => false,
* 'conditions' => ['Article.author_id = Like.user_id']
* ]
* ]);
*
* // Bring only articles that were tagged with 'cake'
* $query->contain([
* 'Tag' => [
* 'matching' => true,
* 'conditions' => ['Tag.name' => 'cake']
* ]
* ]);
* }}}
*
* If called with no arguments, this function will return an ArrayObject with
* with the list of previously configured associations to be contained in the
* result. This object can be modified directly as the reference is kept inside
* the query.
*
* The resulting ArrayObject will always have association aliases as keys, and
* options as values, if no options are passed, the values will be set to an empty
* array
*
* ### Example:
*
* {{{
* // Set some associations
* $query->contain(['Article', 'Author' => ['fields' => ['Author.name']);
*
* // Let's now add another field to Author
* $query->contain()['Author']['fields'][] = 'Author.email';
*
* // Let's also add Article's tags
* $query->contain()['Article']['Tag'] = [];
* }}}
*
* Please note that when modifying directly the containments array, you are
* required to maintain the structure. That is, association names as keys
* having array values. Failing to do so will result in an error
*
* If called with an empty first argument and $override is set to true, the
* previous list will be emptied.
*
* @param array|string $associations list of table aliases to be queried
* @param boolean $override whether override previous list with the one passed
* defaults to merging previous list with the new one.
* @return \ArrayObject|Query
*/
public function contain($associations = null, $override = false) {
if ($this->_containments === null || $override) {
$this->_dirty = true;
$this->_containments = new \ArrayObject;
}
if ($associations === null) {
return $this->_containments;
}
$associations = (array)$associations;
if (isset(current($associations)['instance'])) {
$this->_containments = $this->_normalizedContainments = $associations;
return $this;
}
$old = $this->_containments->getArrayCopy();
$associations = array_merge($old, $this->_reformatContain($associations));
$this->_containments->exchangeArray($associations);
$this->_normalizedContainments = null;
$this->_dirty = true;
return $this;
}
/**
* Formats the containments array so that associations are always set as keys
* in the array.
*
* @param array $associations user provided containments array
* @return array
*/
protected function _reformatContain($associations) {
$result = [];
foreach ((array)$associations as $table => $options) {
if (is_int($table)) {
$table = $options;
$options = [];
} elseif (is_array($options) && !isset($this->_containOptions[$table])) {
$options = $this->_reformatContain($options);
}
$result[$table] = $options;
}
return $result;
}
/**
* Returns the fully normalized array of associations that should be eagerly
* loaded. The normalized array will restructure the original one by sorting
* all associations under one key and special options under another.
*
* Additionally it will set an 'instance' key per association containing the
* association instance from the corresponding source table
*
* @return array
*/
public function normalizedContainments() {
if ($this->_normalizedContainments !== null || empty($this->_containments)) {
return $this->_normalizedContainments;
}
$contain = [];
foreach ($this->_containments as $table => $options) {
if (!empty($options['instance'])) {
$contain = (array)$this->_containments;
break;
}
$contain[$table] = $this->_normalizeContain(
$this->_table,
$table,
$options
);
}
return $this->_normalizedContainments = $contain;
}
/**
* Compiles the SQL representation of this query and executes it using the
* configured connection object. Returns a ResultSet iterator object
*
* Resulting object is traversable, so it can be used in any loop as you would
* with an array.
*
* @return Cake\ORM\ResultSet
*/
public function execute() {
return new ResultSet($this, parent::execute());
}
/**
* Returns an array representation of the results after executing the query.
*
* @return array
*/
public function toArray() {
return $this->execute()->toArray();
}
/**
* 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
* @param string $alias the alias used to prefix the field
* @return array
*/
public function aliasField($field, $alias = null) {
$namespaced = strpos($field, '.') !== false;
$aliasedField = $field;
if ($namespaced) {
list($alias, $field) = explode('.', $field);
}
if (!$alias) {
$alias = $this->repository()->alias();
}
$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
* @param string $defaultAlias
* @return array
*/
public function aliasFields($fields, $defaultAlias = null) {
$aliased = [];
foreach ($fields as $alias => $field) {
if (is_numeric($alias) && is_string($field)) {
$aliased += $this->aliasField($field, $defaultAlias);
continue;
}
$aliased[$alias] = $field;
}
return $aliased;
}
/**
* Auxiliary function used to wrap the original statement from the driver with
* any registered callbacks. This will also setup the correct statement class
* in order to eager load deep associations.
*
* @param Cake\Database\Statement $statement to be decorated
* @return Cake\Database\Statement
*/
protected function _decorateResults($statement) {
$statement = parent::_decorateResults($statement);
if ($this->_loadEagerly) {
if (!($statement instanceof BufferedStatement)) {
$statement = new BufferedStatement($statement, $this->connection()->driver());
}
$statement = $this->_eagerLoad($statement);
}
return $statement;
}
/**
* Applies some defaults to the query object before it is executed.
* Specifically add the FROM clause, adds default table fields if none is
* specified and applies the joins required to eager load associations defined
* using `contain`
*
* @return Query
*/
protected function _transformQuery() {
if (!$this->_dirty) {
return parent::_transformQuery();
}
if ($this->_type === 'select') {
if (empty($this->_parts['from'])) {
$this->from([$this->_table->alias() => $this->_table->table()]);
}
$this->_addDefaultFields();
$this->_addContainments();
}
return parent::_transformQuery();
}
/**
* Helper function used to add the required joins for associations defined using
* `contain()`
*
* @return void
*/
protected function _addContainments() {
$this->_loadEagerly = [];
if (empty($this->_containments)) {
return;
}
$contain = $this->normalizedContainments();
foreach ($contain as $relation => $meta) {
if ($meta['instance'] && !$meta['canBeJoined']) {
$this->_loadEagerly[$relation] = $meta;
}
}
foreach ($this->_resolveJoins($this->_table, $contain) as $options) {
$table = $options['instance']->target();
$alias = $table->alias();
$this->_addJoin($options['instance'], $options['config']);
foreach ($options['associations'] as $relation => $meta) {
if ($meta['instance'] && !$meta['canBeJoined']) {
$this->_loadEagerly[$relation] = $meta;
}
}
}
}
/**
* Auxiliary function responsible for fully normalizing deep associations defined
* using `contain()`
*
* @param Table $parent owning side of the association
* @param string $alias name of the association to be loaded
* @param array $options list of extra options to use for this association
* @return array normalized associations
* @throws \InvalidArgumentException When containments refer to associations that do not exist.
*/
protected function _normalizeContain(Table $parent, $alias, $options) {
$defaults = $this->_containOptions;
$instance = $parent->association($alias);
if (!$instance) {
throw new \InvalidArgumentException(
sprintf('%s is not associated with %s', $parent->alias(), $alias)
);
}
$table = $instance->target();
$extra = array_diff_key($options, $defaults);
$config = [
'associations' => [],
'instance' => $instance,
'config' => array_diff_key($options, $extra)
];
$config['canBeJoined'] = $instance->canBeJoined($config['config']);
foreach ($extra as $t => $assoc) {
$config['associations'][$t] = $this->_normalizeContain($table, $t, $assoc);
}
return $config;
}
/**
* Helper function used to compile a list of all associations that can be
* joined in this query.
*
* @param Table $source the owning side of the association
* @param array $associations list of associations for $source
* @return array
*/
protected function _resolveJoins($source, $associations) {
$result = [];
foreach ($associations as $table => $options) {
$associated = $options['instance'];
if ($options['canBeJoined']) {
$result[$table] = $options;
$result += $this->_resolveJoins($associated->target(), $options['associations']);
}
}
return $result;
}
/**
* Adds a join based on a particular association and some custom options
*
* @param Association $association
* @param array $options
* @return void
*/
protected function _addJoin($association, $options) {
$association->attachTo($this, $options + ['includeFields' => !$this->_hasFields]);
}
/**
* Helper method that will calculate those associations that cannot be joined
* directly in this query and will setup the required extra queries for fetching
* the extra data.
*
* @param Statement $statement original query statement
* @return CallbackStatement $statement modified statement with extra loaders
*/
protected function _eagerLoad($statement) {
$collectKeys = [];
foreach ($this->_loadEagerly as $association => $meta) {
$source = $meta['instance']->source();
if ($meta['instance']->requiresKeys($meta['config'])) {
$alias = $source->alias();
$pkField = key($this->aliasField($source->primaryKey(), $alias));
$collectKeys[] = [$alias, $pkField];
}
}
$keys = [];
if (!empty($collectKeys)) {
while ($result = $statement->fetch('assoc')) {
foreach ($collectKeys as $parts) {
$keys[$parts[0]][] = $result[$parts[1]];
}
}
$statement->rewind();
}
foreach ($this->_loadEagerly as $association => $meta) {
$contain = $meta['associations'];
$alias = $meta['instance']->source()->alias();
$keys = isset($keys[$alias]) ? $keys[$alias] : null;
$f = $meta['instance']->eagerLoader(
$meta['config'] + ['query' => $this, 'contain' => $contain, 'keys' => $keys]
);
$statement = new CallbackStatement($statement, $this->connection()->driver(), $f);
}
return $statement;
}
/**
* Inspects if there are any set fields for selecting, otherwise adds all
* the fields for the default table.
*
* @return void
*/
protected function _addDefaultFields() {
$select = $this->clause('select');
$this->_hasFields = true;
if (!count($select)) {
$this->_hasFields = false;
$this->select($this->repository()->schema()->columns());
$select = $this->clause('select');
}
$aliased = $this->aliasFields($select, $this->repository()->alias());
$this->select($aliased, true);
}
}