/var/www/vhosts/ihelp.ro/httpdocs/vendor/cakephp/cakephp/src/ORM/Association
NameSizeModeActions
Loader/-0755rm
BelongsTo.php66540644editdlrm
BelongsToMany.php538850644editdlrm
DependentDeleteHelper.php23650644editdlrm
HasMany.php232510644editdlrm
HasOne.php47110644editdlrm
Edit: /var/www/vhosts/ihelp.ro/httpdocs/vendor/cakephp/cakephp/src/ORM/Association/BelongsToMany.php (53885B)
|string|null */ protected $_targetForeignKey; /** * The table instance for the junction relation. * * @var \Cake\ORM\Table|string */ protected $_through; /** * Valid strategies for this type of association * * @var array */ protected $_validStrategies = [ self::STRATEGY_SELECT, self::STRATEGY_SUBQUERY, ]; /** * Whether the records on the joint table should be removed when a record * on the source table is deleted. * * Defaults to true for backwards compatibility. * * @var bool */ protected $_dependent = true; /** * Filtered conditions that reference the target table. * * @var array|null */ protected $_targetConditions; /** * Filtered conditions that reference the junction table. * * @var array|null */ protected $_junctionConditions; /** * Order in which target records should be returned * * @var mixed */ protected $_sort; /** * Sets the name of the field representing the foreign key to the target table. * * @param array|string $key the key to be used to link both tables together * @return $this */ public function setTargetForeignKey($key) { $this->_targetForeignKey = $key; return $this; } /** * Gets the name of the field representing the foreign key to the target table. * * @return array|string */ public function getTargetForeignKey() { if ($this->_targetForeignKey === null) { $this->_targetForeignKey = $this->_modelKey($this->getTarget()->getAlias()); } return $this->_targetForeignKey; } /** * 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 = []): bool { return !empty($options['matching']); } /** * Gets the name of the field representing the foreign key to the source table. * * @return array|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; } /** * @inheritDoc */ public function defaultRowValue(array $row, bool $joined): array { $sourceAlias = $this->getSource()->getAlias(); if (isset($row[$sourceAlias])) { $row[$sourceAlias][$this->getProperty()] = $joined ? null : []; } return $row; } /** * Sets the table instance for the junction relation. If no arguments * are passed, the current configured table instance is returned * * @param \Cake\ORM\Table|string|null $table Name or instance for the join table * @return \Cake\ORM\Table * @throws \InvalidArgumentException If the expected associations are incompatible with existing associations. */ public function junction($table = null): Table { if ($table === null && $this->_junctionTable !== null) { return $this->_junctionTable; } $tableLocator = $this->getTableLocator(); if ($table === null && $this->_through) { $table = $this->_through; } elseif ($table === null) { $tableName = $this->_junctionTableName(); $tableAlias = Inflector::camelize($tableName); $config = []; if (!$tableLocator->exists($tableAlias)) { $config = ['table' => $tableName, 'allowFallbackClass' => true]; // Propagate the connection if we'll get an auto-model if (!App::className($tableAlias, 'Model/Table', 'Table')) { $config['connection'] = $this->getSource()->getConnection(); } } $table = $tableLocator->get($tableAlias, $config); } if (is_string($table)) { $table = $tableLocator->get($table); } $source = $this->getSource(); $target = $this->getTarget(); if ($source->getAlias() === $target->getAlias()) { throw new InvalidArgumentException(sprintf( 'The `%s` association on `%s` cannot target the same table.', $this->getName(), $source->getAlias() )); } $this->_generateSourceAssociations($table, $source); $this->_generateTargetAssociations($table, $source, $target); $this->_generateJunctionAssociations($table, $source, $target); return $this->_junctionTable = $table; } /** * Generate reciprocal associations as necessary. * * Generates the following associations: * * - target hasMany junction e.g. Articles hasMany ArticlesTags * - target belongsToMany source e.g Articles belongsToMany Tags. * * You can override these generated associations by defining associations * with the correct aliases. * * @param \Cake\ORM\Table $junction The junction table. * @param \Cake\ORM\Table $source The source table. * @param \Cake\ORM\Table $target The target table. * @return void */ protected function _generateTargetAssociations(Table $junction, Table $source, Table $target): void { $junctionAlias = $junction->getAlias(); $sAlias = $source->getAlias(); $tAlias = $target->getAlias(); $targetBindingKey = null; if ($junction->hasAssociation($tAlias)) { $targetBindingKey = $junction->getAssociation($tAlias)->getBindingKey(); } if (!$target->hasAssociation($junctionAlias)) { $target->hasMany($junctionAlias, [ 'targetTable' => $junction, 'bindingKey' => $targetBindingKey, 'foreignKey' => $this->getTargetForeignKey(), 'strategy' => $this->_strategy, ]); } if (!$target->hasAssociation($sAlias)) { $target->belongsToMany($sAlias, [ 'sourceTable' => $target, 'targetTable' => $source, 'foreignKey' => $this->getTargetForeignKey(), 'targetForeignKey' => $this->getForeignKey(), 'through' => $junction, 'conditions' => $this->getConditions(), 'strategy' => $this->_strategy, ]); } } /** * Generate additional source table associations as necessary. * * Generates the following associations: * * - source hasMany junction e.g. Tags hasMany ArticlesTags * * You can override these generated associations by defining associations * with the correct aliases. * * @param \Cake\ORM\Table $junction The junction table. * @param \Cake\ORM\Table $source The source table. * @return void */ protected function _generateSourceAssociations(Table $junction, Table $source): void { $junctionAlias = $junction->getAlias(); $sAlias = $source->getAlias(); $sourceBindingKey = null; if ($junction->hasAssociation($sAlias)) { $sourceBindingKey = $junction->getAssociation($sAlias)->getBindingKey(); } if (!$source->hasAssociation($junctionAlias)) { $source->hasMany($junctionAlias, [ 'targetTable' => $junction, 'bindingKey' => $sourceBindingKey, 'foreignKey' => $this->getForeignKey(), 'strategy' => $this->_strategy, ]); } } /** * Generate associations on the junction table as necessary * * Generates the following associations: * * - junction belongsTo source e.g. ArticlesTags belongsTo Tags * - junction belongsTo target e.g. ArticlesTags belongsTo Articles * * You can override these generated associations by defining associations * with the correct aliases. * * @param \Cake\ORM\Table $junction The junction table. * @param \Cake\ORM\Table $source The source table. * @param \Cake\ORM\Table $target The target table. * @return void * @throws \InvalidArgumentException If the expected associations are incompatible with existing associations. */ protected function _generateJunctionAssociations(Table $junction, Table $source, Table $target): void { $tAlias = $target->getAlias(); $sAlias = $source->getAlias(); if (!$junction->hasAssociation($tAlias)) { $junction->belongsTo($tAlias, [ 'foreignKey' => $this->getTargetForeignKey(), 'targetTable' => $target, ]); } else { $belongsTo = $junction->getAssociation($tAlias); if ( $this->getTargetForeignKey() !== $belongsTo->getForeignKey() || $target !== $belongsTo->getTarget() ) { throw new InvalidArgumentException( "The existing `{$tAlias}` association on `{$junction->getAlias()}` " . "is incompatible with the `{$this->getName()}` association on `{$source->getAlias()}`" ); } } if (!$junction->hasAssociation($sAlias)) { $junction->belongsTo($sAlias, [ 'bindingKey' => $this->getBindingKey(), 'foreignKey' => $this->getForeignKey(), 'targetTable' => $source, ]); } } /** * Alters a Query object to include the associated target table data in the final * result * * The options array accept the following keys: * * - includeFields: Whether to include target model fields in the result or not * - foreignKey: The name of the field to use as foreign key, if false none * will be used * - conditions: array with a list of conditions to filter the join with * - fields: a list of fields in the target table to include in the result * - type: The type of join to be used (e.g. INNER) * * @param \Cake\ORM\Query $query the query to be altered to include the target table data * @param array $options Any extra options or overrides to be taken in account * @return void */ public function attachTo(Query $query, array $options = []): void { if (!empty($options['negateMatch'])) { $this->_appendNotMatching($query, $options); return; } $junction = $this->junction(); $belongsTo = $junction->getAssociation($this->getSource()->getAlias()); $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); $cond += $this->junctionConditions(); $includeFields = $options['includeFields'] ?? null; // Attach the junction table as well we need it to populate _joinData. $assoc = $this->_targetTable->getAssociation($junction->getAlias()); $newOptions = array_intersect_key($options, ['joinType' => 1, 'fields' => 1]); $newOptions += [ 'conditions' => $cond, 'includeFields' => $includeFields, 'foreignKey' => false, ]; $assoc->attachTo($query, $newOptions); $query->getEagerLoader()->addToJoinsMap($junction->getAlias(), $assoc, true); parent::attachTo($query, $options); $foreignKey = $this->getTargetForeignKey(); $thisJoin = $query->clause('join')[$this->getName()]; $thisJoin['conditions']->add($assoc->_joinCondition(['foreignKey' => $foreignKey])); } /** * @inheritDoc */ protected function _appendNotMatching(Query $query, array $options): void { if (empty($options['negateMatch'])) { return; } $options['conditions'] = $options['conditions'] ?? []; $junction = $this->junction(); $belongsTo = $junction->getAssociation($this->getSource()->getAlias()); $conds = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); $subquery = $this->find() ->select(array_values($conds)) ->where($options['conditions']); if (!empty($options['queryBuilder'])) { $subquery = $options['queryBuilder']($subquery); } $subquery = $this->_appendJunctionJoin($subquery); $query ->andWhere(function (QueryExpression $exp) use ($subquery, $conds) { $identifiers = []; foreach (array_keys($conds) as $field) { $identifiers[] = new IdentifierExpression($field); } $identifiers = $subquery->newExpr()->add($identifiers)->setConjunction(','); $nullExp = clone $exp; return $exp ->or([ $exp->notIn($identifiers, $subquery), $nullExp->and(array_map([$nullExp, 'isNull'], array_keys($conds))), ]); }); } /** * Get the relationship type. * * @return string */ public function type(): string { return self::MANY_TO_MANY; } /** * Return false as join conditions are defined in the junction table * * @param array $options list of options passed to attachTo method * @return array */ protected function _joinCondition(array $options): array { return []; } /** * @inheritDoc */ public function eagerLoader(array $options): Closure { $name = $this->_junctionAssociationName(); $loader = new SelectWithPivotLoader([ '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(), 'junctionAssociationName' => $name, 'junctionProperty' => $this->_junctionProperty, 'junctionAssoc' => $this->getTarget()->getAssociation($name), 'junctionConditions' => $this->junctionConditions(), 'finder' => function () { return $this->_appendJunctionJoin($this->find(), []); }, ]); return $loader->buildEagerLoader($options); } /** * Clear out the data in the junction table for a given entity. * * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascading delete. * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(EntityInterface $entity, array $options = []): bool { if (!$this->getDependent()) { return true; } $foreignKey = (array)$this->getForeignKey(); $bindingKey = (array)$this->getBindingKey(); $conditions = []; if (!empty($bindingKey)) { $conditions = array_combine($foreignKey, $entity->extract($bindingKey)); } $table = $this->junction(); $hasMany = $this->getSource()->getAssociation($table->getAlias()); if ($this->_cascadeCallbacks) { foreach ($hasMany->find('all')->where($conditions)->all()->toList() as $related) { $success = $table->delete($related, $options); if (!$success) { return false; } } return true; } $assocConditions = $hasMany->getConditions(); if (is_array($assocConditions)) { $conditions = array_merge($conditions, $assocConditions); } else { $conditions[] = $assocConditions; } $table->deleteAll($conditions); return true; } /** * Returns boolean true, as both of the tables 'own' rows in the other side * of the association via the joint table. * * @param \Cake\ORM\Table $side The potential Table with ownership * @return bool */ public function isOwningSide(Table $side): bool { return true; } /** * 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(string $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(): string { return $this->_saveStrategy; } /** * 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` * * When using the 'append' strategy, this function will only create new links * between each side of this association. It will not destroy existing ones even * though they may not be present in the array of entities to be saved. * * When using the 'replace' strategy, existing links will be removed and new links * will be created in the joint table. If there exists links in the database to some * of the entities intended to be saved by this method, they will be updated, * not deleted. * * @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 * @throws \InvalidArgumentException if the property representing the association * in the parent entity cannot be traversed * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns * the saved entity * @see \Cake\ORM\Table::save() * @see \Cake\ORM\Association\BelongsToMany::replaceLinks() */ public function saveAssociated(EntityInterface $entity, array $options = []) { $targetEntity = $entity->get($this->getProperty()); $strategy = $this->getSaveStrategy(); $isEmpty = in_array($targetEntity, [null, [], '', false], true); if ($isEmpty && $entity->isNew()) { return $entity; } if ($isEmpty) { $targetEntity = []; } if ($strategy === self::SAVE_APPEND) { return $this->_saveTarget($entity, $targetEntity, $options); } if ($this->replaceLinks($entity, $targetEntity, $options)) { return $entity; } return false; } /** * 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 \Cake\Datasource\EntityInterface $parentEntity the source entity containing the target * entities to be saved. * @param array $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()` * @throws \InvalidArgumentException if the property representing the association * in the parent entity cannot be traversed * @return \Cake\Datasource\EntityInterface|false The parent entity after all links have been * created if no errors happened, false otherwise */ protected function _saveTarget(EntityInterface $parentEntity, array $entities, $options) { $joinAssociations = false; if (isset($options['associated']) && is_array($options['associated'])) { if (!empty($options['associated'][$this->_junctionProperty]['associated'])) { $joinAssociations = $options['associated'][$this->_junctionProperty]['associated']; } unset($options['associated'][$this->_junctionProperty]); } $table = $this->getTarget(); $original = $entities; $persisted = []; foreach ($entities as $k => $entity) { if (!($entity instanceof EntityInterface)) { break; } if (!empty($options['atomic'])) { $entity = clone $entity; } $saved = $table->save($entity, $options); if ($saved) { $entities[$k] = $entity; $persisted[] = $entity; continue; } // Saving the new linked entity failed, copy errors back into the // original entity if applicable and abort. if (!empty($options['atomic'])) { $original[$k]->setErrors($entity->getErrors()); } if ($saved === false) { return false; } } $options['associated'] = $joinAssociations; $success = $this->_saveLinks($parentEntity, $persisted, $options); if (!$success && !empty($options['atomic'])) { $parentEntity->set($this->getProperty(), $original); return false; } $parentEntity->set($this->getProperty(), $entities); return $parentEntity; } /** * Creates links between the source entity and each of the passed target entities * * @param \Cake\Datasource\EntityInterface $sourceEntity the entity from source table in this * association * @param array<\Cake\Datasource\EntityInterface> $targetEntities list of entities to link to link to the source entity using the * junction table * @param array $options list of options accepted by `Table::save()` * @return bool success */ protected function _saveLinks(EntityInterface $sourceEntity, array $targetEntities, array $options): bool { $target = $this->getTarget(); $junction = $this->junction(); $entityClass = $junction->getEntityClass(); $belongsTo = $junction->getAssociation($target->getAlias()); $foreignKey = (array)$this->getForeignKey(); $assocForeignKey = (array)$belongsTo->getForeignKey(); $targetBindingKey = (array)$belongsTo->getBindingKey(); $bindingKey = (array)$this->getBindingKey(); $jointProperty = $this->_junctionProperty; $junctionRegistryAlias = $junction->getRegistryAlias(); foreach ($targetEntities as $e) { $joint = $e->get($jointProperty); if (!$joint || !($joint instanceof EntityInterface)) { $joint = new $entityClass([], ['markNew' => true, 'source' => $junctionRegistryAlias]); } $sourceKeys = array_combine($foreignKey, $sourceEntity->extract($bindingKey)); $targetKeys = array_combine($assocForeignKey, $e->extract($targetBindingKey)); $changedKeys = ( $sourceKeys !== $joint->extract($foreignKey) || $targetKeys !== $joint->extract($assocForeignKey) ); // Keys were changed, the junction table record _could_ be // new. By clearing the primary key values, and marking the entity // as new, we let save() sort out whether we have a new link // or if we are updating an existing link. if ($changedKeys) { $joint->setNew(true); $joint->unset($junction->getPrimaryKey()) ->set(array_merge($sourceKeys, $targetKeys), ['guard' => false]); } $saved = $junction->save($joint, $options); if (!$saved && !empty($options['atomic'])) { return false; } $e->set($jointProperty, $joint); $e->setDirty($jointProperty, false); } return true; } /** * Associates the source entity to each of the target entities provided by * creating links in the junction table. Both the source entity and each of * the target entities are assumed to be already persisted, if they are marked * as new or their status is unknown then an exception will be thrown. * * 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. * * ### Example: * * ``` * $newTags = $tags->find('relevant')->toArray(); * $articles->getAssociation('tags')->link($article, $newTags); * ``` * * `$article->get('tags')` will contain all tags in `$newTags` after liking * * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side * of this association * @param array<\Cake\Datasource\EntityInterface> $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 * @throws \InvalidArgumentException when any of the values in $targetEntities is * detected to not be already persisted * @return bool true on success, false otherwise */ public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool { $this->_checkPersistenceStatus($sourceEntity, $targetEntities); $property = $this->getProperty(); $links = $sourceEntity->get($property) ?: []; $links = array_merge($links, $targetEntities); $sourceEntity->set($property, $links); return $this->junction()->getConnection()->transactional( function () use ($sourceEntity, $targetEntities, $options) { return $this->_saveLinks($sourceEntity, $targetEntities, $options); } ); } /** * 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 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. * * ### Example: * * ``` * $article->tags = [$tag1, $tag2, $tag3, $tag4]; * $tags = [$tag1, $tag2, $tag3]; * $articles->getAssociation('tags')->unlink($article, $tags); * ``` * * `$article->get('tags')` will contain only `[$tag4]` after deleting in the database * * @param \Cake\Datasource\EntityInterface $sourceEntity An entity persisted in the source table for * this association. * @param array<\Cake\Datasource\EntityInterface> $targetEntities List of entities persisted in the target table for * this association. * @param array|bool $options List of options to be passed to the internal `delete` call, * or a `boolean` as `cleanProperty` key shortcut. * @throws \InvalidArgumentException If non persisted entities are passed or if * any of them is lacking a primary key value. * @return bool Success */ public function unlink(EntityInterface $sourceEntity, array $targetEntities, $options = []): bool { if (is_bool($options)) { $options = [ 'cleanProperty' => $options, ]; } else { $options += ['cleanProperty' => true]; } $this->_checkPersistenceStatus($sourceEntity, $targetEntities); $property = $this->getProperty(); $this->junction()->getConnection()->transactional( function () use ($sourceEntity, $targetEntities, $options): void { $links = $this->_collectJointEntities($sourceEntity, $targetEntities); foreach ($links as $entity) { $this->_junctionTable->delete($entity, $options); } } ); /** @var array<\Cake\Datasource\EntityInterface> $existing */ $existing = $sourceEntity->get($property) ?: []; if (!$options['cleanProperty'] || empty($existing)) { return true; } /** @var \SplObjectStorage<\Cake\Datasource\EntityInterface, null> $storage */ $storage = new SplObjectStorage(); foreach ($targetEntities as $e) { $storage->attach($e); } foreach ($existing as $k => $e) { if ($storage->contains($e)) { unset($existing[$k]); } } $sourceEntity->set($property, array_values($existing)); $sourceEntity->setDirty($property, false); return true; } /** * @inheritDoc */ public function setConditions($conditions) { parent::setConditions($conditions); $this->_targetConditions = $this->_junctionConditions = null; return $this; } /** * Sets the current join table, either the name of the Table instance or the instance itself. * * @param \Cake\ORM\Table|string $through Name of the Table instance or the instance itself * @return $this */ public function setThrough($through) { $this->_through = $through; return $this; } /** * Gets the current join table, either the name of the Table instance or the instance itself. * * @return \Cake\ORM\Table|string */ public function getThrough() { return $this->_through; } /** * Returns filtered conditions that reference the target table. * * Any string expressions, or expression objects will * also be returned in this list. * * @return array|\Closure|null Generally an array. If the conditions * are not an array, the association conditions will be * returned unmodified. */ protected function targetConditions() { if ($this->_targetConditions !== null) { return $this->_targetConditions; } $conditions = $this->getConditions(); if (!is_array($conditions)) { return $conditions; } $matching = []; $alias = $this->getAlias() . '.'; foreach ($conditions as $field => $value) { if (is_string($field) && strpos($field, $alias) === 0) { $matching[$field] = $value; } elseif (is_int($field) || $value instanceof ExpressionInterface) { $matching[$field] = $value; } } return $this->_targetConditions = $matching; } /** * Returns filtered conditions that specifically reference * the junction table. * * @return array */ protected function junctionConditions(): array { if ($this->_junctionConditions !== null) { return $this->_junctionConditions; } $matching = []; $conditions = $this->getConditions(); if (!is_array($conditions)) { return $matching; } $alias = $this->_junctionAssociationName() . '.'; foreach ($conditions as $field => $value) { $isString = is_string($field); if ($isString && strpos($field, $alias) === 0) { $matching[$field] = $value; } // Assume that operators contain junction conditions. // Trying to manage complex conditions could result in incorrect queries. if ($isString && in_array(strtoupper($field), ['OR', 'NOT', 'AND', 'XOR'], true)) { $matching[$field] = $value; } } return $this->_junctionConditions = $matching; } /** * Proxies the finding operation to the target table's find method * and modifies the query accordingly based of this association * configuration. * * If your association includes conditions or a finder, the junction table will be * included in the query's contained associations. * * @param array|string|null $type the type of query to perform, if an array is passed, * it will be interpreted as the `$options` parameter * @param array $options The options to for the find * @see \Cake\ORM\Table::find() * @return \Cake\ORM\Query */ public function find($type = null, array $options = []): Query { $type = $type ?: $this->getFinder(); [$type, $opts] = $this->_extractFinder($type); $query = $this->getTarget() ->find($type, $options + $opts) ->where($this->targetConditions()) ->addDefaultTypes($this->getTarget()); if ($this->junctionConditions()) { return $this->_appendJunctionJoin($query); } return $query; } /** * Append a join to the junction table. * * @param \Cake\ORM\Query $query The query to append. * @param array|null $conditions The query conditions to use. * @return \Cake\ORM\Query The modified query. */ protected function _appendJunctionJoin(Query $query, ?array $conditions = null): Query { $junctionTable = $this->junction(); if ($conditions === null) { $belongsTo = $junctionTable->getAssociation($this->getTarget()->getAlias()); $conditions = $belongsTo->_joinCondition([ 'foreignKey' => $this->getTargetForeignKey(), ]); $conditions += $this->junctionConditions(); } $name = $this->_junctionAssociationName(); /** @var array $joins */ $joins = $query->clause('join'); $matching = [ $name => [ 'table' => $junctionTable->getTable(), 'conditions' => $conditions, 'type' => Query::JOIN_TYPE_INNER, ], ]; $query ->addDefaultTypes($junctionTable) ->join($matching + $joins, [], true); return $query; } /** * 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 article is linked to tags 'cake' and 'framework' and you pass * to this method an array containing the entities for tags 'cake', 'php' and 'awesome', * only the link for cake will be kept in database, the link for 'framework' will be * deleted and the links for 'php' and 'awesome' will be created. * * Existing links are not deleted and created again, they are either left untouched * or updated so that potential extra information stored in the joint row is not * lost. Updating the link row can be done by making sure the corresponding passed * target entity contains the joint property with its primary key and any extra * information to be stored. * * On success, the passed `$sourceEntity` will contain `$targetEntities` as value * in the corresponding property for this association. * * This method assumes that links between both the source entity and each of the * target entities are unique. That is, for any given row in the source table there * can only be one link in the junction table pointing to any other given row in * the target table. * * 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: * * ``` * $article->tags = [$tag1, $tag2, $tag3, $tag4]; * $articles->save($article); * $tags = [$tag1, $tag3]; * $articles->getAssociation('tags')->replaceLinks($article, $tags); * ``` * * `$article->get('tags')` will contain only `[$tag1, $tag3]` 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 replaceLinks(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool { $bindingKey = (array)$this->getBindingKey(); $primaryValue = $sourceEntity->extract($bindingKey); if (count(Hash::filter($primaryValue)) !== count($bindingKey)) { $message = 'Could not find primary key value for source entity'; throw new InvalidArgumentException($message); } return $this->junction()->getConnection()->transactional( function () use ($sourceEntity, $targetEntities, $primaryValue, $options) { $junction = $this->junction(); $target = $this->getTarget(); $foreignKey = (array)$this->getForeignKey(); $assocForeignKey = (array)$junction->getAssociation($target->getAlias())->getForeignKey(); $prefixedForeignKey = array_map([$junction, 'aliasField'], $foreignKey); $junctionPrimaryKey = (array)$junction->getPrimaryKey(); $junctionQueryAlias = $junction->getAlias() . '__matches'; $keys = $matchesConditions = []; foreach (array_merge($assocForeignKey, $junctionPrimaryKey) as $key) { $aliased = $junction->aliasField($key); $keys[$key] = $aliased; $matchesConditions[$aliased] = new IdentifierExpression($junctionQueryAlias . '.' . $key); } // Use association to create row selection // with finders & association conditions. $matches = $this->_appendJunctionJoin($this->find()) ->select($keys) ->where(array_combine($prefixedForeignKey, $primaryValue)); // Create a subquery join to ensure we get // the correct entity passed to callbacks. $existing = $junction->query() ->from([$junctionQueryAlias => $matches]) ->innerJoin( [$junction->getAlias() => $junction->getTable()], $matchesConditions ); $jointEntities = $this->_collectJointEntities($sourceEntity, $targetEntities); $inserts = $this->_diffLinks($existing, $jointEntities, $targetEntities, $options); if ($inserts === false) { return false; } if ($inserts && !$this->_saveTarget($sourceEntity, $inserts, $options)) { return false; } $property = $this->getProperty(); if (count($inserts)) { $inserted = array_combine( array_keys($inserts), (array)$sourceEntity->get($property) ) ?: []; $targetEntities = $inserted + $targetEntities; } ksort($targetEntities); $sourceEntity->set($property, array_values($targetEntities)); $sourceEntity->setDirty($property, false); return true; } ); } /** * Helper method used to delete the difference between the links passed in * `$existing` and `$jointEntities`. This method will return the values from * `$targetEntities` that were not deleted from calculating the difference. * * @param \Cake\ORM\Query $existing a query for getting existing links * @param array<\Cake\Datasource\EntityInterface> $jointEntities link entities that should be persisted * @param array $targetEntities entities in target table that are related to * the `$jointEntities` * @param array $options list of options accepted by `Table::delete()` * @return array|false Array of entities not deleted or false in case of deletion failure for atomic saves. */ protected function _diffLinks( Query $existing, array $jointEntities, array $targetEntities, array $options = [] ) { $junction = $this->junction(); $target = $this->getTarget(); $belongsTo = $junction->getAssociation($target->getAlias()); $foreignKey = (array)$this->getForeignKey(); $assocForeignKey = (array)$belongsTo->getForeignKey(); $keys = array_merge($foreignKey, $assocForeignKey); $deletes = $unmatchedEntityKeys = $present = []; foreach ($jointEntities as $i => $entity) { $unmatchedEntityKeys[$i] = $entity->extract($keys); $present[$i] = array_values($entity->extract($assocForeignKey)); } foreach ($existing as $existingLink) { $existingKeys = $existingLink->extract($keys); $found = false; foreach ($unmatchedEntityKeys as $i => $unmatchedKeys) { $matched = false; foreach ($keys as $key) { if (is_object($unmatchedKeys[$key]) && is_object($existingKeys[$key])) { // If both sides are an object then use == so that value objects // are seen as equivalent. $matched = $existingKeys[$key] == $unmatchedKeys[$key]; } else { // Use strict equality for all other values. $matched = $existingKeys[$key] === $unmatchedKeys[$key]; } // Stop checks on first failure. if (!$matched) { break; } } if ($matched) { // Remove the unmatched entity so we don't look at it again. unset($unmatchedEntityKeys[$i]); $found = true; break; } } if (!$found) { $deletes[] = $existingLink; } } $primary = (array)$target->getPrimaryKey(); $jointProperty = $this->_junctionProperty; foreach ($targetEntities as $k => $entity) { if (!($entity instanceof EntityInterface)) { continue; } $key = array_values($entity->extract($primary)); foreach ($present as $i => $data) { if ($key === $data && !$entity->get($jointProperty)) { unset($targetEntities[$k], $present[$i]); break; } } } foreach ($deletes as $entity) { if (!$junction->delete($entity, $options) && !empty($options['atomic'])) { return false; } } return $targetEntities; } /** * Throws an exception should any of the passed entities is not persisted. * * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side * of this association * @param array<\Cake\Datasource\EntityInterface> $targetEntities list of entities belonging to the `target` side * of this association * @return bool * @throws \InvalidArgumentException */ protected function _checkPersistenceStatus(EntityInterface $sourceEntity, array $targetEntities): bool { if ($sourceEntity->isNew()) { $error = 'Source entity needs to be persisted before links can be created or removed.'; throw new InvalidArgumentException($error); } foreach ($targetEntities as $entity) { if ($entity->isNew()) { $error = 'Cannot link entities that have not been persisted yet.'; throw new InvalidArgumentException($error); } } return true; } /** * Returns the list of joint entities that exist between the source entity * and each of the passed target entities * * @param \Cake\Datasource\EntityInterface $sourceEntity The row belonging to the source side * of this association. * @param array $targetEntities The rows belonging to the target side of this * association. * @throws \InvalidArgumentException if any of the entities is lacking a primary * key value * @return array<\Cake\Datasource\EntityInterface> */ protected function _collectJointEntities(EntityInterface $sourceEntity, array $targetEntities): array { $target = $this->getTarget(); $source = $this->getSource(); $junction = $this->junction(); $jointProperty = $this->_junctionProperty; $primary = (array)$target->getPrimaryKey(); $result = []; $missing = []; foreach ($targetEntities as $entity) { if (!($entity instanceof EntityInterface)) { continue; } $joint = $entity->get($jointProperty); if (!$joint || !($joint instanceof EntityInterface)) { $missing[] = $entity->extract($primary); continue; } $result[] = $joint; } if (empty($missing)) { return $result; } $belongsTo = $junction->getAssociation($target->getAlias()); $hasMany = $source->getAssociation($junction->getAlias()); $foreignKey = (array)$this->getForeignKey(); $foreignKey = array_map(function ($key) { return $key . ' IS'; }, $foreignKey); $assocForeignKey = (array)$belongsTo->getForeignKey(); $assocForeignKey = array_map(function ($key) { return $key . ' IS'; }, $assocForeignKey); $sourceKey = $sourceEntity->extract((array)$source->getPrimaryKey()); $unions = []; foreach ($missing as $key) { $unions[] = $hasMany->find() ->where(array_combine($foreignKey, $sourceKey)) ->where(array_combine($assocForeignKey, $key)); } $query = array_shift($unions); foreach ($unions as $q) { $query->union($q); } return array_merge($result, $query->toArray()); } /** * Returns the name of the association from the target table to the junction table, * this name is used to generate alias in the query and to later on retrieve the * results. * * @return string */ protected function _junctionAssociationName(): string { if (!$this->_junctionAssociationName) { $this->_junctionAssociationName = $this->getTarget() ->getAssociation($this->junction()->getAlias()) ->getName(); } return $this->_junctionAssociationName; } /** * Sets the name of the junction table. * If no arguments are passed the current configured name is returned. A default * name based of the associated tables will be generated if none found. * * @param string|null $name The name of the junction table. * @return string */ protected function _junctionTableName(?string $name = null): string { if ($name === null) { if (empty($this->_junctionTableName)) { $tablesNames = array_map('Cake\Utility\Inflector::underscore', [ $this->getSource()->getTable(), $this->getTarget()->getTable(), ]); sort($tablesNames); $this->_junctionTableName = implode('_', $tablesNames); } return $this->_junctionTableName; } return $this->_junctionTableName = $name; } /** * Parse extra options passed in the constructor. * * @param array $options original list of options passed in constructor * @return void */ protected function _options(array $options): void { if (!empty($options['targetForeignKey'])) { $this->setTargetForeignKey($options['targetForeignKey']); } if (!empty($options['joinTable'])) { $this->_junctionTableName($options['joinTable']); } if (!empty($options['through'])) { $this->setThrough($options['through']); } if (!empty($options['saveStrategy'])) { $this->setSaveStrategy($options['saveStrategy']); } if (isset($options['sort'])) { $this->setSort($options['sort']); } } }