/** * Updates counter cache for a single association * * @param \Cake\Event\Event $event Event instance. * @param \Cake\ORM\Entity $entity Entity * @param Association $assoc The association object * @param array $settings The settings for for counter cache for this association * @return void */ protected function _processAssociation(Event $event, Entity $entity, Association $assoc, array $settings) { $foreignKeys = (array) $assoc->foreignKey(); $primaryKeys = (array) $assoc->target()->primaryKey(); $countConditions = $entity->extract($foreignKeys); $updateConditions = array_combine($primaryKeys, $countConditions); $countOriginalConditions = $entity->extractOriginal($foreignKeys); if ($countOriginalConditions !== []) { $updateOriginalConditions = array_combine($primaryKeys, $countOriginalConditions); } foreach ($settings as $field => $config) { if (is_int($field)) { $field = $config; $config = []; } if (!is_string($config) && is_callable($config)) { $count = $config($event, $entity, $this->_table, false); } else { $count = $this->_getCount($config, $countConditions); } $assoc->target()->updateAll([$field => $count], $updateConditions); if (isset($updateOriginalConditions)) { if (!is_string($config) && is_callable($config)) { $count = $config($event, $entity, $this->_table, true); } else { $count = $this->_getCount($config, $countOriginalConditions); } $assoc->target()->updateAll([$field => $count], $updateOriginalConditions); } } }
/** * Converts an entity class type to its DocBlock hint type counterpart. * * @param string $type The entity class type (a fully qualified class name). * @param \Cake\ORM\Association $association The association related to the entity class. * @return string The DocBlock type */ public function associatedEntityTypeToHintType($type, Association $association) { if ($association->type() === Association::MANY_TO_MANY || $association->type() === Association::ONE_TO_MANY) { return $type . '[]'; } return $type; }
/** * Get finder to use for provided association. * * @param \Cake\ORM\Association $association Association instance * @return string */ public function finder(Association $association) { if ($association->target()->behaviors()->has('Tree')) { return 'treeList'; } return 'list'; }
/** * Method that fetches and deletes document-file manyToMany association record Entity. * * @param string $id file id * @return bool */ protected function _deleteFileAssociationRecord($id) { $query = $this->_fileStorageAssociation->find('all', ['conditions' => [$this->_fileStorageForeignKey => $id]]); $entity = $query->first(); if (is_null($entity)) { return false; } return $this->_fileStorageAssociation->delete($entity); }
/** * Sets the name of the field representing the foreign key to the target table. * If no parameters are passed current field is returned * * @param string|null $key the key to be used to link both tables together * @return string */ public function foreignKey($key = null) { if ($key === null) { if ($this->_foreignKey === null) { $this->_foreignKey = $this->_modelKey($this->target()->alias()); } return $this->_foreignKey; } return parent::foreignKey($key); }
/** * Sets the property name that should be filled with data from the target table * in the source table record. * If no arguments are passed, currently configured type is returned. * * @param string|null $name The name of the property. Pass null to read the current value. * @return string */ public function property($name = null) { if ($name !== null) { return parent::property($name); } if ($name === null && !$this->_propertyName) { list(, $name) = pluginSplit($this->_name); $this->_propertyName = Inflector::underscore(Inflector::singularize($name)); } return $this->_propertyName; }
/** * Marshals data for belongsToMany associations. * * Builds the related entities and handles the special casing * for junction table entities. * * @param \Cake\ORM\Association $assoc The association to marshal. * @param array $data The data to convert into entities. * @param array $options List of options. * @return array An array of built entities. */ protected function _belongsToMany(Association $assoc, array $data, $options = []) { $associated = isset($options['associated']) ? $options['associated'] : []; $forceNew = isset($options['forceNew']) ? $options['forceNew'] : false; $data = array_values($data); $target = $assoc->target(); $primaryKey = array_flip((array) $target->primaryKey()); $records = $conditions = []; $primaryCount = count($primaryKey); $conditions = []; foreach ($data as $i => $row) { if (!is_array($row)) { continue; } if (array_intersect_key($primaryKey, $row) === $primaryKey) { $keys = array_intersect_key($row, $primaryKey); if (count($keys) === $primaryCount) { $rowConditions = []; foreach ($keys as $key => $value) { $rowConditions[][$target->aliasfield($key)] = $value; } if ($forceNew && !$target->exists($rowConditions)) { $records[$i] = $this->one($row, $options); } $conditions = array_merge($conditions, $rowConditions); } } else { $records[$i] = $this->one($row, $options); } } if (!empty($conditions)) { $query = $target->find(); $query->andWhere(function ($exp) use($conditions) { return $exp->or_($conditions); }); $keyFields = array_keys($primaryKey); $existing = []; foreach ($query as $row) { $k = implode(';', $row->extract($keyFields)); $existing[$k] = $row; } foreach ($data as $i => $row) { $key = []; foreach ($keyFields as $k) { if (isset($row[$k])) { $key[] = $row[$k]; } } $key = implode(';', $key); // Update existing record and child associations if (isset($existing[$key])) { $records[$i] = $this->merge($existing[$key], $data[$i], $options); } } } $jointMarshaller = $assoc->junction()->marshaller(); $nested = []; if (isset($associated['_joinData'])) { $nested = (array) $associated['_joinData']; } foreach ($records as $i => $record) { // Update junction table data in _joinData. if (isset($data[$i]['_joinData'])) { $joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested); $record->set('_joinData', $joinData); } } return $records; }
/** * Merge the special _joinData property into the entity set. * * @param \Cake\Datasource\EntityInterface $original The original entity * @param \Cake\ORM\Association $assoc The association to marshall * @param array $value The data to hydrate * @param array $options List of options. * @return array An array of entities */ protected function _mergeJoinData($original, $assoc, $value, $options) { $associated = isset($options['associated']) ? $options['associated'] : []; $extra = []; foreach ($original as $entity) { // Mark joinData as accessible so we can marshal it properly. $entity->accessible('_joinData', true); $joinData = $entity->get('_joinData'); if ($joinData && $joinData instanceof EntityInterface) { $extra[spl_object_hash($entity)] = $joinData; } } $joint = $assoc->junction(); $marshaller = $joint->marshaller(); $nested = []; if (isset($associated['_joinData'])) { $nested = (array) $associated['_joinData']; } $records = $this->mergeMany($original, $value, $options); foreach ($records as $record) { $hash = spl_object_hash($record); $value = $record->get('_joinData'); if (!is_array($value)) { $record->unsetProperty('_joinData'); continue; } if (isset($extra[$hash])) { $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested)); } else { $joinData = $marshaller->one($value, $nested); $record->set('_joinData', $joinData); } } return $records; }
/** * Loads a list of belongs to many from ids. * * @param Association $assoc The association class for the belongsToMany association. * @param array $ids The list of ids to load. * @return array An array of entities. */ protected function _loadAssociatedByIds($assoc, $ids) { if (empty($ids)) { return []; } $target = $assoc->target(); $primaryKey = (array) $target->primaryKey(); $multi = count($primaryKey) > 1; $primaryKey = array_map(function ($key) use($target) { return $target->alias() . '.' . $key; }, $primaryKey); if ($multi) { if (count(current($ids)) !== count($primaryKey)) { return []; } $filter = new TupleComparison($primaryKey, $ids, [], 'IN'); } else { $filter = [$primaryKey[0] . ' IN' => $ids]; } return $target->find()->where($filter)->toArray(); }
/** * 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, the junction table will be * included in the query's contained associations. * * @param string|array $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 = parent::find($type, $options); if (!$this->conditions()) { return $query; } $belongsTo = $this->junction()->association($this->target()->alias()); $conditions = $belongsTo->_joinCondition(['foreignKey' => $this->foreignKey()]); return $this->_appendJunctionJoin($query, $conditions); }
/** * 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->property()]); }
/** * Method that retrieves one to many associated records * @param \Cake\ORM\Table $table Table object * @param \Cake\ORM\Association $association Association object * @return array associated records */ protected function _oneToManyAssociatedRecords(\Cake\ORM\Table $table, \Cake\ORM\Association $association) { $assocName = $association->name(); $assocTableName = $association->table(); $assocForeignKey = $association->foreignKey(); $recordId = $this->request->params['pass'][0]; // get associated index View csv fields $fields = $this->_getTableFields($association); $query = $table->{$assocName}->find('all', ['conditions' => [$assocForeignKey => $recordId], 'fields' => $fields]); $records = $query->all(); // store associated table records $result['records'] = $records; // store associated table fields $result['fields'] = $fields; // store associated table name $result['table_name'] = $assocTableName; return $result; }
/** * 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 = []) { if (!empty($options['negateMatch'])) { $this->_appendNotMatching($query, $options); return; } $junction = $this->junction(); $belongsTo = $junction->association($this->source()->alias()); $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->foreignKey()]); $cond += $this->junctionConditions(); if (isset($options['includeFields'])) { $includeFields = $options['includeFields']; } // Attach the junction table as well we need it to populate _joinData. $assoc = $this->_targetTable->association($junction->alias()); $newOptions = array_intersect_key($options, ['joinType' => 1, 'fields' => 1]); $newOptions += ['conditions' => $cond, 'includeFields' => $includeFields, 'foreignKey' => false]; $assoc->attachTo($query, $newOptions); $query->eagerLoader()->addToJoinsMap($junction->alias(), $assoc, true); parent::attachTo($query, $options); $foreignKey = $this->targetForeignKey(); $thisJoin = $query->clause('join')[$this->name()]; $thisJoin['conditions']->add($assoc->_joinCondition(['foreignKey' => $foreignKey])); }
/** * Loads a list of belongs to many from ids. * * @param Association $assoc The association class for the belongsToMany association. * @param array $ids The list of ids to load. * @return array An array of entities. */ protected function _loadBelongsToMany($assoc, $ids) { $target = $assoc->target(); $primaryKey = (array) $target->primaryKey(); $multi = count($primaryKey) > 1; if ($multi) { if (count(current($ids)) !== count($primaryKey)) { return []; } $filter = new TupleComparison($primaryKey, $ids, [], 'IN'); } else { $filter = [$primaryKey[0] . ' IN' => $ids]; } return $assoc->find()->where($filter)->toArray(); }
/** * Merge the special _joinData property into the entity set. * * @param \Cake\Datasource\EntityInterface $original The original entity * @param \Cake\ORM\Association $assoc The association to marshall * @param array $value The data to hydrate * @param array $options List of options. * @return array An array of entities */ protected function _mergeJoinData($original, $assoc, $value, $options) { $associated = isset($options['associated']) ? $options['associated'] : []; $extra = []; foreach ($original as $entity) { // Mark joinData as accessible so we can marshal it properly. $entity->accessible('_joinData', true); $joinData = $entity->get('_joinData'); if ($joinData && $joinData instanceof EntityInterface) { $extra[spl_object_hash($entity)] = $joinData; } } $joint = $assoc->junction(); $marshaller = $joint->marshaller(); $nested = []; if (isset($associated['_joinData'])) { $nested = (array) $associated['_joinData']; } $options['accessibleFields'] = ['_joinData' => true]; $records = $this->mergeMany($original, $value, $options); foreach ($records as $record) { $hash = spl_object_hash($record); $value = $record->get('_joinData'); // Already an entity, no further marshalling required. if ($value instanceof EntityInterface) { continue; } // Scalar data can't be handled if (!is_array($value)) { $record->unsetProperty('_joinData'); continue; } // Marshal data into the old object, or make a new joinData object. if (isset($extra[$hash])) { $record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested)); } elseif (is_array($value)) { $joinData = $marshaller->one($value, $nested); $record->set('_joinData', $joinData); } } return $records; }
/** * Helper method for saving an association's data. * * @param Association $association The association object to save with. * @param Entity $entity The entity to save * @param array $nested Options for deeper associations * @param array $options Original options * @return bool Success */ protected function _save($association, $entity, $nested, $options) { if (!$entity->dirty($association->property())) { return true; } if (!empty($nested)) { $options = (array) $nested + $options; } return (bool) $association->save($entity, $options); }
/** * {@inheritDoc} */ public function transformRow($row, $nestKey, $joined) { $alias = $this->junction()->alias(); if ($joined) { $row[$this->target()->alias()][$this->_junctionProperty] = $row[$alias]; unset($row[$alias]); } return parent::transformRow($row, $nestKey, $joined); }
/** * Get association CSV fields * @param Cake\ORM\Associations $association ORM association * @param object $action action passed * @return array */ protected function _getAssociationCsvFields(Association $association, $action) { list($plugin, $controller) = pluginSplit($association->className()); $fields = $this->_getCsvFields($controller, $action); return $fields; }