Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/cakephp/cakephp/src/ORM/Association/Loader/SelectLoader.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.4.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\ORM\Association\Loader;

use
Cake\Database\Expression\IdentifierExpression;
use
Cake\Database\Expression\TupleComparison;
use
Cake\Database\ValueBinder;
use
Cake\ORM\Association;
use
InvalidArgumentException;
use
RuntimeException;

/**
 * Implements the logic for loading an association using a SELECT query
 *
 * @internal
 */
class SelectLoader
{
   
/**
     * The alias of the association loading the results
     *
     * @var string
     */
   
protected $alias;

   
/**
     * The alias of the source association
     *
     * @var string
     */
   
protected $sourceAlias;

   
/**
     * The alias of the target association
     *
     * @var string
     */
   
protected $targetAlias;

   
/**
     * The foreignKey to the target association
     *
     * @var string|array
     */
   
protected $foreignKey;

   
/**
     * The strategy to use for loading, either select or subquery
     *
     * @var string
     */
   
protected $strategy;

   
/**
     * The binding key for the source association.
     *
     * @var string
     */
   
protected $bindingKey;

   
/**
     * A callable that will return a query object used for loading the association results
     *
     * @var callable
     */
   
protected $finder;

   
/**
     * The type of the association triggering the load
     *
     * @var string
     */
   
protected $associationType;

   
/**
     * The sorting options for loading the association
     *
     * @var string
     */
   
protected $sort;

   
/**
     * Copies the options array to properties in this class. The keys in the array correspond
     * to properties in this class.
     *
     * @param array $options Properties to be copied to this class
     */
   
public function __construct(array $options)
    {
       
$this->alias = $options['alias'];
       
$this->sourceAlias = $options['sourceAlias'];
       
$this->targetAlias = $options['targetAlias'];
       
$this->foreignKey = $options['foreignKey'];
       
$this->strategy = $options['strategy'];
       
$this->bindingKey = $options['bindingKey'];
       
$this->finder = $options['finder'];
       
$this->associationType = $options['associationType'];
       
$this->sort = isset($options['sort']) ? $options['sort'] : null;
    }

   
/**
     * Returns a callable that can be used for injecting association results into a given
     * iterator. The options accepted by this method are the same as `Association::eagerLoader()`
     *
     * @param array $options Same options as `Association::eagerLoader()`
     * @return \Closure
     */
   
public function buildEagerLoader(array $options)
    {
       
$options += $this->_defaultOptions();
       
$fetchQuery = $this->_buildQuery($options);
       
$resultMap = $this->_buildResultMap($fetchQuery, $options);

        return
$this->_resultInjector($fetchQuery, $resultMap, $options);
    }

   
/**
     * Returns the default options to use for the eagerLoader
     *
     * @return array
     */
   
protected function _defaultOptions()
    {
        return [
           
'foreignKey' => $this->foreignKey,
           
'conditions' => [],
           
'strategy' => $this->strategy,
           
'nestKey' => $this->alias,
           
'sort' => $this->sort,
        ];
    }

   
/**
     * Auxiliary function to construct a new Query object to return all the records
     * in the target table that are associated to those specified in $options from
     * the source table
     *
     * @param array $options options accepted by eagerLoader()
     * @return \Cake\ORM\Query
     * @throws \InvalidArgumentException When a key is required for associations but not selected.
     */
   
protected function _buildQuery($options)
    {
       
$key = $this->_linkField($options);
       
$filter = $options['keys'];
       
$useSubquery = $options['strategy'] === Association::STRATEGY_SUBQUERY;
       
$finder = $this->finder;

        if (!isset(
$options['fields'])) {
           
$options['fields'] = [];
        }

       
/** @var \Cake\ORM\Query $query */
       
$query = $finder();
        if (isset(
$options['finder'])) {
            list(
$finderName, $opts) = $this->_extractFinder($options['finder']);
           
$query = $query->find($finderName, $opts);
        }

       
$fetchQuery = $query
           
->select($options['fields'])
            ->
where($options['conditions'])
            ->
eagerLoaded(true)
            ->
enableHydration($options['query']->isHydrationEnabled());

        if (
$useSubquery) {
           
$filter = $this->_buildSubquery($options['query']);
           
$fetchQuery = $this->_addFilteringJoin($fetchQuery, $key, $filter);
        } else {
           
$fetchQuery = $this->_addFilteringCondition($fetchQuery, $key, $filter);
        }

        if (!empty(
$options['sort'])) {
           
$fetchQuery->order($options['sort']);
        }

        if (!empty(
$options['contain'])) {
           
$fetchQuery->contain($options['contain']);
        }

        if (!empty(
$options['queryBuilder'])) {
           
$fetchQuery = $options['queryBuilder']($fetchQuery);
        }

       
$this->_assertFieldsPresent($fetchQuery, (array)$key);

        return
$fetchQuery;
    }

   
/**
     * Helper method to infer the requested finder and its options.
     *
     * Returns the inferred options from the finder $type.
     *
     * ### Examples:
     *
     * The following will call the finder 'translations' with the value of the finder as its options:
     * $query->contain(['Comments' => ['finder' => ['translations']]]);
     * $query->contain(['Comments' => ['finder' => ['translations' => []]]]);
     * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]);
     *
     * @param string|array $finderData The finder name or an array having the name as key
     * and options as value.
     * @return array
     */
   
protected function _extractFinder($finderData)
    {
       
$finderData = (array)$finderData;

        if (
is_numeric(key($finderData))) {
            return [
current($finderData), []];
        }

        return [
key($finderData), current($finderData)];
    }

   
/**
     * Checks that the fetching query either has auto fields on or
     * has the foreignKey fields selected.
     * If the required fields are missing, throws an exception.
     *
     * @param \Cake\ORM\Query $fetchQuery The association fetching query
     * @param array $key The foreign key fields to check
     * @return void
     * @throws \InvalidArgumentException
     */
   
protected function _assertFieldsPresent($fetchQuery, $key)
    {
       
$select = $fetchQuery->aliasFields($fetchQuery->clause('select'));
        if (empty(
$select)) {
            return;
        }
       
$missingKey = function ($fieldList, $key) {
            foreach (
$key as $keyField) {
                if (!
in_array($keyField, $fieldList, true)) {
                    return
true;
                }
            }

            return
false;
        };

       
$missingFields = $missingKey($select, $key);
        if (
$missingFields) {
           
$driver = $fetchQuery->getConnection()->getDriver();
           
$quoted = array_map([$driver, 'quoteIdentifier'], $key);
           
$missingFields = $missingKey($select, $quoted);
        }

        if (
$missingFields) {
            throw new
InvalidArgumentException(
               
sprintf(
                   
'You are required to select the "%s" field(s)',
                   
implode(', ', (array)$key)
                )
            );
        }
    }

   
/**
     * Appends any conditions required to load the relevant set of records in the
     * target table query given a filter key and some filtering values when the
     * filtering needs to be done using a subquery.
     *
     * @param \Cake\ORM\Query $query Target table's query
     * @param string|string[] $key the fields that should be used for filtering
     * @param \Cake\ORM\Query $subquery The Subquery to use for filtering
     * @return \Cake\ORM\Query
     */
   
protected function _addFilteringJoin($query, $key, $subquery)
    {
       
$filter = [];
       
$aliasedTable = $this->sourceAlias;

        foreach (
$subquery->clause('select') as $aliasedField => $field) {
            if (
is_int($aliasedField)) {
               
$filter[] = new IdentifierExpression($field);
            } else {
               
$filter[$aliasedField] = $field;
            }
        }
       
$subquery->select($filter, true);

       
$conditions = null;
        if (
is_array($key)) {
           
$conditions = $this->_createTupleCondition($query, $key, $filter, '=');
        } else {
           
$filter = current($filter);
        }

       
$conditions = $conditions ?: $query->newExpr([$key => $filter]);

        return
$query->innerJoin(
            [
$aliasedTable => $subquery],
           
$conditions
       
);
    }

   
/**
     * Appends any conditions required to load the relevant set of records in the
     * target table query given a filter key and some filtering values.
     *
     * @param \Cake\ORM\Query $query Target table's query
     * @param string|array $key The fields that should be used for filtering
     * @param mixed $filter The value that should be used to match for $key
     * @return \Cake\ORM\Query
     */
   
protected function _addFilteringCondition($query, $key, $filter)
    {
        if (
is_array($key)) {
           
$conditions = $this->_createTupleCondition($query, $key, $filter, 'IN');
        }

       
$conditions = isset($conditions) ? $conditions : [$key . ' IN' => $filter];

        return
$query->andWhere($conditions);
    }

   
/**
     * Returns a TupleComparison object that can be used for matching all the fields
     * from $keys with the tuple values in $filter using the provided operator.
     *
     * @param \Cake\ORM\Query $query Target table's query
     * @param string[] $keys the fields that should be used for filtering
     * @param mixed $filter the value that should be used to match for $key
     * @param string $operator The operator for comparing the tuples
     * @return \Cake\Database\Expression\TupleComparison
     */
   
protected function _createTupleCondition($query, $keys, $filter, $operator)
    {
       
$types = [];
       
$defaults = $query->getDefaultTypes();
        foreach (
$keys as $k) {
            if (isset(
$defaults[$k])) {
               
$types[] = $defaults[$k];
            }
        }

        return new
TupleComparison($keys, $filter, $types, $operator);
    }

   
/**
     * Generates a string used as a table field that contains the values upon
     * which the filter should be applied
     *
     * @param array $options The options for getting the link field.
     * @return string|string[]
     * @throws \RuntimeException
     */
   
protected function _linkField($options)
    {
       
$links = [];
       
$name = $this->alias;

        if (
$options['foreignKey'] === false && $this->associationType === Association::ONE_TO_MANY) {
           
$msg = 'Cannot have foreignKey = false for hasMany associations. ' .
                   
'You must provide a foreignKey column.';
            throw new
RuntimeException($msg);
        }

       
$keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY]) ?
           
$this->foreignKey :
           
$this->bindingKey;

        foreach ((array)
$keys as $key) {
           
$links[] = sprintf('%s.%s', $name, $key);
        }

        if (
count($links) === 1) {
            return
$links[0];
        }

        return
$links;
    }

   
/**
     * Builds a query to be used as a condition for filtering records in the
     * target table, it is constructed by cloning the original query that was used
     * to load records in the source table.
     *
     * @param \Cake\ORM\Query $query the original query used to load source records
     * @return \Cake\ORM\Query
     */
   
protected function _buildSubquery($query)
    {
       
$filterQuery = clone $query;
       
$filterQuery->disableAutoFields();
       
$filterQuery->mapReduce(null, null, true);
       
$filterQuery->formatResults(null, true);
       
$filterQuery->contain([], true);
       
$filterQuery->setValueBinder(new ValueBinder());

        if (!
$filterQuery->clause('limit')) {
           
$filterQuery->limit(null);
           
$filterQuery->order([], true);
           
$filterQuery->offset(null);
        }

       
$fields = $this->_subqueryFields($query);
       
$filterQuery->select($fields['select'], true)->group($fields['group']);

        return
$filterQuery;
    }

   
/**
     * Calculate the fields that need to participate in a subquery.
     *
     * Normally this includes the binding key columns. If there is a an ORDER BY,
     * those columns are also included as the fields may be calculated or constant values,
     * that need to be present to ensure the correct association data is loaded.
     *
     * @param \Cake\ORM\Query $query The query to get fields from.
     * @return array The list of fields for the subquery.
     */
   
protected function _subqueryFields($query)
    {
       
$keys = (array)$this->bindingKey;

        if (
$this->associationType === Association::MANY_TO_ONE) {
           
$keys = (array)$this->foreignKey;
        }

       
$fields = $query->aliasFields($keys, $this->sourceAlias);
       
$group = $fields = array_values($fields);

       
$order = $query->clause('order');
        if (
$order) {
           
$columns = $query->clause('select');
           
$order->iterateParts(function ($direction, $field) use (&$fields, $columns) {
                if (isset(
$columns[$field])) {
                   
$fields[$field] = $columns[$field];
                }
            });
        }

        return [
'select' => $fields, 'group' => $group];
    }

   
/**
     * Builds an array containing the results from fetchQuery indexed by
     * the foreignKey value corresponding to this association.
     *
     * @param \Cake\ORM\Query $fetchQuery The query to get results from
     * @param array $options The options passed to the eager loader
     * @return array
     */
   
protected function _buildResultMap($fetchQuery, $options)
    {
       
$resultMap = [];
       
$singleResult = in_array($this->associationType, [Association::MANY_TO_ONE, Association::ONE_TO_ONE], true);
       
$keys = in_array($this->associationType, [Association::ONE_TO_ONE, Association::ONE_TO_MANY], true) ?
           
$this->foreignKey :
           
$this->bindingKey;
       
$key = (array)$keys;

        foreach (
$fetchQuery->all() as $result) {
           
$values = [];
            foreach (
$key as $k) {
               
$values[] = $result[$k];
            }
            if (
$singleResult) {
               
$resultMap[implode(';', $values)] = $result;
            } else {
               
$resultMap[implode(';', $values)][] = $result;
            }
        }

        return
$resultMap;
    }

   
/**
     * Returns a callable to be used for each row in a query result set
     * for injecting the eager loaded rows
     *
     * @param \Cake\ORM\Query $fetchQuery the Query used to fetch results
     * @param array $resultMap an array with the foreignKey as keys and
     * the corresponding target table results as value.
     * @param array $options The options passed to the eagerLoader method
     * @return \Closure
     */
   
protected function _resultInjector($fetchQuery, $resultMap, $options)
    {
       
$keys = $this->associationType === Association::MANY_TO_ONE ?
           
$this->foreignKey :
           
$this->bindingKey;

       
$sourceKeys = [];
        foreach ((array)
$keys as $key) {
           
$f = $fetchQuery->aliasField($key, $this->sourceAlias);
           
$sourceKeys[] = key($f);
        }

       
$nestKey = $options['nestKey'];
        if (
count($sourceKeys) > 1) {
            return
$this->_multiKeysInjector($resultMap, $sourceKeys, $nestKey);
        }

       
$sourceKey = $sourceKeys[0];

        return function (
$row) use ($resultMap, $sourceKey, $nestKey) {
            if (isset(
$row[$sourceKey], $resultMap[$row[$sourceKey]])) {
               
$row[$nestKey] = $resultMap[$row[$sourceKey]];
            }

            return
$row;
        };
    }

   
/**
     * Returns a callable to be used for each row in a query result set
     * for injecting the eager loaded rows when the matching needs to
     * be done with multiple foreign keys
     *
     * @param array $resultMap A keyed arrays containing the target table
     * @param string[] $sourceKeys An array with aliased keys to match
     * @param string $nestKey The key under which results should be nested
     * @return \Closure
     */
   
protected function _multiKeysInjector($resultMap, $sourceKeys, $nestKey)
    {
        return function (
$row) use ($resultMap, $sourceKeys, $nestKey) {
           
$values = [];
            foreach (
$sourceKeys as $key) {
               
$values[] = $row[$key];
            }

           
$key = implode(';', $values);
            if (isset(
$resultMap[$key])) {
               
$row[$nestKey] = $resultMap[$key];
            }

            return
$row;
        };
    }
}