Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/cakephp/cakephp/src/ORM/EagerLoader.php
<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\ORM;

use
Cake\Database\Statement\BufferedStatement;
use
Cake\Database\Statement\CallbackStatement;
use
Cake\Datasource\QueryInterface;
use
Closure;
use
InvalidArgumentException;

/**
 * Exposes the methods for storing the associations that should be eager loaded
 * for a table once a query is provided and delegates the job of creating the
 * required joins and decorating the results so that those associations can be
 * part of the result set.
 */
class EagerLoader
{
   
/**
     * Nested array describing the association to be fetched
     * and the options to apply for each of them, if any
     *
     * @var array
     */
   
protected $_containments = [];

   
/**
     * Contains a nested array with the compiled containments tree
     * This is a normalized version of the user provided containments array.
     *
     * @var \Cake\ORM\EagerLoadable[]|\Cake\ORM\EagerLoadable|null
     */
   
protected $_normalized;

   
/**
     * List of options accepted by associations in contain()
     * index by key for faster access
     *
     * @var array
     */
   
protected $_containOptions = [
       
'associations' => 1,
       
'foreignKey' => 1,
       
'conditions' => 1,
       
'fields' => 1,
       
'sort' => 1,
       
'matching' => 1,
       
'queryBuilder' => 1,
       
'finder' => 1,
       
'joinType' => 1,
       
'strategy' => 1,
       
'negateMatch' => 1,
    ];

   
/**
     * A list of associations that should be loaded with a separate query
     *
     * @var \Cake\ORM\EagerLoadable[]
     */
   
protected $_loadExternal = [];

   
/**
     * Contains a list of the association names that are to be eagerly loaded
     *
     * @var array
     */
   
protected $_aliasList = [];

   
/**
     * Another EagerLoader instance that will be used for 'matching' associations.
     *
     * @var \Cake\ORM\EagerLoader
     */
   
protected $_matching;

   
/**
     * A map of table aliases pointing to the association objects they represent
     * for the query.
     *
     * @var array
     */
   
protected $_joinsMap = [];

   
/**
     * Controls whether or not fields from associated tables
     * will be eagerly loaded. When set to false, no fields will
     * be loaded from associations.
     *
     * @var bool
     */
   
protected $_autoFields = true;

   
/**
     * Sets the list of associations that should be eagerly loaded along for a
     * specific table using when a query is provided. The list of associated tables
     * passed to this method must have been previously set as associations using the
     * Table API.
     *
     * Associations can be arbitrarily nested using dot notation or nested arrays,
     * this allows this object to calculate joins or any additional queries that
     * must be executed to bring the required associated data.
     *
     * The getter part is deprecated as of 3.6.0. Use getContain() instead.
     *
     * Accepted options per passed association:
     *
     * - foreignKey: Used to set a different field to match both tables, if set to false
     *   no join conditions will be generated automatically
     * - fields: An array with the fields that should be fetched from the association
     * - queryBuilder: Equivalent to passing a callable instead of an options array
     * - matching: Whether to inform the association class that it should filter the
     *  main query by the results fetched by that class.
     * - joinType: For joinable associations, the SQL join type to use.
     * - strategy: The loading strategy to use (join, select, subquery)
     *
     * @param array|string $associations list of table aliases to be queried.
     * When this method is called multiple times it will merge previous list with
     * the new one.
     * @param callable|null $queryBuilder The query builder callable
     * @return array Containments.
     * @throws \InvalidArgumentException When using $queryBuilder with an array of $associations
     */
   
public function contain($associations = [], callable $queryBuilder = null)
    {
        if (empty(
$associations)) {
           
deprecationWarning(
               
'Using EagerLoader::contain() as getter is deprecated. ' .
               
'Use getContain() instead.'
           
);

            return
$this->getContain();
        }

        if (
$queryBuilder) {
            if (!
is_string($associations)) {
                throw new
InvalidArgumentException(
                   
sprintf('Cannot set containments. To use $queryBuilder, $associations must be a string')
                );
            }

           
$associations = [
               
$associations => [
                   
'queryBuilder' => $queryBuilder,
                ],
            ];
        }

       
$associations = (array)$associations;
       
$associations = $this->_reformatContain($associations, $this->_containments);
       
$this->_normalized = null;
       
$this->_loadExternal = [];
       
$this->_aliasList = [];

        return
$this->_containments = $associations;
    }

   
/**
     * Gets the list of associations that should be eagerly loaded along for a
     * specific table using when a query is provided. The list of associated tables
     * passed to this method must have been previously set as associations using the
     * Table API.
     *
     * @return array Containments.
     */
   
public function getContain()
    {
        return
$this->_containments;
    }

   
/**
     * Remove any existing non-matching based containments.
     *
     * This will reset/clear out any contained associations that were not
     * added via matching().
     *
     * @return void
     */
   
public function clearContain()
    {
       
$this->_containments = [];
       
$this->_normalized = null;
       
$this->_loadExternal = [];
       
$this->_aliasList = [];
    }

   
/**
     * Sets whether or not contained associations will load fields automatically.
     *
     * @param bool $enable The value to set.
     * @return $this
     */
   
public function enableAutoFields($enable = true)
    {
       
$this->_autoFields = (bool)$enable;

        return
$this;
    }

   
/**
     * Disable auto loading fields of contained associations.
     *
     * @return $this
     */
   
public function disableAutoFields()
    {
       
$this->_autoFields = false;

        return
$this;
    }

   
/**
     * Gets whether or not contained associations will load fields automatically.
     *
     * @return bool The current value.
     */
   
public function isAutoFieldsEnabled()
    {
        return
$this->_autoFields;
    }

   
/**
     * Sets/Gets whether or not contained associations will load fields automatically.
     *
     * @deprecated 3.4.0 Use enableAutoFields()/isAutoFieldsEnabled() instead.
     * @param bool|null $enable The value to set.
     * @return bool The current value.
     */
   
public function autoFields($enable = null)
    {
       
deprecationWarning(
           
'EagerLoader::autoFields() is deprecated. ' .
           
'Use enableAutoFields()/isAutoFieldsEnabled() instead.'
       
);
        if (
$enable !== null) {
           
$this->enableAutoFields($enable);
        }

        return
$this->isAutoFieldsEnabled();
    }

   
/**
     * Adds a new association to the list that will be used to filter the results of
     * any given query based on the results of finding records for that association.
     * You can pass a dot separated path of associations to this method as its first
     * parameter, this will translate in setting all those associations with the
     * `matching` option.
     *
     *  ### Options
     *  - 'joinType': INNER, OUTER, ...
     *  - 'fields': Fields to contain
     *
     * @param string $assoc A single association or a dot separated path of associations.
     * @param callable|null $builder the callback function to be used for setting extra
     * options to the filtering query
     * @param array $options Extra options for the association matching.
     * @return $this
     */
   
public function setMatching($assoc, callable $builder = null, $options = [])
    {
        if (
$this->_matching === null) {
           
$this->_matching = new static();
        }

        if (!isset(
$options['joinType'])) {
           
$options['joinType'] = QueryInterface::JOIN_TYPE_INNER;
        }

       
$assocs = explode('.', $assoc);
       
$last = array_pop($assocs);
       
$containments = [];
       
$pointer =& $containments;
       
$opts = ['matching' => true] + $options;
        unset(
$opts['negateMatch']);

        foreach (
$assocs as $name) {
           
$pointer[$name] = $opts;
           
$pointer =& $pointer[$name];
        }

       
$pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options;

       
$this->_matching->contain($containments);

        return
$this;
    }

   
/**
     * Returns the current tree of associations to be matched.
     *
     * @return array The resulting containments array
     */
   
public function getMatching()
    {
        if (
$this->_matching === null) {
           
$this->_matching = new static();
        }

        return
$this->_matching->getContain();
    }

   
/**
     * Adds a new association to the list that will be used to filter the results of
     * any given query based on the results of finding records for that association.
     * You can pass a dot separated path of associations to this method as its first
     * parameter, this will translate in setting all those associations with the
     * `matching` option.
     *
     * If called with no arguments it will return the current tree of associations to
     * be matched.
     *
     * @deprecated 3.4.0 Use setMatching()/getMatching() instead.
     * @param string|null $assoc A single association or a dot separated path of associations.
     * @param callable|null $builder the callback function to be used for setting extra
     * options to the filtering query
     * @param array $options Extra options for the association matching, such as 'joinType'
     * and 'fields'
     * @return array The resulting containments array
     */
   
public function matching($assoc = null, callable $builder = null, $options = [])
    {
       
deprecationWarning(
           
'EagerLoader::matching() is deprecated. ' .
           
'Use setMatch()/getMatching() instead.'
       
);
        if (
$assoc !== null) {
           
$this->setMatching($assoc, $builder, $options);
        }

        return
$this->getMatching();
    }

   
/**
     * Returns the fully normalized array of associations that should be eagerly
     * loaded for a table. The normalized array will restructure the original array
     * by sorting all associations under one key and special options under another.
     *
     * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable
     * object, that contains all the information required for the association objects
     * to load the information from the database.
     *
     * Additionally it will set an 'instance' key per association containing the
     * association instance from the corresponding source table
     *
     * @param \Cake\ORM\Table $repository The table containing the association that
     * will be normalized
     * @return array
     */
   
public function normalized(Table $repository)
    {
        if (
$this->_normalized !== null || empty($this->_containments)) {
            return (array)
$this->_normalized;
        }

       
$contain = [];
        foreach (
$this->_containments as $alias => $options) {
            if (!empty(
$options['instance'])) {
               
$contain = (array)$this->_containments;
                break;
            }
           
$contain[$alias] = $this->_normalizeContain(
               
$repository,
               
$alias,
               
$options,
                [
'root' => null]
            );
        }

        return
$this->_normalized = $contain;
    }

   
/**
     * Formats the containments array so that associations are always set as keys
     * in the array. This function merges the original associations array with
     * the new associations provided
     *
     * @param array $associations user provided containments array
     * @param array $original The original containments array to merge
     * with the new one
     * @return array
     */
   
protected function _reformatContain($associations, $original)
    {
       
$result = $original;

        foreach ((array)
$associations as $table => $options) {
           
$pointer =& $result;
            if (
is_int($table)) {
               
$table = $options;
               
$options = [];
            }

            if (
$options instanceof EagerLoadable) {
               
$options = $options->asContainArray();
               
$table = key($options);
               
$options = current($options);
            }

            if (isset(
$this->_containOptions[$table])) {
               
$pointer[$table] = $options;
                continue;
            }

            if (
strpos($table, '.')) {
               
$path = explode('.', $table);
               
$table = array_pop($path);
                foreach (
$path as $t) {
                   
$pointer += [$t => []];
                   
$pointer =& $pointer[$t];
                }
            }

            if (
is_array($options)) {
               
$options = isset($options['config']) ?
                   
$options['config'] + $options['associations'] :
                   
$options;
               
$options = $this->_reformatContain(
                   
$options,
                    isset(
$pointer[$table]) ? $pointer[$table] : []
                );
            }

            if (
$options instanceof Closure) {
               
$options = ['queryBuilder' => $options];
            }

           
$pointer += [$table => []];

            if (isset(
$options['queryBuilder'], $pointer[$table]['queryBuilder'])) {
               
$first = $pointer[$table]['queryBuilder'];
               
$second = $options['queryBuilder'];
               
$options['queryBuilder'] = function ($query) use ($first, $second) {
                    return
$second($first($query));
                };
            }

            if (!
is_array($options)) {
               
$options = [$options => []];
            }

           
$pointer[$table] = $options + $pointer[$table];
        }

        return
$result;
    }

   
/**
     * Modifies the passed query to apply joins or any other transformation required
     * in order to eager load the associations described in the `contain` array.
     * This method will not modify the query for loading external associations, i.e.
     * those that cannot be loaded without executing a separate query.
     *
     * @param \Cake\ORM\Query $query The query to be modified
     * @param \Cake\ORM\Table $repository The repository containing the associations
     * @param bool $includeFields whether to append all fields from the associations
     * to the passed query. This can be overridden according to the settings defined
     * per association in the containments array
     * @return void
     */
   
public function attachAssociations(Query $query, Table $repository, $includeFields)
    {
        if (empty(
$this->_containments) && $this->_matching === null) {
            return;
        }

       
$attachable = $this->attachableAssociations($repository);
       
$processed = [];
        do {
            foreach (
$attachable as $alias => $loadable) {
               
$config = $loadable->getConfig() + [
                   
'aliasPath' => $loadable->aliasPath(),
                   
'propertyPath' => $loadable->propertyPath(),
                   
'includeFields' => $includeFields,
                ];
               
$loadable->instance()->attachTo($query, $config);
               
$processed[$alias] = true;
            }

           
$newAttachable = $this->attachableAssociations($repository);
           
$attachable = array_diff_key($newAttachable, $processed);
        } while (!empty(
$attachable));
    }

   
/**
     * Returns an array with the associations that can be fetched using a single query,
     * the array keys are the association aliases and the values will contain an array
     * with Cake\ORM\EagerLoadable objects.
     *
     * @param \Cake\ORM\Table $repository The table containing the associations to be
     * attached
     * @return array
     */
   
public function attachableAssociations(Table $repository)
    {
       
$contain = $this->normalized($repository);
       
$matching = $this->_matching ? $this->_matching->normalized($repository) : [];
       
$this->_fixStrategies();
       
$this->_loadExternal = [];

        return
$this->_resolveJoins($contain, $matching);
    }

   
/**
     * Returns an array with the associations that need to be fetched using a
     * separate query, each array value will contain a Cake\ORM\EagerLoadable object.
     *
     * @param \Cake\ORM\Table $repository The table containing the associations
     * to be loaded
     * @return \Cake\ORM\EagerLoadable[]
     */
   
public function externalAssociations(Table $repository)
    {
        if (
$this->_loadExternal) {
            return
$this->_loadExternal;
        }

       
$this->attachableAssociations($repository);

        return
$this->_loadExternal;
    }

   
/**
     * Auxiliary function responsible for fully normalizing deep associations defined
     * using `contain()`
     *
     * @param \Cake\ORM\Table $parent owning side of the association
     * @param string $alias name of the association to be loaded
     * @param array $options list of extra options to use for this association
     * @param array $paths An array with two values, the first one is a list of dot
     * separated strings representing associations that lead to this `$alias` in the
     * chain of associations to be loaded. The second value is the path to follow in
     * entities' properties to fetch a record of the corresponding association.
     * @return \Cake\ORM\EagerLoadable Object with normalized associations
     * @throws \InvalidArgumentException When containments refer to associations that do not exist.
     */
   
protected function _normalizeContain(Table $parent, $alias, $options, $paths)
    {
       
$defaults = $this->_containOptions;
       
$instance = $parent->getAssociation($alias);
        if (!
$instance) {
            throw new
InvalidArgumentException(
               
sprintf('%s is not associated with %s', $parent->getAlias(), $alias)
            );
        }

       
$paths += ['aliasPath' => '', 'propertyPath' => '', 'root' => $alias];
       
$paths['aliasPath'] .= '.' . $alias;

        if (
            isset(
$options['matching']) &&
           
$options['matching'] === true
       
) {
           
$paths['propertyPath'] = '_matchingData.' . $alias;
        } else {
           
$paths['propertyPath'] .= '.' . $instance->getProperty();
        }

       
$table = $instance->getTarget();

       
$extra = array_diff_key($options, $defaults);
       
$config = [
           
'associations' => [],
           
'instance' => $instance,
           
'config' => array_diff_key($options, $extra),
           
'aliasPath' => trim($paths['aliasPath'], '.'),
           
'propertyPath' => trim($paths['propertyPath'], '.'),
           
'targetProperty' => $instance->getProperty(),
        ];
       
$config['canBeJoined'] = $instance->canBeJoined($config['config']);
       
$eagerLoadable = new EagerLoadable($alias, $config);

        if (
$config['canBeJoined']) {
           
$this->_aliasList[$paths['root']][$alias][] = $eagerLoadable;
        } else {
           
$paths['root'] = $config['aliasPath'];
        }

        foreach (
$extra as $t => $assoc) {
           
$eagerLoadable->addAssociation(
               
$t,
               
$this->_normalizeContain($table, $t, $assoc, $paths)
            );
        }

        return
$eagerLoadable;
    }

   
/**
     * Iterates over the joinable aliases list and corrects the fetching strategies
     * in order to avoid aliases collision in the generated queries.
     *
     * This function operates on the array references that were generated by the
     * _normalizeContain() function.
     *
     * @return void
     */
   
protected function _fixStrategies()
    {
        foreach (
$this->_aliasList as $aliases) {
            foreach (
$aliases as $configs) {
                if (
count($configs) < 2) {
                    continue;
                }
               
/** @var \Cake\ORM\EagerLoadable $loadable */
               
foreach ($configs as $loadable) {
                    if (
strpos($loadable->aliasPath(), '.')) {
                       
$this->_correctStrategy($loadable);
                    }
                }
            }
        }
    }

   
/**
     * Changes the association fetching strategy if required because of duplicate
     * under the same direct associations chain
     *
     * @param \Cake\ORM\EagerLoadable $loadable The association config
     * @return void
     */
   
protected function _correctStrategy($loadable)
    {
       
$config = $loadable->getConfig();
       
$currentStrategy = isset($config['strategy']) ?
           
$config['strategy'] :
           
'join';

        if (!
$loadable->canBeJoined() || $currentStrategy !== 'join') {
            return;
        }

       
$config['strategy'] = Association::STRATEGY_SELECT;
       
$loadable->setConfig($config);
       
$loadable->setCanBeJoined(false);
    }

   
/**
     * Helper function used to compile a list of all associations that can be
     * joined in the query.
     *
     * @param array $associations list of associations from which to obtain joins.
     * @param array $matching list of associations that should be forcibly joined.
     * @return array
     */
   
protected function _resolveJoins($associations, $matching = [])
    {
       
$result = [];
        foreach (
$matching as $table => $loadable) {
           
$result[$table] = $loadable;
           
$result += $this->_resolveJoins($loadable->associations(), []);
        }
        foreach (
$associations as $table => $loadable) {
           
$inMatching = isset($matching[$table]);
            if (!
$inMatching && $loadable->canBeJoined()) {
               
$result[$table] = $loadable;
               
$result += $this->_resolveJoins($loadable->associations(), []);
                continue;
            }

            if (
$inMatching) {
               
$this->_correctStrategy($loadable);
            }

           
$loadable->setCanBeJoined(false);
           
$this->_loadExternal[] = $loadable;
        }

        return
$result;
    }

   
/**
     * Decorates the passed statement object in order to inject data from associations
     * that cannot be joined directly.
     *
     * @param \Cake\ORM\Query $query The query for which to eager load external
     * associations
     * @param \Cake\Database\StatementInterface $statement The statement created after executing the $query
     * @return \Cake\Database\StatementInterface statement modified statement with extra loaders
     */
   
public function loadExternal($query, $statement)
    {
       
$external = $this->externalAssociations($query->getRepository());
        if (empty(
$external)) {
            return
$statement;
        }

       
$driver = $query->getConnection()->getDriver();
        list(
$collected, $statement) = $this->_collectKeys($external, $query, $statement);

        foreach (
$external as $meta) {
           
$contain = $meta->associations();
           
$instance = $meta->instance();
           
$config = $meta->getConfig();
           
$alias = $instance->getSource()->getAlias();
           
$path = $meta->aliasPath();

           
$requiresKeys = $instance->requiresKeys($config);
            if (
$requiresKeys && empty($collected[$path][$alias])) {
                continue;
            }

           
$keys = isset($collected[$path][$alias]) ? $collected[$path][$alias] : null;
           
$f = $instance->eagerLoader(
               
$config + [
                   
'query' => $query,
                   
'contain' => $contain,
                   
'keys' => $keys,
                   
'nestKey' => $meta->aliasPath(),
                ]
            );
           
$statement = new CallbackStatement($statement, $driver, $f);
        }

        return
$statement;
    }

   
/**
     * Returns an array having as keys a dotted path of associations that participate
     * in this eager loader. The values of the array will contain the following keys
     *
     * - alias: The association alias
     * - instance: The association instance
     * - canBeJoined: Whether or not the association will be loaded using a JOIN
     * - entityClass: The entity that should be used for hydrating the results
     * - nestKey: A dotted path that can be used to correctly insert the data into the results.
     * - matching: Whether or not it is an association loaded through `matching()`.
     *
     * @param \Cake\ORM\Table $table The table containing the association that
     * will be normalized
     * @return array
     */
   
public function associationsMap($table)
    {
       
$map = [];

        if (!
$this->getMatching() && !$this->getContain() && empty($this->_joinsMap)) {
            return
$map;
        }

       
$map = $this->_buildAssociationsMap($map, $this->_matching->normalized($table), true);
       
$map = $this->_buildAssociationsMap($map, $this->normalized($table));
       
$map = $this->_buildAssociationsMap($map, $this->_joinsMap);

        return
$map;
    }

   
/**
     * An internal method to build a map which is used for the return value of the
     * associationsMap() method.
     *
     * @param array $map An initial array for the map.
     * @param array $level An array of EagerLoadable instances.
     * @param bool $matching Whether or not it is an association loaded through `matching()`.
     * @return array
     */
   
protected function _buildAssociationsMap($map, $level, $matching = false)
    {
       
/** @var \Cake\ORM\EagerLoadable $meta */
       
foreach ($level as $assoc => $meta) {
           
$canBeJoined = $meta->canBeJoined();
           
$instance = $meta->instance();
           
$associations = $meta->associations();
           
$forMatching = $meta->forMatching();
           
$map[] = [
               
'alias' => $assoc,
               
'instance' => $instance,
               
'canBeJoined' => $canBeJoined,
               
'entityClass' => $instance->getTarget()->getEntityClass(),
               
'nestKey' => $canBeJoined ? $assoc : $meta->aliasPath(),
               
'matching' => $forMatching !== null ? $forMatching : $matching,
               
'targetProperty' => $meta->targetProperty(),
            ];
            if (
$canBeJoined && $associations) {
               
$map = $this->_buildAssociationsMap($map, $associations, $matching);
            }
        }

        return
$map;
    }

   
/**
     * Registers a table alias, typically loaded as a join in a query, as belonging to
     * an association. This helps hydrators know what to do with the columns coming
     * from such joined table.
     *
     * @param string $alias The table alias as it appears in the query.
     * @param \Cake\ORM\Association $assoc The association object the alias represents;
     * will be normalized
     * @param bool $asMatching Whether or not this join results should be treated as a
     * 'matching' association.
     * @param string $targetProperty The property name where the results of the join should be nested at.
     * If not passed, the default property for the association will be used.
     * @return void
     */
   
public function addToJoinsMap($alias, Association $assoc, $asMatching = false, $targetProperty = null)
    {
       
$this->_joinsMap[$alias] = new EagerLoadable($alias, [
           
'aliasPath' => $alias,
           
'instance' => $assoc,
           
'canBeJoined' => true,
           
'forMatching' => $asMatching,
           
'targetProperty' => $targetProperty ?: $assoc->getProperty(),
        ]);
    }

   
/**
     * Helper function used to return the keys from the query records that will be used
     * to eagerly load associations.
     *
     * @param array $external the list of external associations to be loaded
     * @param \Cake\ORM\Query $query The query from which the results where generated
     * @param \Cake\Database\Statement\BufferedStatement $statement The statement to work on
     * @return array
     */
   
protected function _collectKeys($external, $query, $statement)
    {
       
$collectKeys = [];
       
/** @var \Cake\ORM\EagerLoadable $meta */
       
foreach ($external as $meta) {
           
$instance = $meta->instance();
            if (!
$instance->requiresKeys($meta->getConfig())) {
                continue;
            }

           
$source = $instance->getSource();
           
$keys = $instance->type() === Association::MANY_TO_ONE ?
                (array)
$instance->getForeignKey() :
                (array)
$instance->getBindingKey();

           
$alias = $source->getAlias();
           
$pkFields = [];
            foreach (
$keys as $key) {
               
$pkFields[] = key($query->aliasField($key, $alias));
            }
           
$collectKeys[$meta->aliasPath()] = [$alias, $pkFields, count($pkFields) === 1];
        }

        if (empty(
$collectKeys)) {
            return [[],
$statement];
        }

        if (!(
$statement instanceof BufferedStatement)) {
           
$statement = new BufferedStatement($statement, $query->getConnection()->getDriver());
        }

        return [
$this->_groupKeys($statement, $collectKeys), $statement];
    }

   
/**
     * Helper function used to iterate a statement and extract the columns
     * defined in $collectKeys
     *
     * @param \Cake\Database\Statement\BufferedStatement $statement The statement to read from.
     * @param array $collectKeys The keys to collect
     * @return array
     */
   
protected function _groupKeys($statement, $collectKeys)
    {
       
$keys = [];
        while (
$result = $statement->fetch('assoc')) {
            foreach (
$collectKeys as $nestKey => $parts) {
               
// Missed joins will have null in the results.
               
if ($parts[2] === true && !isset($result[$parts[1][0]])) {
                    continue;
                }
                if (
$parts[2] === true) {
                   
$value = $result[$parts[1][0]];
                   
$keys[$nestKey][$parts[0]][$value] = $value;
                    continue;
                }

               
// Handle composite keys.
               
$collected = [];
                foreach (
$parts[1] as $key) {
                   
$collected[] = $result[$key];
                }
               
$keys[$nestKey][$parts[0]][implode(';', $collected)] = $collected;
            }
        }

       
$statement->rewind();

        return
$keys;
    }

   
/**
     * Clone hook implementation
     *
     * Clone the _matching eager loader as well.
     *
     * @return void
     */
   
public function __clone()
    {
        if (
$this->_matching) {
           
$this->_matching = clone $this->_matching;
        }
    }
}