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