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.8 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.8
      • 3.8
      • 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:      * 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: 
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