public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options) { if (empty($entity->display)) { $entity->display = Inflector::humanize($entity->name); } if ($entity->dirty('is_default') && $entity->is_default) { $this->updateAll(['is_default' => false], ['is_default' => true]); } elseif ($entity->dirty('is_default') && !$entity->is_default) { $entity->is_default = $entity->getOriginal('is_default'); } }
/** * beforeSave callback * * @param Event $event CakePHP Event * @param Entity $entity Entity to be saved * @param ArrayObject $options Additional options * @return void */ public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $options) { if (!$entity->isNew() && $entity->dirty()) { $fields = array_keys($entity->toArray()); $dirtyFields = $entity->extract($fields, true); unset($dirtyFields['modified']); $this->_dirtyFields[$entity->id] = array_keys($dirtyFields); } }
/** * Restores all (or given) trashed row(s). * * @param \Cake\Datasource\EntityInterface|null $entity to restore. * @return bool|\Cake\Datasource\EntityInterface|int|mixed */ public function restoreTrash(EntityInterface $entity = null) { $data = [$this->getTrashField(false) => null]; if ($entity instanceof EntityInterface) { if ($entity->dirty()) { throw new RuntimeException('Can not restore from a dirty entity.'); } $entity->set($data); return $this->_table->save($entity); } return $this->_table->updateAll($data, $this->_getUnaryExpression()); }
/** * {@inheritDoc} * * ### Options * * The options array accepts the following keys: * * - atomic: Whether to execute the save and callbacks inside a database * transaction (default: true) * - checkRules: Whether or not to check the rules on entity before saving, if the checking * fails, it will abort the save operation. (default:true) * - associated: If true it will save all associated entities as they are found * in the passed `$entity` whenever the property defined for the association * is marked as dirty. Associated records are saved recursively unless told * otherwise. If an array, it will be interpreted as the list of associations * to be saved. It is possible to provide different options for saving on associated * table objects using this key by making the custom options the array value. * If false no associated records will be saved. (default: true) * - checkExisting: Whether or not to check if the entity already exists, assuming that the * entity is marked as not new, and the primary key has been set. * * ### Events * * When saving, this method will trigger four events: * * - Model.beforeRules: Will be triggered right before any rule checking is done * for the passed entity if the `checkRules` key in $options is not set to false. * Listeners will receive as arguments the entity, options array and the operation type. * If the event is stopped the rules check result will be set to the result of the event itself. * - Model.afterRules: Will be triggered right after the `checkRules()` method is * called for the entity. Listeners will receive as arguments the entity, * options array, the result of checking the rules and the operation type. * If the event is stopped the checking result will be set to the result of * the event itself. * - Model.beforeSave: Will be triggered just before the list of fields to be * persisted is calculated. It receives both the entity and the options as * arguments. The options array is passed as an ArrayObject, so any changes in * it will be reflected in every listener and remembered at the end of the event * so it can be used for the rest of the save operation. Returning false in any * of the listeners will abort the saving process. If the event is stopped * using the event API, the event object's `result` property will be returned. * This can be useful when having your own saving strategy implemented inside a * listener. * - Model.afterSave: Will be triggered after a successful insert or save, * listeners will receive the entity and the options array as arguments. The type * of operation performed (insert or update) can be determined by checking the * entity's method `isNew`, true meaning an insert and false an update. * - Model.afterSaveCommit: Will be triggered after the transaction is commited * for atomic save, listeners will receive the entity and the options array * as arguments. * * This method will determine whether the passed entity needs to be * inserted or updated in the database. It does that by checking the `isNew` * method on the entity. If the entity to be saved returns a non-empty value from * its `errors()` method, it will not be saved. * * ### Saving on associated tables * * This method will by default persist entities belonging to associated tables, * whenever a dirty property matching the name of the property name set for an * association in this table. It is possible to control what associations will * be saved and to pass additional option for saving them. * * ``` * // Only save the comments association * $articles->save($entity, ['associated' => ['Comments']); * * // Save the company, the employees and related addresses for each of them. * // For employees do not check the entity rules * $companies->save($entity, [ * 'associated' => [ * 'Employees' => [ * 'associated' => ['Addresses'], * 'checkRules' => false * ] * ] * ]); * * // Save no associations * $articles->save($entity, ['associated' => false]); * ``` * */ public function save(EntityInterface $entity, $options = []) { $options = new ArrayObject($options + ['atomic' => true, 'associated' => true, 'checkRules' => true, 'checkExisting' => true, '_primary' => true]); if ($entity->errors()) { return false; } if ($entity->isNew() === false && !$entity->dirty()) { return $entity; } $connection = $this->connection(); if ($options['atomic']) { $success = $connection->transactional(function () use($entity, $options) { return $this->_processSave($entity, $options); }); } else { $success = $this->_processSave($entity, $options); } if ($success) { if (!$connection->inTransaction() && ($options['atomic'] || !$options['atomic'] && $options['_primary'])) { $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); } if ($options['atomic'] || $options['_primary']) { $entity->isNew(false); $entity->source($this->registryAlias()); } } return $success; }
/** * 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]; } $foreignKey = (array) $this->foreignKey(); $target = $this->target(); $targetPrimaryKey = array_merge((array) $target->primaryKey(), $foreignKey); $property = $this->property(); $conditions = ['OR' => (new Collection($targetEntities))->map(function ($entity) use($targetPrimaryKey) { return $entity->extract($targetPrimaryKey); })->toList()]; $this->_unlink($foreignKey, $target, $conditions, $options); if ($options['cleanProperty']) { $sourceEntity->set($property, (new Collection($sourceEntity->get($property)))->reject(function ($assoc) use($targetEntities) { return in_array($assoc, $targetEntities); })->toList()); } $sourceEntity->dirty($property, false); }
/** * Helper method used to generated multiple translated field entities * out of the data found in the `_translations` property in the passed * entity. The result will be put into its `_i18n` property * * @param \Cake\Datasource\EntityInterface $entity Entity * @return void */ protected function _bundleTranslatedFields($entity) { $translations = (array) $entity->get('_translations'); if (empty($translations) && !$entity->dirty('_translations')) { return; } $fields = $this->_config['fields']; $primaryKey = (array) $this->_table->primaryKey(); $key = $entity->get(current($primaryKey)); $find = []; foreach ($translations as $lang => $translation) { foreach ($fields as $field) { if (!$translation->dirty($field)) { continue; } $find[] = ['locale' => $lang, 'field' => $field, 'foreign_key' => $key]; $contents[] = new Entity(['content' => $translation->get($field)], ['useSetters' => false]); } } if (empty($find)) { return; } $results = $this->_findExistingTranslations($find); foreach ($find as $i => $translation) { if (!empty($results[$i])) { $contents[$i]->set('id', $results[$i], ['setter' => false]); $contents[$i]->isNew(false); } else { $translation['model'] = $this->_config['referenceName']; $contents[$i]->set($translation, ['setter' => false, 'guard' => false]); $contents[$i]->isNew(true); } } $entity->set('_i18n', $contents); }
/** * Helper method for saving an association's data. * * @param \Cake\ORM\Association $association The association object to save with. * @param \Cake\Datasource\EntityInterface $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->saveAssociated($entity, $options); }
/** * Helper method used to generated multiple translated field entities * out of the data found in the `_translations` property in the passed * entity. The result will be put into its `_i18n` property * * @param \Cake\Datasource\EntityInterface $entity Entity * @return void */ protected function _bundleTranslatedFields($entity) { $translations = (array) $entity->get('_translations'); if (empty($translations) && !$entity->dirty('_translations')) { return; } $primaryKey = (array) $this->_table->primaryKey(); $key = $entity->get(current($primaryKey)); foreach ($translations as $lang => $translation) { if (!$translation->id) { $update = ['id' => $key, 'locale' => $lang]; $translation->set($update, ['guard' => false]); } } $entity->set('_i18n', $translations); }
/** * Returns fields that have been marked as protected. * * @param \Cake\Datasource\EntityInterface $entity * @return array */ public function fields(EntityInterface $entity) { $fields = []; foreach ((array) $this->config('fields') as $field) { if (!$entity->dirty($field)) { continue; } $fields[$field] = $entity->{$field}; $entity->dirty($field, false); } return $fields; }
/** * Update a field, if it hasn't been updated already * * @param \Cake\Datasource\EntityInterface $entity Entity instance. * @param string $field Field name * @param bool $refreshTimestamp Whether to refresh timestamp. * @return void */ protected function _updateField($entity, $field, $refreshTimestamp) { if ($entity->dirty($field)) { return; } $entity->set($field, $this->timestamp(null, $refreshTimestamp)); }
/** * Callback to obfuscate the record(s)' primary key returned after a save operation. * * @param \Cake\ORM\Behavior\Event $event Event. * @param \Cake\ORM\Behavior\EntityInterface $entity Entity. * @param \ArrayObject $options Options. * @return void */ public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options) { $pk = $this->_table->primaryKey(); $entity->set($pk, $this->obfuscate($entity->{$pk})); $entity->dirty($pk, false); }
/** * Checks if required fields have been modified. Returns true * if any of the fields has been modified, otherwise false. * * @param \Cake\Datasource\EntityInterface $entity Entity instance * @param array $requiredFields Required fields list * @return bool */ protected function _requiredFieldsModified(EntityInterface $entity, array $requiredFields) { $result = false; if (empty($requiredFields)) { return $result; } // check if any of the required fields was modified and set modified flag to true foreach ($requiredFields as $field) { if (!$entity->dirty($field)) { continue; } $result = true; break; } return $result; }
/** * checkRules rule. * * @param \Cake\Datasource\EntityInterface $entity Entity. * @return bool */ public function checkRules(EntityInterface $entity) { $field = $this->_config['discriminatorField']; if ($entity->dirty($field)) { return $this->_matches($entity->get($field), $this->acceptedDiscriminators()); } return true; }
/** * Merges `$data` into `$entity` and recursively does the same for each one of * the association names passed in `$options`. When merging associations, if an * entity is not present in the parent entity for a given association, a new one * will be created. * * When merging HasMany or BelongsToMany associations, all the entities in the * `$data` array will appear, those that can be matched by primary key will get * the data merged, but those that cannot, will be discarded. `ids` option can be used * to determine whether the association must use the `_ids` format. * * ### Options: * * - associated: Associations listed here will be marshalled as well. * - validate: Whether or not to validate data before hydrating the entities. Can * also be set to a string to use a specific validator. Defaults to true/default. * - fieldList: A whitelist of fields to be assigned to the entity. If not present * the accessible fields list in the entity will be used. * - accessibleFields: A list of fields to allow or deny in entity accessible fields. * * The above options can be used in each nested `associated` array. In addition to the above * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations. * When true this option restricts the request data to only be read from `_ids`. * * ``` * $result = $marshaller->merge($entity, $data, [ * 'associated' => ['Tags' => ['onlyIds' => true]] * ]); * ``` * * @param \Cake\Datasource\EntityInterface $entity the entity that will get the * data merged in * @param array $data key value list of fields to be merged into the entity * @param array $options List of options. * @return \Cake\Datasource\EntityInterface */ public function merge(EntityInterface $entity, array $data, array $options = []) { list($data, $options) = $this->_prepareDataAndOptions($data, $options); $propertyMap = $this->_buildPropertyMap($options); $isNew = $entity->isNew(); $keys = []; if (!$isNew) { $keys = $entity->extract((array) $this->_table->primaryKey()); } if (isset($options['accessibleFields'])) { foreach ((array) $options['accessibleFields'] as $key => $value) { $entity->accessible($key, $value); } } $errors = $this->_validate($data + $keys, $options, $isNew); $schema = $this->_table->schema(); $properties = $marshalledAssocs = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { if ($entity instanceof InvalidPropertyInterface) { $entity->invalid($key, $value); } continue; } $columnType = $schema->columnType($key); $original = $entity->get($key); if (isset($propertyMap[$key])) { $assoc = $propertyMap[$key]['association']; $value = $this->_mergeAssociation($original, $assoc, $value, $propertyMap[$key]); $marshalledAssocs[$key] = true; } elseif ($columnType) { $converter = Type::build($columnType); $value = $converter->marshal($value); $isObject = is_object($value); if (!$isObject && $original === $value || $isObject && $original == $value) { continue; } } $properties[$key] = $value; } if (!isset($options['fieldList'])) { $entity->set($properties); $entity->errors($errors); foreach (array_keys($marshalledAssocs) as $field) { if ($properties[$field] instanceof EntityInterface) { $entity->dirty($field, $properties[$field]->dirty()); } } return $entity; } foreach ((array) $options['fieldList'] as $field) { if (array_key_exists($field, $properties)) { $entity->set($field, $properties[$field]); if ($properties[$field] instanceof EntityInterface && isset($marshalledAssocs[$field])) { $entity->dirty($field, $properties[$field]->dirty()); } } } $entity->errors($errors); return $entity; }
/** * {@inheritDoc} * * ### Options * * The options array can receive the following keys: * * - atomic: Whether to execute the save and callbacks inside a database * transaction (default: true) * - validate: Whether or not validate the entity before saving, if validation * fails, it will abort the save operation. If this key is set to a string value, * the validator object registered in this table under the provided name will be * used instead of the default one. (default:true) * - associated: If true it will save all associated entities as they are found * in the passed `$entity` whenever the property defined for the association * is marked as dirty. Associated records are saved recursively unless told * otherwise. If an array, it will be interpreted as the list of associations * to be saved. It is possible to provide different options for saving on associated * table objects using this key by making the custom options the array value. * If false no associated records will be saved. (default: true) * * ### Events * * When saving, this method will trigger four events: * * - Model.beforeValidate: Will be triggered right before any validation is done * for the passed entity if the validate key in $options is not set to false. * Listeners will receive as arguments the entity, the options array and the * validation object to be used for validating the entity. If the event is * stopped the validation result will be set to the result of the event itself. * - Model.afterValidate: Will be triggered right after the `validate()` method is * called in the entity. Listeners will receive as arguments the entity, the * options array and the validation object to be used for validating the entity. * If the event is stopped the validation result will be set to the result of * the event itself. * - Model.beforeSave: Will be triggered just before the list of fields to be * persisted is calculated. It receives both the entity and the options as * arguments. The options array is passed as an ArrayObject, so any changes in * it will be reflected in every listener and remembered at the end of the event * so it can be used for the rest of the save operation. Returning false in any * of the listeners will abort the saving process. If the event is stopped * using the event API, the event object's `result` property will be returned. * This can be useful when having your own saving strategy implemented inside a * listener. * - Model.afterSave: Will be triggered after a successful insert or save, * listeners will receive the entity and the options array as arguments. The type * of operation performed (insert or update) can be determined by checking the * entity's method `isNew`, true meaning an insert and false an update. * * This method will determine whether the passed entity needs to be * inserted or updated in the database. It does that by checking the `isNew` * method on the entity, if no information can be found there, it will go * directly to the database to check the entity's status. * * ### Saving on associated tables * * This method will by default persist entities belonging to associated tables, * whenever a dirty property matching the name of the property name set for an * association in this table. It is possible to control what associations will * be saved and to pass additional option for saving them. * * {{{ * // Only save the comments association * $articles->save($entity, ['associated' => ['Comments']); * * // Save the company, the employees and related addresses for each of them. * // For employees use the 'special' validation group * $companies->save($entity, [ * 'associated' => [ * 'Employees' => [ * 'associated' => ['Addresses'], * 'validate' => 'special' * ] * ] * ]); * * // Save no associations * $articles->save($entity, ['associated' => false]); * }}} * */ public function save(EntityInterface $entity, $options = []) { $options = new \ArrayObject($options + ['atomic' => true, 'validate' => true, 'associated' => true]); if ($entity->isNew() === false && !$entity->dirty()) { return $entity; } if ($options['atomic']) { $connection = $this->connection(); $success = $connection->transactional(function () use($entity, $options) { return $this->_processSave($entity, $options); }); } else { $success = $this->_processSave($entity, $options); } return $success; }
/** * Ensures that the provided entity contains non-empty values for the left and * right fields * * @param \Cake\Datasource\EntityInterface $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); } }
/** * Sets up hashid for model. * * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved * @return bool True if save should proceed, false otherwise */ public function encode(EntityInterface $entity) { $idField = $this->_primaryKey; $id = $entity->get($idField); if (!$id) { return false; } $field = $this->_config['field']; if (!$field) { return false; } $hashid = $this->encodeId($id); $entity->set($field, $hashid); $entity->dirty($field, false); return true; }