/** * 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); } } }
/** * 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\ORM\Entity $entity an entity from the source table * @param array|\ArrayObject $options options to be passed to the save method in * the target table * @return bool|Entity false if $entity could not be saved, otherwise it returns * the saved entity * @see Table::save() * @throws \InvalidArgumentException when the association data cannot be traversed. */ public function save(Entity $entity, array $options = []) { $targetEntities = $entity->get($this->property()); if (empty($targetEntities)) { return $entity; } if (!is_array($targetEntities) && !$targetEntities instanceof \Traversable) { $name = $this->property(); $message = sprintf('Could not save %s, it cannot be traversed', $name); throw new \InvalidArgumentException($message); } $properties = array_combine((array) $this->foreignKey(), $entity->extract((array) $this->source()->primaryKey())); $target = $this->target(); $original = $targetEntities; foreach ($targetEntities as $k => $targetEntity) { if (!$targetEntity instanceof Entity) { break; } if (!empty($options['atomic'])) { $targetEntity = clone $targetEntity; } $targetEntity->set($properties, ['guard' => false]); if ($target->save($targetEntity, $options)) { $targetEntities[$k] = $targetEntity; continue; } if (!empty($options['atomic'])) { $original[$k]->errors($targetEntity->errors()); $entity->set($this->property(), $original); return false; } } $entity->set($this->property(), $targetEntities); return $entity; }
/** * Cascade a delete to remove dependent records. * * This method does nothing if the association is not dependent. * * @param \Cake\ORM\Entity $entity The entity that started the cascaded delete. * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(Entity $entity, array $options = []) { if (!$this->dependent()) { return true; } $table = $this->target(); $foreignKey = (array) $this->foreignKey(); $primaryKey = (array) $this->source()->primaryKey(); $conditions = array_combine($foreignKey, $entity->extract($primaryKey)); if ($this->_cascadeCallbacks) { $query = $this->find('all')->where($conditions)->bufferResults(false); foreach ($query as $related) { $table->delete($related, $options); } return true; } $conditions = array_merge($conditions, $this->conditions()); return $table->deleteAll($conditions); }
/** * Ensures that the provided entity contains non-empty values for the left and * right fields * * @param \Cake\ORM\Entity $entity The entity to ensure fields for * @return void */ protected function _ensureFields($entity) { $config = $this->config(); $fields = [$config['left'], $config['right']]; $values = array_filter($entity->extract($fields)); if (count($values) === count($fields)) { return; } $fresh = $this->_table->get($entity->get($this->_getPrimaryKey()), $fields); $entity->set($fresh->extract($fields), ['guard' => false]); foreach ($fields as $field) { $entity->dirty($field, false); } }
/** * Tests extract only dirty properties * * @return void */ public function testExtractDirty() { $entity = new Entity(['id' => 1, 'title' => 'Foo', 'author_id' => 3]); $entity->dirty('id', false); $entity->dirty('title', false); $expected = ['author_id' => 3]; $result = $entity->extract(['id', 'title', 'author_id'], true); $this->assertEquals($expected, $result); }
/** * Sets entity's order and updates order of other records when necessary. * * @param \Cake\Event\Event $event The beforeSave event that was fired. * @param \Cake\ORM\Entity $entity The entity that is going to be saved. * @param \ArrayObject $options The options passed to the save method. * * @return void */ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) { $config = $this->config(); $newOrder = null; $newScope = []; // If scope are specified and data for all scope fields is not // provided we cannot calculate new order if ($config['scope']) { $newScope = $entity->extract($config['scope']); if (count($newScope) !== count($config['scope'])) { return; } // Modify where clauses when NULL values are used foreach ($newScope as $field => $value) { if (is_null($value)) { $newScope[$field . ' IS'] = $value; unset($newScope[$field]); } } } $orderField = $config['order']; $newOrder = $entity->get($orderField); // Adding if ($entity->isNew()) { // Order not specified if ($newOrder === null) { // Insert at end of list $entity->set($orderField, $this->_getHighestOrder($newScope) + 1); // Order specified } else { // Increment order of records it's inserted before $this->_sync([$orderField => $this->_getUpdateExpression('+')], [$orderField . ' >=' => $newOrder], $newScope); } // Editing } else { // No action if no new order or scope specified if ($newOrder === null && !$newScope) { return; } list($oldOrder, $oldScope) = $this->_getOldValues($entity); // No action if new and old scope and order same if ($newOrder == $oldOrder && $newScope == $oldScope) { return; } // If changing scope if ($newScope && $newScope != $oldScope) { // Decrement records in old scope with higher order than moved record old order $this->_sync([$orderField => $this->_getUpdateExpression('-')], [$orderField . ' >' => $oldOrder], $oldScope); // Order not specified if ($newOrder === null) { // Insert at end of new scope $entity->set($orderField, $this->_getHighestOrder($newScope) + 1); // Order specified } else { // Increment records in new scope with higher order than moved record new order $this->_sync([$orderField => $this->_getUpdateExpression('+')], [$orderField . ' >=' => $newOrder], $newScope); } // Same scope } else { // If moving up if ($newOrder < $oldOrder) { // Increment order of those in between $this->_sync([$orderField => $this->_getUpdateExpression('+')], [$orderField . ' >=' => $newOrder, $orderField . ' <' => $oldOrder], $newScope); // Moving down } else { // Decrement order of those in between $this->_sync([$orderField => $this->_getUpdateExpression('-')], [$orderField . ' >' => $oldOrder, $orderField . ' <=' => $newOrder], $newScope); } } } }
/** * Returns the list of joint entities that exist between the source entity * and each of the passed target entities * * @param \Cake\ORM\Entity $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 */ protected function _collectJointEntities($sourceEntity, $targetEntities) { $target = $this->target(); $source = $this->source(); $junction = $this->junction(); $jointProperty = $this->_junctionProperty; $primary = (array) $target->primaryKey(); $result = []; $missing = []; foreach ($targetEntities as $entity) { $joint = $entity->get($jointProperty); if (!$joint) { $missing[] = $entity->extract($primary); continue; } $result[] = $joint; } if (empty($missing)) { return $result; } $belongsTo = $junction->association($target->alias()); $hasMany = $source->association($junction->alias()); $foreignKey = (array) $this->foreignKey(); $assocForeignKey = (array) $belongsTo->foreignKey(); $sourceKey = $sourceEntity->extract((array) $source->primaryKey()); foreach ($missing as $key) { $unions[] = $hasMany->find('all')->where(array_combine($foreignKey, $sourceKey))->andWhere(array_combine($assocForeignKey, $key)); } $query = array_shift($unions); foreach ($unions as $q) { $query->union($q); } return array_merge($result, $query->toArray()); }
/** * Validator method used to check the uniqueness of a value for a column. * This is meant to be used with the validation API and not to be called * directly. * * ### Example: * * ``` * $validator->add('email', [ * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table'] * ]) * ``` * * Unique validation can be scoped to the value of another column: * * ``` * $validator->add('email', [ * 'unique' => [ * 'rule' => ['validateUnique', ['scope' => 'site_id']], * 'provider' => 'table' * ] * ]); * ``` * * In the above example, the email uniqueness will be scoped to only rows having * the same site_id. Scoping will only be used if the scoping field is present in * the data to be validated. * * @param mixed $value The value of column to be checked for uniqueness * @param array $options The options array, optionally containing the 'scope' key. * May also be the validation context if there are no options. * @param array|null $context Either the validation context or null. * @return bool true if the value is unique */ public function validateUnique($value, array $options, array $context = null) { if ($context === null) { $context = $options; } $entity = new Entity($context['data'], ['useSetters' => false, 'markNew' => $context['newRecord'], 'source' => $this->registryAlias()]); $fields = array_merge([$context['field']], isset($options['scope']) ? (array) $options['scope'] : []); $values = $entity->extract($fields); foreach ($values as $field) { if ($field !== null && !is_scalar($field)) { return false; } } $rule = new IsUnique($fields); return $rule($entity, ['repository' => $this]); }
/** * Modifies the entity before it is saved so that versioned fields are persisted * in the database too. * * @param \Cake\Event\Event $event The beforeSave event that was fired * @param \Cake\ORM\Entity $entity The entity that is going to be saved * @param \ArrayObject $options the options passed to the save method * @return void */ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) { $association = $this->versionAssociation(); $name = $association->name(); $newOptions = [$name => ['validate' => false]]; $options['associated'] = $newOptions + $options['associated']; $fields = $this->_fields(); $values = $entity->extract($fields, $this->_config['onlyDirty']); $model = $this->_config['referenceName']; $primaryKey = (array) $this->_table->primaryKey(); $foreignKey = $this->_extractForeignKey($entity); $versionField = $this->_config['versionField']; if (isset($options['versionId'])) { $versionId = $options['versionId']; } else { $table = TableRegistry::get($this->_config['versionTable']); $preexistent = $table->find()->select(['version_id'])->where(['model' => $model] + $foreignKey)->order(['id desc'])->limit(1)->hydrate(false)->toArray(); $versionId = Hash::get($preexistent, '0.version_id', 0) + 1; } $created = new DateTime(); $new = []; foreach ($values as $field => $content) { if (in_array($field, $primaryKey) || $field == $versionField) { continue; } $data = ['version_id' => $versionId, 'model' => $model, 'field' => $field, 'content' => $content, 'created' => $created] + $foreignKey; $event = new Event('Model.Version.beforeSave', $this, $options); $userData = EventManager::instance()->dispatch($event); if (isset($userData->result) && is_array($userData->result)) { $data = array_merge($data, $userData->result); } $entityClass = $table->entityClass(); $new[$field] = new $entityClass($data, ['useSetters' => false, 'markNew' => true]); } $entity->set($association->property(), $new); if (!empty($versionField) && in_array($versionField, $this->_table->schema()->columns())) { $entity->set($this->_config['versionField'], $versionId); } }
/** * 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\ORM\Entity $entity an entity from the source table * @param array|\ArrayObject $options options to be passed to the save method in * the target table * @return bool|Entity false if $entity could not be saved, otherwise it returns * the saved entity * @see Table::save() */ public function save(Entity $entity, array $options = []) { $targetEntity = $entity->get($this->property()); if (empty($targetEntity) || !$targetEntity instanceof Entity) { return $entity; } $properties = array_combine((array) $this->foreignKey(), $entity->extract((array) $this->source()->primaryKey())); $targetEntity->set($properties, ['guard' => false]); if (!$this->target()->save($targetEntity, $options)) { $targetEntity->unsetProperty(array_keys($properties)); return false; } return $entity; }
/** * Modifies the entity before it is saved so that versioned fields are persisted * in the database too. * * @param \Cake\Event\Event $event The beforeSave event that was fired * @param \Cake\ORM\Entity $entity The entity that is going to be saved * @param \ArrayObject $options the options passed to the save method * @return void */ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) { $table = $this->_config['versionTable']; $newOptions = [$table => ['validate' => false]]; $options['associated'] = $newOptions + $options['associated']; $fields = $this->_fields(); $values = $entity->extract($fields); $model = $this->_table->alias(); $primaryKey = (array) $this->_table->primaryKey(); $primaryKey = current($primaryKey); $foreignKey = $entity->get($primaryKey); $versionField = $this->_config['versionField']; $preexistent = TableRegistry::get($table)->find()->select(['version_id'])->where(compact('foreign_key', 'model'))->order(['id desc'])->limit(1)->hydrate(false)->toArray(); $versionId = Hash::get($preexistent, '0.version_id', 0) + 1; $created = new Time(); foreach ($values as $field => $content) { if ($field == $primaryKey || $field == $versionField) { continue; } $data = ['version_id' => $versionId, 'model' => $model, 'foreign_key' => $foreignKey, 'field' => $field, 'content' => $content, 'created' => $created]; $new[$field] = new Entity($data, ['useSetters' => false, 'markNew' => true]); } $entity->set('__version', $new); if (!empty($versionField) && in_array($versionField, $fields)) { $entity->set($this->_config['versionField'], $versionId); } }