1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\ORM;
16:
17: use Cake\Database\Statement\BufferedStatement;
18: use Cake\Database\Statement\CallbackStatement;
19: use Cake\Datasource\QueryInterface;
20: use Closure;
21: use InvalidArgumentException;
22:
23: /**
24: * Exposes the methods for storing the associations that should be eager loaded
25: * for a table once a query is provided and delegates the job of creating the
26: * required joins and decorating the results so that those associations can be
27: * part of the result set.
28: */
29: class EagerLoader
30: {
31:
32: /**
33: * Nested array describing the association to be fetched
34: * and the options to apply for each of them, if any
35: *
36: * @var array
37: */
38: protected $_containments = [];
39:
40: /**
41: * Contains a nested array with the compiled containments tree
42: * This is a normalized version of the user provided containments array.
43: *
44: * @var \Cake\ORM\EagerLoadable[]|\Cake\ORM\EagerLoadable|null
45: */
46: protected $_normalized;
47:
48: /**
49: * List of options accepted by associations in contain()
50: * index by key for faster access
51: *
52: * @var array
53: */
54: protected $_containOptions = [
55: 'associations' => 1,
56: 'foreignKey' => 1,
57: 'conditions' => 1,
58: 'fields' => 1,
59: 'sort' => 1,
60: 'matching' => 1,
61: 'queryBuilder' => 1,
62: 'finder' => 1,
63: 'joinType' => 1,
64: 'strategy' => 1,
65: 'negateMatch' => 1
66: ];
67:
68: /**
69: * A list of associations that should be loaded with a separate query
70: *
71: * @var \Cake\ORM\EagerLoadable[]
72: */
73: protected $_loadExternal = [];
74:
75: /**
76: * Contains a list of the association names that are to be eagerly loaded
77: *
78: * @var array
79: */
80: protected $_aliasList = [];
81:
82: /**
83: * Another EagerLoader instance that will be used for 'matching' associations.
84: *
85: * @var \Cake\ORM\EagerLoader
86: */
87: protected $_matching;
88:
89: /**
90: * A map of table aliases pointing to the association objects they represent
91: * for the query.
92: *
93: * @var array
94: */
95: protected $_joinsMap = [];
96:
97: /**
98: * Controls whether or not fields from associated tables
99: * will be eagerly loaded. When set to false, no fields will
100: * be loaded from associations.
101: *
102: * @var bool
103: */
104: protected $_autoFields = true;
105:
106: /**
107: * Sets the list of associations that should be eagerly loaded along for a
108: * specific table using when a query is provided. The list of associated tables
109: * passed to this method must have been previously set as associations using the
110: * Table API.
111: *
112: * Associations can be arbitrarily nested using dot notation or nested arrays,
113: * this allows this object to calculate joins or any additional queries that
114: * must be executed to bring the required associated data.
115: *
116: * The getter part is deprecated as of 3.6.0. Use getContain() instead.
117: *
118: * Accepted options per passed association:
119: *
120: * - foreignKey: Used to set a different field to match both tables, if set to false
121: * no join conditions will be generated automatically
122: * - fields: An array with the fields that should be fetched from the association
123: * - queryBuilder: Equivalent to passing a callable instead of an options array
124: * - matching: Whether to inform the association class that it should filter the
125: * main query by the results fetched by that class.
126: * - joinType: For joinable associations, the SQL join type to use.
127: * - strategy: The loading strategy to use (join, select, subquery)
128: *
129: * @param array|string $associations list of table aliases to be queried.
130: * When this method is called multiple times it will merge previous list with
131: * the new one.
132: * @param callable|null $queryBuilder The query builder callable
133: * @return array Containments.
134: * @throws \InvalidArgumentException When using $queryBuilder with an array of $associations
135: */
136: public function contain($associations = [], callable $queryBuilder = null)
137: {
138: if (empty($associations)) {
139: deprecationWarning(
140: 'Using EagerLoader::contain() as getter is deprecated. ' .
141: 'Use getContain() instead.'
142: );
143:
144: return $this->getContain();
145: }
146:
147: if ($queryBuilder) {
148: if (!is_string($associations)) {
149: throw new InvalidArgumentException(
150: sprintf('Cannot set containments. To use $queryBuilder, $associations must be a string')
151: );
152: }
153:
154: $associations = [
155: $associations => [
156: 'queryBuilder' => $queryBuilder
157: ]
158: ];
159: }
160:
161: $associations = (array)$associations;
162: $associations = $this->_reformatContain($associations, $this->_containments);
163: $this->_normalized = null;
164: $this->_loadExternal = [];
165: $this->_aliasList = [];
166:
167: return $this->_containments = $associations;
168: }
169:
170: /**
171: * Gets the list of associations that should be eagerly loaded along for a
172: * specific table using when a query is provided. The list of associated tables
173: * passed to this method must have been previously set as associations using the
174: * Table API.
175: *
176: * @return array Containments.
177: */
178: public function getContain()
179: {
180: return $this->_containments;
181: }
182:
183: /**
184: * Remove any existing non-matching based containments.
185: *
186: * This will reset/clear out any contained associations that were not
187: * added via matching().
188: *
189: * @return void
190: */
191: public function clearContain()
192: {
193: $this->_containments = [];
194: $this->_normalized = null;
195: $this->_loadExternal = [];
196: $this->_aliasList = [];
197: }
198:
199: /**
200: * Sets whether or not contained associations will load fields automatically.
201: *
202: * @param bool $enable The value to set.
203: * @return $this
204: */
205: public function enableAutoFields($enable = true)
206: {
207: $this->_autoFields = (bool)$enable;
208:
209: return $this;
210: }
211:
212: /**
213: * Disable auto loading fields of contained associations.
214: *
215: * @return $this
216: */
217: public function disableAutoFields()
218: {
219: $this->_autoFields = false;
220:
221: return $this;
222: }
223:
224: /**
225: * Gets whether or not contained associations will load fields automatically.
226: *
227: * @return bool The current value.
228: */
229: public function isAutoFieldsEnabled()
230: {
231: return $this->_autoFields;
232: }
233:
234: /**
235: * Sets/Gets whether or not contained associations will load fields automatically.
236: *
237: * @deprecated 3.4.0 Use enableAutoFields()/isAutoFieldsEnabled() instead.
238: * @param bool|null $enable The value to set.
239: * @return bool The current value.
240: */
241: public function autoFields($enable = null)
242: {
243: deprecationWarning(
244: 'EagerLoader::autoFields() is deprecated. ' .
245: 'Use enableAutoFields()/isAutoFieldsEnabled() instead.'
246: );
247: if ($enable !== null) {
248: $this->enableAutoFields($enable);
249: }
250:
251: return $this->isAutoFieldsEnabled();
252: }
253:
254: /**
255: * Adds a new association to the list that will be used to filter the results of
256: * any given query based on the results of finding records for that association.
257: * You can pass a dot separated path of associations to this method as its first
258: * parameter, this will translate in setting all those associations with the
259: * `matching` option.
260: *
261: * ### Options
262: * - 'joinType': INNER, OUTER, ...
263: * - 'fields': Fields to contain
264: *
265: * @param string $assoc A single association or a dot separated path of associations.
266: * @param callable|null $builder the callback function to be used for setting extra
267: * options to the filtering query
268: * @param array $options Extra options for the association matching.
269: * @return $this
270: */
271: public function setMatching($assoc, callable $builder = null, $options = [])
272: {
273: if ($this->_matching === null) {
274: $this->_matching = new static();
275: }
276:
277: if (!isset($options['joinType'])) {
278: $options['joinType'] = QueryInterface::JOIN_TYPE_INNER;
279: }
280:
281: $assocs = explode('.', $assoc);
282: $last = array_pop($assocs);
283: $containments = [];
284: $pointer =& $containments;
285: $opts = ['matching' => true] + $options;
286: unset($opts['negateMatch']);
287:
288: foreach ($assocs as $name) {
289: $pointer[$name] = $opts;
290: $pointer =& $pointer[$name];
291: }
292:
293: $pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options;
294:
295: $this->_matching->contain($containments);
296:
297: return $this;
298: }
299:
300: /**
301: * Returns the current tree of associations to be matched.
302: *
303: * @return array The resulting containments array
304: */
305: public function getMatching()
306: {
307: if ($this->_matching === null) {
308: $this->_matching = new static();
309: }
310:
311: return $this->_matching->getContain();
312: }
313:
314: /**
315: * Adds a new association to the list that will be used to filter the results of
316: * any given query based on the results of finding records for that association.
317: * You can pass a dot separated path of associations to this method as its first
318: * parameter, this will translate in setting all those associations with the
319: * `matching` option.
320: *
321: * If called with no arguments it will return the current tree of associations to
322: * be matched.
323: *
324: * @deprecated 3.4.0 Use setMatching()/getMatching() instead.
325: * @param string|null $assoc A single association or a dot separated path of associations.
326: * @param callable|null $builder the callback function to be used for setting extra
327: * options to the filtering query
328: * @param array $options Extra options for the association matching, such as 'joinType'
329: * and 'fields'
330: * @return array The resulting containments array
331: */
332: public function matching($assoc = null, callable $builder = null, $options = [])
333: {
334: deprecationWarning(
335: 'EagerLoader::matching() is deprecated. ' .
336: 'Use setMatch()/getMatching() instead.'
337: );
338: if ($assoc !== null) {
339: $this->setMatching($assoc, $builder, $options);
340: }
341:
342: return $this->getMatching();
343: }
344:
345: /**
346: * Returns the fully normalized array of associations that should be eagerly
347: * loaded for a table. The normalized array will restructure the original array
348: * by sorting all associations under one key and special options under another.
349: *
350: * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable
351: * object, that contains all the information required for the association objects
352: * to load the information from the database.
353: *
354: * Additionally it will set an 'instance' key per association containing the
355: * association instance from the corresponding source table
356: *
357: * @param \Cake\ORM\Table $repository The table containing the association that
358: * will be normalized
359: * @return array
360: */
361: public function normalized(Table $repository)
362: {
363: if ($this->_normalized !== null || empty($this->_containments)) {
364: return (array)$this->_normalized;
365: }
366:
367: $contain = [];
368: foreach ($this->_containments as $alias => $options) {
369: if (!empty($options['instance'])) {
370: $contain = (array)$this->_containments;
371: break;
372: }
373: $contain[$alias] = $this->_normalizeContain(
374: $repository,
375: $alias,
376: $options,
377: ['root' => null]
378: );
379: }
380:
381: return $this->_normalized = $contain;
382: }
383:
384: /**
385: * Formats the containments array so that associations are always set as keys
386: * in the array. This function merges the original associations array with
387: * the new associations provided
388: *
389: * @param array $associations user provided containments array
390: * @param array $original The original containments array to merge
391: * with the new one
392: * @return array
393: */
394: protected function _reformatContain($associations, $original)
395: {
396: $result = $original;
397:
398: foreach ((array)$associations as $table => $options) {
399: $pointer =& $result;
400: if (is_int($table)) {
401: $table = $options;
402: $options = [];
403: }
404:
405: if ($options instanceof EagerLoadable) {
406: $options = $options->asContainArray();
407: $table = key($options);
408: $options = current($options);
409: }
410:
411: if (isset($this->_containOptions[$table])) {
412: $pointer[$table] = $options;
413: continue;
414: }
415:
416: if (strpos($table, '.')) {
417: $path = explode('.', $table);
418: $table = array_pop($path);
419: foreach ($path as $t) {
420: $pointer += [$t => []];
421: $pointer =& $pointer[$t];
422: }
423: }
424:
425: if (is_array($options)) {
426: $options = isset($options['config']) ?
427: $options['config'] + $options['associations'] :
428: $options;
429: $options = $this->_reformatContain(
430: $options,
431: isset($pointer[$table]) ? $pointer[$table] : []
432: );
433: }
434:
435: if ($options instanceof Closure) {
436: $options = ['queryBuilder' => $options];
437: }
438:
439: $pointer += [$table => []];
440:
441: if (isset($options['queryBuilder'], $pointer[$table]['queryBuilder'])) {
442: $first = $pointer[$table]['queryBuilder'];
443: $second = $options['queryBuilder'];
444: $options['queryBuilder'] = function ($query) use ($first, $second) {
445: return $second($first($query));
446: };
447: }
448:
449: if (!is_array($options)) {
450: $options = [$options => []];
451: }
452:
453: $pointer[$table] = $options + $pointer[$table];
454: }
455:
456: return $result;
457: }
458:
459: /**
460: * Modifies the passed query to apply joins or any other transformation required
461: * in order to eager load the associations described in the `contain` array.
462: * This method will not modify the query for loading external associations, i.e.
463: * those that cannot be loaded without executing a separate query.
464: *
465: * @param \Cake\ORM\Query $query The query to be modified
466: * @param \Cake\ORM\Table $repository The repository containing the associations
467: * @param bool $includeFields whether to append all fields from the associations
468: * to the passed query. This can be overridden according to the settings defined
469: * per association in the containments array
470: * @return void
471: */
472: public function attachAssociations(Query $query, Table $repository, $includeFields)
473: {
474: if (empty($this->_containments) && $this->_matching === null) {
475: return;
476: }
477:
478: $attachable = $this->attachableAssociations($repository);
479: $processed = [];
480: do {
481: foreach ($attachable as $alias => $loadable) {
482: $config = $loadable->getConfig() + [
483: 'aliasPath' => $loadable->aliasPath(),
484: 'propertyPath' => $loadable->propertyPath(),
485: 'includeFields' => $includeFields,
486: ];
487: $loadable->instance()->attachTo($query, $config);
488: $processed[$alias] = true;
489: }
490:
491: $newAttachable = $this->attachableAssociations($repository);
492: $attachable = array_diff_key($newAttachable, $processed);
493: } while (!empty($attachable));
494: }
495:
496: /**
497: * Returns an array with the associations that can be fetched using a single query,
498: * the array keys are the association aliases and the values will contain an array
499: * with Cake\ORM\EagerLoadable objects.
500: *
501: * @param \Cake\ORM\Table $repository The table containing the associations to be
502: * attached
503: * @return array
504: */
505: public function attachableAssociations(Table $repository)
506: {
507: $contain = $this->normalized($repository);
508: $matching = $this->_matching ? $this->_matching->normalized($repository) : [];
509: $this->_fixStrategies();
510: $this->_loadExternal = [];
511:
512: return $this->_resolveJoins($contain, $matching);
513: }
514:
515: /**
516: * Returns an array with the associations that need to be fetched using a
517: * separate query, each array value will contain a Cake\ORM\EagerLoadable object.
518: *
519: * @param \Cake\ORM\Table $repository The table containing the associations
520: * to be loaded
521: * @return \Cake\ORM\EagerLoadable[]
522: */
523: public function externalAssociations(Table $repository)
524: {
525: if ($this->_loadExternal) {
526: return $this->_loadExternal;
527: }
528:
529: $this->attachableAssociations($repository);
530:
531: return $this->_loadExternal;
532: }
533:
534: /**
535: * Auxiliary function responsible for fully normalizing deep associations defined
536: * using `contain()`
537: *
538: * @param \Cake\ORM\Table $parent owning side of the association
539: * @param string $alias name of the association to be loaded
540: * @param array $options list of extra options to use for this association
541: * @param array $paths An array with two values, the first one is a list of dot
542: * separated strings representing associations that lead to this `$alias` in the
543: * chain of associations to be loaded. The second value is the path to follow in
544: * entities' properties to fetch a record of the corresponding association.
545: * @return \Cake\ORM\EagerLoadable Object with normalized associations
546: * @throws \InvalidArgumentException When containments refer to associations that do not exist.
547: */
548: protected function _normalizeContain(Table $parent, $alias, $options, $paths)
549: {
550: $defaults = $this->_containOptions;
551: $instance = $parent->getAssociation($alias);
552: if (!$instance) {
553: throw new InvalidArgumentException(
554: sprintf('%s is not associated with %s', $parent->getAlias(), $alias)
555: );
556: }
557:
558: $paths += ['aliasPath' => '', 'propertyPath' => '', 'root' => $alias];
559: $paths['aliasPath'] .= '.' . $alias;
560: $paths['propertyPath'] .= '.' . $instance->getProperty();
561:
562: $table = $instance->getTarget();
563:
564: $extra = array_diff_key($options, $defaults);
565: $config = [
566: 'associations' => [],
567: 'instance' => $instance,
568: 'config' => array_diff_key($options, $extra),
569: 'aliasPath' => trim($paths['aliasPath'], '.'),
570: 'propertyPath' => trim($paths['propertyPath'], '.'),
571: 'targetProperty' => $instance->getProperty()
572: ];
573: $config['canBeJoined'] = $instance->canBeJoined($config['config']);
574: $eagerLoadable = new EagerLoadable($alias, $config);
575:
576: if ($config['canBeJoined']) {
577: $this->_aliasList[$paths['root']][$alias][] = $eagerLoadable;
578: } else {
579: $paths['root'] = $config['aliasPath'];
580: }
581:
582: foreach ($extra as $t => $assoc) {
583: $eagerLoadable->addAssociation(
584: $t,
585: $this->_normalizeContain($table, $t, $assoc, $paths)
586: );
587: }
588:
589: return $eagerLoadable;
590: }
591:
592: /**
593: * Iterates over the joinable aliases list and corrects the fetching strategies
594: * in order to avoid aliases collision in the generated queries.
595: *
596: * This function operates on the array references that were generated by the
597: * _normalizeContain() function.
598: *
599: * @return void
600: */
601: protected function _fixStrategies()
602: {
603: foreach ($this->_aliasList as $aliases) {
604: foreach ($aliases as $configs) {
605: if (count($configs) < 2) {
606: continue;
607: }
608: /* @var \Cake\ORM\EagerLoadable $loadable */
609: foreach ($configs as $loadable) {
610: if (strpos($loadable->aliasPath(), '.')) {
611: $this->_correctStrategy($loadable);
612: }
613: }
614: }
615: }
616: }
617:
618: /**
619: * Changes the association fetching strategy if required because of duplicate
620: * under the same direct associations chain
621: *
622: * @param \Cake\ORM\EagerLoadable $loadable The association config
623: * @return void
624: */
625: protected function _correctStrategy($loadable)
626: {
627: $config = $loadable->getConfig();
628: $currentStrategy = isset($config['strategy']) ?
629: $config['strategy'] :
630: 'join';
631:
632: if (!$loadable->canBeJoined() || $currentStrategy !== 'join') {
633: return;
634: }
635:
636: $config['strategy'] = Association::STRATEGY_SELECT;
637: $loadable->setConfig($config);
638: $loadable->setCanBeJoined(false);
639: }
640:
641: /**
642: * Helper function used to compile a list of all associations that can be
643: * joined in the query.
644: *
645: * @param array $associations list of associations from which to obtain joins.
646: * @param array $matching list of associations that should be forcibly joined.
647: * @return array
648: */
649: protected function _resolveJoins($associations, $matching = [])
650: {
651: $result = [];
652: foreach ($matching as $table => $loadable) {
653: $result[$table] = $loadable;
654: $result += $this->_resolveJoins($loadable->associations(), []);
655: }
656: foreach ($associations as $table => $loadable) {
657: $inMatching = isset($matching[$table]);
658: if (!$inMatching && $loadable->canBeJoined()) {
659: $result[$table] = $loadable;
660: $result += $this->_resolveJoins($loadable->associations(), []);
661: continue;
662: }
663:
664: if ($inMatching) {
665: $this->_correctStrategy($loadable);
666: }
667:
668: $loadable->setCanBeJoined(false);
669: $this->_loadExternal[] = $loadable;
670: }
671:
672: return $result;
673: }
674:
675: /**
676: * Decorates the passed statement object in order to inject data from associations
677: * that cannot be joined directly.
678: *
679: * @param \Cake\ORM\Query $query The query for which to eager load external
680: * associations
681: * @param \Cake\Database\StatementInterface $statement The statement created after executing the $query
682: * @return \Cake\Database\StatementInterface statement modified statement with extra loaders
683: */
684: public function loadExternal($query, $statement)
685: {
686: $external = $this->externalAssociations($query->getRepository());
687: if (empty($external)) {
688: return $statement;
689: }
690:
691: $driver = $query->getConnection()->getDriver();
692: list($collected, $statement) = $this->_collectKeys($external, $query, $statement);
693:
694: foreach ($external as $meta) {
695: $contain = $meta->associations();
696: $instance = $meta->instance();
697: $config = $meta->getConfig();
698: $alias = $instance->getSource()->getAlias();
699: $path = $meta->aliasPath();
700:
701: $requiresKeys = $instance->requiresKeys($config);
702: if ($requiresKeys && empty($collected[$path][$alias])) {
703: continue;
704: }
705:
706: $keys = isset($collected[$path][$alias]) ? $collected[$path][$alias] : null;
707: $f = $instance->eagerLoader(
708: $config + [
709: 'query' => $query,
710: 'contain' => $contain,
711: 'keys' => $keys,
712: 'nestKey' => $meta->aliasPath()
713: ]
714: );
715: $statement = new CallbackStatement($statement, $driver, $f);
716: }
717:
718: return $statement;
719: }
720:
721: /**
722: * Returns an array having as keys a dotted path of associations that participate
723: * in this eager loader. The values of the array will contain the following keys
724: *
725: * - alias: The association alias
726: * - instance: The association instance
727: * - canBeJoined: Whether or not the association will be loaded using a JOIN
728: * - entityClass: The entity that should be used for hydrating the results
729: * - nestKey: A dotted path that can be used to correctly insert the data into the results.
730: * - matching: Whether or not it is an association loaded through `matching()`.
731: *
732: * @param \Cake\ORM\Table $table The table containing the association that
733: * will be normalized
734: * @return array
735: */
736: public function associationsMap($table)
737: {
738: $map = [];
739:
740: if (!$this->getMatching() && !$this->getContain() && empty($this->_joinsMap)) {
741: return $map;
742: }
743:
744: $map = $this->_buildAssociationsMap($map, $this->_matching->normalized($table), true);
745: $map = $this->_buildAssociationsMap($map, $this->normalized($table));
746: $map = $this->_buildAssociationsMap($map, $this->_joinsMap);
747:
748: return $map;
749: }
750:
751: /**
752: * An internal method to build a map which is used for the return value of the
753: * associationsMap() method.
754: *
755: * @param array $map An initial array for the map.
756: * @param array $level An array of EagerLoadable instances.
757: * @param bool $matching Whether or not it is an association loaded through `matching()`.
758: * @return array
759: */
760: protected function _buildAssociationsMap($map, $level, $matching = false)
761: {
762: /* @var \Cake\ORM\EagerLoadable $meta */
763: foreach ($level as $assoc => $meta) {
764: $canBeJoined = $meta->canBeJoined();
765: $instance = $meta->instance();
766: $associations = $meta->associations();
767: $forMatching = $meta->forMatching();
768: $map[] = [
769: 'alias' => $assoc,
770: 'instance' => $instance,
771: 'canBeJoined' => $canBeJoined,
772: 'entityClass' => $instance->getTarget()->getEntityClass(),
773: 'nestKey' => $canBeJoined ? $assoc : $meta->aliasPath(),
774: 'matching' => $forMatching !== null ? $forMatching : $matching,
775: 'targetProperty' => $meta->targetProperty()
776: ];
777: if ($canBeJoined && $associations) {
778: $map = $this->_buildAssociationsMap($map, $associations, $matching);
779: }
780: }
781:
782: return $map;
783: }
784:
785: /**
786: * Registers a table alias, typically loaded as a join in a query, as belonging to
787: * an association. This helps hydrators know what to do with the columns coming
788: * from such joined table.
789: *
790: * @param string $alias The table alias as it appears in the query.
791: * @param \Cake\ORM\Association $assoc The association object the alias represents;
792: * will be normalized
793: * @param bool $asMatching Whether or not this join results should be treated as a
794: * 'matching' association.
795: * @param string $targetProperty The property name where the results of the join should be nested at.
796: * If not passed, the default property for the association will be used.
797: * @return void
798: */
799: public function addToJoinsMap($alias, Association $assoc, $asMatching = false, $targetProperty = null)
800: {
801: $this->_joinsMap[$alias] = new EagerLoadable($alias, [
802: 'aliasPath' => $alias,
803: 'instance' => $assoc,
804: 'canBeJoined' => true,
805: 'forMatching' => $asMatching,
806: 'targetProperty' => $targetProperty ?: $assoc->getProperty()
807: ]);
808: }
809:
810: /**
811: * Helper function used to return the keys from the query records that will be used
812: * to eagerly load associations.
813: *
814: * @param array $external the list of external associations to be loaded
815: * @param \Cake\ORM\Query $query The query from which the results where generated
816: * @param \Cake\Database\Statement\BufferedStatement $statement The statement to work on
817: * @return array
818: */
819: protected function _collectKeys($external, $query, $statement)
820: {
821: $collectKeys = [];
822: /* @var \Cake\ORM\EagerLoadable $meta */
823: foreach ($external as $meta) {
824: $instance = $meta->instance();
825: if (!$instance->requiresKeys($meta->getConfig())) {
826: continue;
827: }
828:
829: $source = $instance->getSource();
830: $keys = $instance->type() === Association::MANY_TO_ONE ?
831: (array)$instance->getForeignKey() :
832: (array)$instance->getBindingKey();
833:
834: $alias = $source->getAlias();
835: $pkFields = [];
836: foreach ($keys as $key) {
837: $pkFields[] = key($query->aliasField($key, $alias));
838: }
839: $collectKeys[$meta->aliasPath()] = [$alias, $pkFields, count($pkFields) === 1];
840: }
841:
842: if (empty($collectKeys)) {
843: return [[], $statement];
844: }
845:
846: if (!($statement instanceof BufferedStatement)) {
847: $statement = new BufferedStatement($statement, $query->getConnection()->getDriver());
848: }
849:
850: return [$this->_groupKeys($statement, $collectKeys), $statement];
851: }
852:
853: /**
854: * Helper function used to iterate a statement and extract the columns
855: * defined in $collectKeys
856: *
857: * @param \Cake\Database\Statement\BufferedStatement $statement The statement to read from.
858: * @param array $collectKeys The keys to collect
859: * @return array
860: */
861: protected function _groupKeys($statement, $collectKeys)
862: {
863: $keys = [];
864: while ($result = $statement->fetch('assoc')) {
865: foreach ($collectKeys as $nestKey => $parts) {
866: // Missed joins will have null in the results.
867: if ($parts[2] === true && !isset($result[$parts[1][0]])) {
868: continue;
869: }
870: if ($parts[2] === true) {
871: $value = $result[$parts[1][0]];
872: $keys[$nestKey][$parts[0]][$value] = $value;
873: continue;
874: }
875:
876: // Handle composite keys.
877: $collected = [];
878: foreach ($parts[1] as $key) {
879: $collected[] = $result[$key];
880: }
881: $keys[$nestKey][$parts[0]][implode(';', $collected)] = $collected;
882: }
883: }
884:
885: $statement->rewind();
886:
887: return $keys;
888: }
889:
890: /**
891: * Clone hook implementation
892: *
893: * Clone the _matching eager loader as well.
894: *
895: * @return void
896: */
897: public function __clone()
898: {
899: if ($this->_matching) {
900: $this->_matching = clone $this->_matching;
901: }
902: }
903: }
904: