Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/cakephp/cakephp/src/ORM/Association/HasMany.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\Association;

use
Cake\Collection\Collection;
use
Cake\Database\Expression\FieldInterface;
use
Cake\Database\Expression\QueryExpression;
use
Cake\Datasource\EntityInterface;
use
Cake\Datasource\QueryInterface;
use
Cake\ORM\Association;
use
Cake\ORM\Association\DependentDeleteHelper;
use
Cake\ORM\Association\Loader\SelectLoader;
use
Cake\ORM\Table;
use
InvalidArgumentException;
use
Traversable;

/**
 * Represents an N - 1 relationship where the target side of the relationship
 * will have one or multiple records per each one in the source side.
 *
 * An example of a HasMany association would be Author has many Articles.
 */
class HasMany extends Association
{
   
/**
     * Order in which target records should be returned
     *
     * @var mixed
     */
   
protected $_sort;

   
/**
     * The type of join to be used when adding the association to a query
     *
     * @var string
     */
   
protected $_joinType = QueryInterface::JOIN_TYPE_INNER;

   
/**
     * The strategy name to be used to fetch associated records.
     *
     * @var string
     */
   
protected $_strategy = self::STRATEGY_SELECT;

   
/**
     * Valid strategies for this type of association
     *
     * @var string[]
     */
   
protected $_validStrategies = [
       
self::STRATEGY_SELECT,
       
self::STRATEGY_SUBQUERY,
    ];

   
/**
     * Saving strategy that will only append to the links set
     *
     * @var string
     */
   
const SAVE_APPEND = 'append';

   
/**
     * Saving strategy that will replace the links with the provided set
     *
     * @var string
     */
   
const SAVE_REPLACE = 'replace';

   
/**
     * Saving strategy to be used by this association
     *
     * @var string
     */
   
protected $_saveStrategy = self::SAVE_APPEND;

   
/**
     * Returns whether or not the passed table is the owning side for this
     * association. This means that rows in the 'target' table would miss important
     * or required information if the row in 'source' did not exist.
     *
     * @param \Cake\ORM\Table $side The potential Table with ownership
     * @return bool
     */
   
public function isOwningSide(Table $side)
    {
        return
$side === $this->getSource();
    }

   
/**
     * Sets the strategy that should be used for saving.
     *
     * @param string $strategy the strategy name to be used
     * @throws \InvalidArgumentException if an invalid strategy name is passed
     * @return $this
     */
   
public function setSaveStrategy($strategy)
    {
        if (!
in_array($strategy, [self::SAVE_APPEND, self::SAVE_REPLACE], true)) {
           
$msg = sprintf('Invalid save strategy "%s"', $strategy);
            throw new
InvalidArgumentException($msg);
        }

       
$this->_saveStrategy = $strategy;

        return
$this;
    }

   
/**
     * Gets the strategy that should be used for saving.
     *
     * @return string the strategy to be used for saving
     */
   
public function getSaveStrategy()
    {
        return
$this->_saveStrategy;
    }

   
/**
     * Sets the strategy that should be used for saving. If called with no
     * arguments, it will return the currently configured strategy
     *
     * @deprecated 3.4.0 Use setSaveStrategy()/getSaveStrategy() instead.
     * @param string|null $strategy the strategy name to be used
     * @throws \InvalidArgumentException if an invalid strategy name is passed
     * @return string the strategy to be used for saving
     */
   
public function saveStrategy($strategy = null)
    {
       
deprecationWarning(
           
'HasMany::saveStrategy() is deprecated. ' .
           
'Use setSaveStrategy()/getSaveStrategy() instead.'
       
);
        if (
$strategy !== null) {
           
$this->setSaveStrategy($strategy);
        }

        return
$this->getSaveStrategy();
    }

   
/**
     * Takes an entity from the source table and looks if there is a field
     * matching the property name for this association. The found entity will be
     * saved on the target table for this association by passing supplied
     * `$options`
     *
     * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
     * @param array $options options to be passed to the save method in the target table
     * @return \Cake\Datasource\EntityInterface|false False if $entity could not be saved, otherwise it returns
     * the saved entity
     * @see \Cake\ORM\Table::save()
     * @throws \InvalidArgumentException when the association data cannot be traversed.
     */
   
public function saveAssociated(EntityInterface $entity, array $options = [])
    {
       
$targetEntities = $entity->get($this->getProperty());

       
$isEmpty = in_array($targetEntities, [null, [], '', false], true);
        if (
$isEmpty) {
            if (
               
$entity->isNew() ||
               
$this->getSaveStrategy() !== self::SAVE_REPLACE
           
) {
                return
$entity;
            }

           
$targetEntities = [];
        }

        if (
            !
is_array($targetEntities) &&
            !(
$targetEntities instanceof Traversable)
        ) {
           
$name = $this->getProperty();
           
$message = sprintf('Could not save %s, it cannot be traversed', $name);
            throw new
InvalidArgumentException($message);
        }

       
$foreignKeyReference = array_combine(
            (array)
$this->getForeignKey(),
           
$entity->extract((array)$this->getBindingKey())
        );

       
$options['_sourceTable'] = $this->getSource();

        if (
           
$this->_saveStrategy === self::SAVE_REPLACE &&
            !
$this->_unlinkAssociated($foreignKeyReference, $entity, $this->getTarget(), $targetEntities, $options)
        ) {
            return
false;
        }

        if (!
$this->_saveTarget($foreignKeyReference, $entity, $targetEntities, $options)) {
            return
false;
        }

        return
$entity;
    }

   
/**
     * Persists each of the entities into the target table and creates links between
     * the parent entity and each one of the saved target entities.
     *
     * @param array $foreignKeyReference The foreign key reference defining the link between the
     * target entity, and the parent entity.
     * @param \Cake\Datasource\EntityInterface $parentEntity The source entity containing the target
     * entities to be saved.
     * @param array|\Traversable $entities list of entities to persist in target table and to
     * link to the parent entity
     * @param array $options list of options accepted by `Table::save()`.
     * @return bool `true` on success, `false` otherwise.
     */
   
protected function _saveTarget(array $foreignKeyReference, EntityInterface $parentEntity, $entities, array $options)
    {
       
$foreignKey = array_keys($foreignKeyReference);
       
$table = $this->getTarget();
       
$original = $entities;

        foreach (
$entities as $k => $entity) {
            if (!(
$entity instanceof EntityInterface)) {
                break;
            }

            if (!empty(
$options['atomic'])) {
               
$entity = clone $entity;
            }

            if (
$foreignKeyReference !== $entity->extract($foreignKey)) {
               
$entity->set($foreignKeyReference, ['guard' => false]);
            }

            if (
$table->save($entity, $options)) {
               
$entities[$k] = $entity;
                continue;
            }

            if (!empty(
$options['atomic'])) {
               
$original[$k]->setErrors($entity->getErrors());
               
$entity->set($this->getProperty(), $original);

                return
false;
            }
        }

       
$parentEntity->set($this->getProperty(), $entities);

        return
true;
    }

   
/**
     * Associates the source entity to each of the target entities provided.
     * When using this method, all entities in `$targetEntities` will be appended to
     * the source entity's property corresponding to this association object.
     *
     * This method does not check link uniqueness.
     * Changes are persisted in the database and also in the source entity.
     *
     * ### Example:
     *
     * ```
     * $user = $users->get(1);
     * $allArticles = $articles->find('all')->toArray();
     * $users->Articles->link($user, $allArticles);
     * ```
     *
     * `$user->get('articles')` will contain all articles in `$allArticles` after linking
     *
     * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side
     * of this association
     * @param array $targetEntities list of entities belonging to the `target` side
     * of this association
     * @param array $options list of options to be passed to the internal `save` call
     * @return bool true on success, false otherwise
     */
   
public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
    {
       
$saveStrategy = $this->getSaveStrategy();
       
$this->setSaveStrategy(self::SAVE_APPEND);
       
$property = $this->getProperty();

       
$currentEntities = array_unique(
           
array_merge(
                (array)
$sourceEntity->get($property),
               
$targetEntities
           
)
        );

       
$sourceEntity->set($property, $currentEntities);

       
$savedEntity = $this->getConnection()->transactional(function () use ($sourceEntity, $options) {
            return
$this->saveAssociated($sourceEntity, $options);
        });

       
$ok = ($savedEntity instanceof EntityInterface);

       
$this->setSaveStrategy($saveStrategy);

        if (
$ok) {
           
$sourceEntity->set($property, $savedEntity->get($property));
           
$sourceEntity->setDirty($property, false);
        }

        return
$ok;
    }

   
/**
     * Removes all links between the passed source entity and each of the provided
     * target entities. This method assumes that all passed objects are already persisted
     * in the database and that each of them contain a primary key value.
     *
     * ### Options
     *
     * Additionally to the default options accepted by `Table::delete()`, the following
     * keys are supported:
     *
     * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that
     * are stored in `$sourceEntity` (default: true)
     *
     * By default this method will unset each of the entity objects stored inside the
     * source entity.
     *
     * Changes are persisted in the database and also in the source entity.
     *
     * ### Example:
     *
     * ```
     * $user = $users->get(1);
     * $user->articles = [$article1, $article2, $article3, $article4];
     * $users->save($user, ['Associated' => ['Articles']]);
     * $allArticles = [$article1, $article2, $article3];
     * $users->Articles->unlink($user, $allArticles);
     * ```
     *
     * `$article->get('articles')` will contain only `[$article4]` after deleting in the database
     *
     * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
     * this association
     * @param array $targetEntities list of entities persisted in the target table for
     * this association
     * @param array $options list of options to be passed to the internal `delete` call
     * @throws \InvalidArgumentException if non persisted entities are passed or if
     * any of them is lacking a primary key value
     * @return void
     */
   
public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = [])
    {
        if (
is_bool($options)) {
           
$options = [
               
'cleanProperty' => $options,
            ];
        } else {
           
$options += ['cleanProperty' => true];
        }
        if (
count($targetEntities) === 0) {
            return;
        }

       
$foreignKey = (array)$this->getForeignKey();
       
$target = $this->getTarget();
       
$targetPrimaryKey = array_merge((array)$target->getPrimaryKey(), $foreignKey);
       
$property = $this->getProperty();

       
$conditions = [
           
'OR' => (new Collection($targetEntities))
                ->
map(function ($entity) use ($targetPrimaryKey) {
                   
/** @var \Cake\Datasource\EntityInterface $entity */
                   
return $entity->extract($targetPrimaryKey);
                })
                ->
toList(),
        ];

       
$this->_unlink($foreignKey, $target, $conditions, $options);

       
$result = $sourceEntity->get($property);
        if (
$options['cleanProperty'] && $result !== null) {
           
$sourceEntity->set(
               
$property,
                (new
Collection($sourceEntity->get($property)))
                ->
reject(
                    function (
$assoc) use ($targetEntities) {
                        return
in_array($assoc, $targetEntities);
                    }
                )
                ->
toList()
            );
        }

       
$sourceEntity->setDirty($property, false);
    }

   
/**
     * Replaces existing association links between the source entity and the target
     * with the ones passed. This method does a smart cleanup, links that are already
     * persisted and present in `$targetEntities` will not be deleted, new links will
     * be created for the passed target entities that are not already in the database
     * and the rest will be removed.
     *
     * For example, if an author has many articles, such as 'article1','article 2' and 'article 3' and you pass
     * to this method an array containing the entities for articles 'article 1' and 'article 4',
     * only the link for 'article 1' will be kept in database, the links for 'article 2' and 'article 3' will be
     * deleted and the link for 'article 4' will be created.
     *
     * Existing links are not deleted and created again, they are either left untouched
     * or updated.
     *
     * This method does not check link uniqueness.
     *
     * On success, the passed `$sourceEntity` will contain `$targetEntities` as value
     * in the corresponding property for this association.
     *
     * Additional options for new links to be saved can be passed in the third argument,
     * check `Table::save()` for information on the accepted options.
     *
     * ### Example:
     *
     * ```
     * $author->articles = [$article1, $article2, $article3, $article4];
     * $authors->save($author);
     * $articles = [$article1, $article3];
     * $authors->getAssociation('articles')->replace($author, $articles);
     * ```
     *
     * `$author->get('articles')` will contain only `[$article1, $article3]` at the end
     *
     * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for
     * this association
     * @param array $targetEntities list of entities from the target table to be linked
     * @param array $options list of options to be passed to the internal `save`/`delete` calls
     * when persisting/updating new links, or deleting existing ones
     * @throws \InvalidArgumentException if non persisted entities are passed or if
     * any of them is lacking a primary key value
     * @return bool success
     */
   
public function replace(EntityInterface $sourceEntity, array $targetEntities, array $options = [])
    {
       
$property = $this->getProperty();
       
$sourceEntity->set($property, $targetEntities);
       
$saveStrategy = $this->getSaveStrategy();
       
$this->setSaveStrategy(self::SAVE_REPLACE);
       
$result = $this->saveAssociated($sourceEntity, $options);
       
$ok = ($result instanceof EntityInterface);

        if (
$ok) {
           
$sourceEntity = $result;
        }
       
$this->setSaveStrategy($saveStrategy);

        return
$ok;
    }

   
/**
     * Deletes/sets null the related objects according to the dependency between source and targets and foreign key nullability
     * Skips deleting records present in $remainingEntities
     *
     * @param array $foreignKeyReference The foreign key reference defining the link between the
     * target entity, and the parent entity.
     * @param \Cake\Datasource\EntityInterface $entity the entity which should have its associated entities unassigned
     * @param \Cake\ORM\Table $target The associated table
     * @param array $remainingEntities Entities that should not be deleted
     * @param array $options list of options accepted by `Table::delete()`
     * @return bool success
     */
   
protected function _unlinkAssociated(array $foreignKeyReference, EntityInterface $entity, Table $target, array $remainingEntities = [], array $options = [])
    {
       
$primaryKey = (array)$target->getPrimaryKey();
       
$exclusions = new Collection($remainingEntities);
       
$exclusions = $exclusions->map(
            function (
$ent) use ($primaryKey) {
               
/** @var \Cake\Datasource\EntityInterface $ent */
               
return $ent->extract($primaryKey);
            }
        )
        ->
filter(
            function (
$v) {
                return !
in_array(null, $v, true);
            }
        )
        ->
toList();

       
$conditions = $foreignKeyReference;

        if (
count($exclusions) > 0) {
           
$conditions = [
               
'NOT' => [
                   
'OR' => $exclusions,
                ],
               
$foreignKeyReference,
            ];
        }

        return
$this->_unlink(array_keys($foreignKeyReference), $target, $conditions, $options);
    }

   
/**
     * Deletes/sets null the related objects matching $conditions.
     * The action which is taken depends on the dependency between source and targets and also on foreign key nullability
     *
     * @param array $foreignKey array of foreign key properties
     * @param \Cake\ORM\Table $target The associated table
     * @param array $conditions The conditions that specifies what are the objects to be unlinked
     * @param array $options list of options accepted by `Table::delete()`
     * @return bool success
     */
   
protected function _unlink(array $foreignKey, Table $target, array $conditions = [], array $options = [])
    {
       
$mustBeDependent = (!$this->_foreignKeyAcceptsNull($target, $foreignKey) || $this->getDependent());

        if (
$mustBeDependent) {
            if (
$this->_cascadeCallbacks) {
               
$conditions = new QueryExpression($conditions);
               
$conditions->traverse(function ($entry) use ($target) {
                    if (
$entry instanceof FieldInterface) {
                       
$entry->setField($target->aliasField($entry->getField()));
                    }
                });
               
$query = $this->find('all')->where($conditions);
               
$ok = true;
                foreach (
$query as $assoc) {
                   
$ok = $ok && $target->delete($assoc, $options);
                }

                return
$ok;
            }

           
$conditions = array_merge($conditions, $this->getConditions());
           
$target->deleteAll($conditions);

            return
true;
        }

       
$updateFields = array_fill_keys($foreignKey, null);
       
$conditions = array_merge($conditions, $this->getConditions());
       
$target->updateAll($updateFields, $conditions);

        return
true;
    }

   
/**
     * Checks the nullable flag of the foreign key
     *
     * @param \Cake\ORM\Table $table the table containing the foreign key
     * @param array $properties the list of fields that compose the foreign key
     * @return bool
     */
   
protected function _foreignKeyAcceptsNull(Table $table, array $properties)
    {
        return !
in_array(
           
false,
           
array_map(
                function (
$prop) use ($table) {
                    return
$table->getSchema()->isNullable($prop);
                },
               
$properties
           
)
        );
    }

   
/**
     * Get the relationship type.
     *
     * @return string
     */
   
public function type()
    {
        return
self::ONE_TO_MANY;
    }

   
/**
     * Whether this association can be expressed directly in a query join
     *
     * @param array $options custom options key that could alter the return value
     * @return bool if the 'matching' key in $option is true then this function
     * will return true, false otherwise
     */
   
public function canBeJoined(array $options = [])
    {
        return !empty(
$options['matching']);
    }

   
/**
     * Gets the name of the field representing the foreign key to the source table.
     *
     * @return string
     */
   
public function getForeignKey()
    {
        if (
$this->_foreignKey === null) {
           
$this->_foreignKey = $this->_modelKey($this->getSource()->getTable());
        }

        return
$this->_foreignKey;
    }

   
/**
     * Sets the sort order in which target records should be returned.
     *
     * @param mixed $sort A find() compatible order clause
     * @return $this
     */
   
public function setSort($sort)
    {
       
$this->_sort = $sort;

        return
$this;
    }

   
/**
     * Gets the sort order in which target records should be returned.
     *
     * @return mixed
     */
   
public function getSort()
    {
        return
$this->_sort;
    }

   
/**
     * Sets the sort order in which target records should be returned.
     * If no arguments are passed the currently configured value is returned
     *
     * @deprecated 3.4.0 Use setSort()/getSort() instead.
     * @param mixed $sort A find() compatible order clause
     * @return mixed
     */
   
public function sort($sort = null)
    {
       
deprecationWarning(
           
'HasMany::sort() is deprecated. ' .
           
'Use setSort()/getSort() instead.'
       
);
        if (
$sort !== null) {
           
$this->setSort($sort);
        }

        return
$this->getSort();
    }

   
/**
     * {@inheritDoc}
     */
   
public function defaultRowValue($row, $joined)
    {
       
$sourceAlias = $this->getSource()->getAlias();
        if (isset(
$row[$sourceAlias])) {
           
$row[$sourceAlias][$this->getProperty()] = $joined ? null : [];
        }

        return
$row;
    }

   
/**
     * Parse extra options passed in the constructor.
     *
     * @param array $opts original list of options passed in constructor
     * @return void
     */
   
protected function _options(array $opts)
    {
        if (!empty(
$opts['saveStrategy'])) {
           
$this->setSaveStrategy($opts['saveStrategy']);
        }
        if (isset(
$opts['sort'])) {
           
$this->setSort($opts['sort']);
        }
    }

   
/**
     * {@inheritDoc}
     *
     * @return \Closure
     */
   
public function eagerLoader(array $options)
    {
       
$loader = new SelectLoader([
           
'alias' => $this->getAlias(),
           
'sourceAlias' => $this->getSource()->getAlias(),
           
'targetAlias' => $this->getTarget()->getAlias(),
           
'foreignKey' => $this->getForeignKey(),
           
'bindingKey' => $this->getBindingKey(),
           
'strategy' => $this->getStrategy(),
           
'associationType' => $this->type(),
           
'sort' => $this->getSort(),
           
'finder' => [$this, 'find'],
        ]);

        return
$loader->buildEagerLoader($options);
    }

   
/**
     * {@inheritDoc}
     */
   
public function cascadeDelete(EntityInterface $entity, array $options = [])
    {
       
$helper = new DependentDeleteHelper();

        return
$helper->cascadeDelete($this, $entity, $options);
    }
}