CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Team
    • Issues (Github)
    • YouTube Channel
    • Get Involved
    • Bakery
    • Featured Resources
    • Newsletter
    • Certification
    • My CakePHP
    • CakeFest
    • Facebook
    • Twitter
    • Help & Support
    • Forum
    • Stack Overflow
    • IRC
    • Slack
    • Paid Support
CakePHP

C CakePHP 3.7 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.7
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Namespaces

  • Cake
    • Auth
      • Storage
    • Cache
      • Engine
    • Collection
      • Iterator
    • Command
    • Console
      • Exception
    • Controller
      • Component
      • Exception
    • Core
      • Configure
        • Engine
      • Exception
      • Retry
    • Database
      • Driver
      • Exception
      • Expression
      • Schema
      • Statement
      • Type
    • Datasource
      • Exception
    • Error
      • Middleware
    • Event
      • Decorator
    • Filesystem
    • Form
    • Http
      • Client
        • Adapter
        • Auth
      • Cookie
      • Exception
      • Middleware
      • Session
    • I18n
      • Formatter
      • Middleware
      • Parser
    • Log
      • Engine
    • Mailer
      • Exception
      • Transport
    • Network
      • Exception
    • ORM
      • Association
      • Behavior
        • Translate
      • Exception
      • Locator
      • Rule
    • Routing
      • Exception
      • Filter
      • Middleware
      • Route
    • Shell
      • Helper
      • Task
    • TestSuite
      • Fixture
      • Stub
    • Utility
      • Exception
    • Validation
    • View
      • Exception
      • Form
      • Helper
      • Widget
  • None

Classes

  • Association
  • AssociationCollection
  • Behavior
  • BehaviorRegistry
  • EagerLoader
  • Entity
  • Marshaller
  • Query
  • ResultSet
  • RulesChecker
  • SaveOptionsBuilder
  • Table
  • TableRegistry

Interfaces

  • PropertyMarshalInterface

Traits

  • AssociationsNormalizerTrait
  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: 
Follow @CakePHP
#IRC
OpenHub
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Logos & Trademarks
  • Community
  • Team
  • Issues (Github)
  • YouTube Channel
  • Get Involved
  • Bakery
  • Featured Resources
  • Newsletter
  • Certification
  • My CakePHP
  • CakeFest
  • Facebook
  • Twitter
  • Help & Support
  • Forum
  • Stack Overflow
  • IRC
  • Slack
  • Paid Support

Generated using CakePHP API Docs