/** * Perform the delete operation. * * Will soft delete the entity provided. Will remove rows from any * dependent associations, and clear out join tables for BelongsToMany associations. * * @param \Cake\DataSource\EntityInterface $entity The entity to soft delete. * @param \ArrayObject $options The options for the delete. * @throws \InvalidArgumentException if there are no primary key values of the * passed entity * @return bool success */ protected function _processDelete($entity, $options) { if ($entity->isNew()) { return false; } $primaryKey = (array) $this->primaryKey(); if (!$entity->has($primaryKey)) { $msg = 'Deleting requires all primary key values.'; throw new \InvalidArgumentException($msg); } if ($options['checkRules'] && !$this->checkRules($entity, RulesChecker::DELETE, $options)) { return false; } $event = $this->dispatchEvent('Model.beforeDelete', ['entity' => $entity, 'options' => $options]); if ($event->isStopped()) { return $event->result; } $this->_associations->cascadeDelete($entity, ['_primary' => false] + $options->getArrayCopy()); $query = $this->query(); $conditions = (array) $entity->extract($primaryKey); $statement = $query->update()->set([$this->getSoftDeleteField() => 0])->where($conditions)->execute(); $success = $statement->rowCount() > 0; if (!$success) { return $success; } $this->dispatchEvent('Model.afterDelete', ['entity' => $entity, 'options' => $options]); return $success; }
public function beforeSave(Event $event, EntityInterface $entity) { if ($entity === null) { return true; } $isNew = $entity->isNew(); $fields = $this->config('fields'); $ip = self::$_request->clientIp(); foreach ($fields as $field => $when) { $when = strtolower($when); if (!in_array($when, ['always', 'new'])) { throw new UnexpectedValueException(sprintf('"When" should be one of "always", "new". The passed value "%s" is invalid', $when)); } switch ($when) { case 'always': $entity->set($field, $ip); continue; break; case 'new': if ($isNew) { $entity->set($field, $ip); continue; } break; } } return true; }
public function beforeSave(Event $event, EntityInterface $entity) { $config = $this->config(); $new = $entity->isNew(); if ($config['when'] === 'always' || $config['when'] === 'new' && $new || $config['when'] === 'existing' && !$new) { $this->slug($entity); } }
/** * afterSave callback * * Does not call the parent to avoid that the regular file storage event listener saves the image already * * @param \Cake\Event\Event $event * @param \Cake\Datasource\EntityInterface $entity * @param array $options * @return boolean */ public function afterSave(Event $event, EntityInterface $entity, $options) { if ($entity->isNew()) { $this->dispatchEvent('ImageStorage.afterSave', ['record' => $entity, 'storage' => $this->storageAdapter($entity->get('adapter'))]); $this->deleteOldFileOnSave($entity); } return true; }
/** * Generates IDs for an entity before it is saved to the database. * * @param Event $event Instance of save event * @param EntityInterface $entity Entity being saved */ public function beforeSave(Event $event, EntityInterface $entity) { // Check if entity is being created in database // If so, update appropriate ID fields if present if ($entity->isNew()) { $entity->set($this->config('base64.field'), $this->generateBase64Id()); $entity->set($this->config('uuid.field'), $this->generateUuid()); } }
/** * afterSave Callback * * @param Event $event CakePHP Event * @param EntityInterface $entity Entity that was saved * @return void */ public function afterSave(Event $event, EntityInterface $entity) { $action = $entity->isNew() ? ModelHistory::ACTION_CREATE : ModelHistory::ACTION_UPDATE; $dirtyFields = null; if ($action === ModelHistory::ACTION_UPDATE && isset($this->_dirtyFields[$entity->id])) { $dirtyFields = $this->_dirtyFields[$entity->id]; unset($this->_dirtyFields[$entity->id]); } $this->ModelHistory->add($entity, $action, $this->_getUserId(), ['dirtyFields' => $dirtyFields]); }
/** * Save the file to the storage backend after the record was created. * * @param \Cake\Event\Event $event * @param \Cake\Datasource\EntityInterface $entity * @return void */ public function afterSave(Event $event, EntityInterface $entity) { if ($this->_checkEvent($event) && $entity->isNew()) { $fileField = $this->config('fileField'); $entity['hash'] = $this->getFileHash($entity, $fileField); $entity['path'] = $this->pathBuilder()->path($entity); if (!$this->_storeFile($event)) { return; } $event->stopPropagation(); } }
/** * Performs the uniqueness check * * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields * @param array $options Options passed to the check, * where the `repository` key is required. * @return bool */ public function __invoke(EntityInterface $entity, array $options) { if (!$entity->extract($this->_fields, true)) { return true; } $conditions = $entity->extract($this->_fields); if ($entity->isNew() === false) { $keys = (array) $options['repository']->primaryKey(); $keys = $entity->extract($keys); if (array_filter($keys, 'strlen')) { $conditions['NOT'] = $keys; } } return !$options['repository']->exists($conditions); }
/** * There is only one event handler, it can be configured to be called for any event * * @param \Cake\Event\Event $event Event instance. * @param \Cake\Datasource\EntityInterface $entity Entity instance. * @throws \UnexpectedValueException if a field's when value is misdefined * @return true (irrespective of the behavior logic, the save will not be prevented) * @throws \UnexpectedValueException When the value for an event is not 'always', 'new' or 'existing' */ public function handleEvent(Event $event, EntityInterface $entity) { $eventName = $event->name(); $events = $this->_config['events']; $new = $entity->isNew() !== false; $refresh = $this->_config['refreshTimestamp']; foreach ($events[$eventName] as $field => $when) { if (!in_array($when, ['always', 'new', 'existing'])) { throw new UnexpectedValueException(sprintf('When should be one of "always", "new" or "existing". The passed value "%s" is invalid', $when)); } if ($when === 'always' || $when === 'new' && $new || $when === 'existing' && !$new) { $this->_updateField($entity, $field, $refresh); } } return true; }
/** * Save the file to the storage backend after the record was created. * * @param \Cake\Event\Event $event * @param \Cake\Datasource\EntityInterface $entity * @return void */ public function afterSave(Event $event, EntityInterface $entity) { if ($this->_checkEvent($event) && $entity->isNew()) { $fileField = $this->config('fileField'); $entity['hash'] = $this->getFileHash($entity, $fileField); $entity['path'] = $this->pathBuilder()->fullPath($entity); if (!$this->_storeFile($event)) { return; } if ($this->_config['imageProcessing'] === true) { $this->autoProcessImageVersions($entity, 'create'); } $event->result = true; $event->stopPropagation(); } }
/** * Modify entity * * @param \Cake\Datasource\EntityInterface entity * @param \Cake\ORM\Association table * @param string path prefix * @return void */ protected function _modifyEntity(EntityInterface $entity, Association $table = null, $pathPrefix = '') { if (is_null($table)) { $table = $this->_table; } // unset primary key unset($entity->{$table->primaryKey()}); // unset foreign key if ($table instanceof Association) { unset($entity->{$table->foreignKey()}); } // unset configured foreach ($this->config('remove') as $field) { $field = $this->_fieldByPath($field, $pathPrefix); if ($field) { unset($entity->{$field}); } } // set / prepend / append foreach (['set', 'prepend', 'append'] as $action) { foreach ($this->config($action) as $field => $value) { $field = $this->_fieldByPath($field, $pathPrefix); if ($field) { if ($action == 'prepend') { $value .= $entity->{$field}; } if ($action == 'append') { $value = $entity->{$field} . $value; } $entity->{$field} = $value; } } } // set as new $entity->isNew(true); // modify related entities foreach ($this->config('contain') as $contain) { if (preg_match('/^' . preg_quote($pathPrefix, '/') . '([^.]+)/', $contain, $matches)) { foreach ($entity->{Inflector::tableize($matches[1])} as $related) { if ($related->isNew()) { continue; } $this->_modifyEntity($related, $table->{$matches[1]}, $pathPrefix . $matches[1] . '.'); } } } }
/** * Regenerates snapshot after new content type is created. * * @param \Cake\Event\Event $event The event that was triggered * @param \Cake\Datasource\EntityInterface $entity The entity that was saved * @param \ArrayObject $options Array of options * @return void */ public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options = null) { if ($entity->isNew()) { snapshot(); } }
/** * Persists an resource based on the fields that are marked as dirty and * returns the same resource after a successful save or false in case * of any error. * * @param \Cake\Datasource\EntityInterface $resource the resource to be saved * @param array|\ArrayAccess $options The options to use when saving. * * @return \Cake\Datasource\EntityInterface|bool */ public function save(EntityInterface $resource, $options = []) { $options = new ArrayObject($options + ['checkRules' => true]); $mode = $resource->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; if ($options['checkRules'] && !$this->checkRules($resource, $mode, $options)) { return false; } $data = $resource->extract($this->schema()->columns(), true); if ($resource->isNew()) { $query = $this->query()->create(); } else { $query = $this->query()->update()->where([$this->primaryKey() => $resource->get($this->primaryKey())]); } $query->set($data); $result = $query->execute(); if (!$result) { return false; } if ($resource->isNew() && $result instanceof EntityInterface) { return $result; } $className = get_class($resource); return new $className($resource->toArray(), ['markNew' => false, 'markClean' => true]); }
/** * 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 $targetEntities list of entities belonging to the `target` side * of this association * @return bool * @throws \InvalidArgumentException */ protected function _checkPersistenceStatus($sourceEntity, array $targetEntities) { if ($sourceEntity->isNew()) { $error = 'Source entity needs to be persisted before proceeding'; throw new InvalidArgumentException($error); } foreach ($targetEntities as $entity) { if ($entity->isNew()) { $error = 'Cannot link not persisted entities'; throw new InvalidArgumentException($error); } } return true; }
/** * Persists an entity based on the fields that are marked as dirty and * returns the same entity after a successful save or false in case * of any error. * * Triggers the `Model.beforeSave` and `Model.afterSave` events. * * ## Options * * - `checkRules` Defaults to true. Check deletion rules before deleting the record. * * @param \Cake\Datasource\EntityInterface $entity The entity to be saved * @param array $options An array of options to be used for the event * @return \Cake\Datasource\EntityInterface|bool */ public function save(EntityInterface $entity, $options = []) { $options += ['checkRules' => true]; $options = new ArrayObject($options); $event = $this->dispatchEvent('Model.beforeSave', ['entity' => $entity, 'options' => $options]); if ($event->isStopped()) { return $event->result; } if ($entity->errors()) { return false; } $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) { return false; } $type = $this->connection()->getIndex()->getType($this->name()); $id = $entity->id ?: null; $data = $entity->toArray(); unset($data[$id]); $doc = new ElasticaDocument($id, $data); $doc->setAutoPopulate(true); $result = $type->addDocument($doc); $entity->id = $doc->getId(); $entity->_version = $doc->getVersion(); $entity->isNew(false); $entity->source($this->name()); $entity->clean(); $this->dispatchEvent('Model.afterSave', ['entity' => $entity, 'options' => $options]); return $entity; }
/** * Generates a list of words after each entity is saved. * * Triggers the following events: * * - `Model.beforeIndex`: Before entity gets indexed by the configured search * engine adapter. First argument is the entity instance being indexed. * * - `Model.afterIndex`: After entity was indexed by the configured search * engine adapter. First argument is the entity instance that was indexed, and * second indicates whether the indexing process completed correctly or not. * * @param \Cake\Event\Event $event The event that was triggered * @param \Cake\Datasource\EntityInterface $entity The entity that was saved * @param \ArrayObject $options Additional options * @return void */ public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options) { $isNew = $entity->isNew(); if ($this->config('on') === 'update' && $isNew || $this->config('on') === 'insert' && !$isNew || isset($options['index']) && $options['index'] === false) { return; } $this->_table->dispatchEvent('Model.beforeIndex', compact('entity')); $success = $this->searchEngine()->index($entity); $this->_table->dispatchEvent('Model.afterIndex', compact('entity', 'success')); }
/** * beforeSave callback. * * @param \Cake\Event\Event $event Event. * @param \Cake\Datasource\EntityInterface $entity Entity. * @return void */ public function beforeSave(Event $event, EntityInterface $entity) { $field = $this->_config['discriminatorField']; if ($entity->isNew() && !$entity->has($field)) { $discriminator = $this->discriminator(); $entity->set($field, $discriminator); } }
/** * {@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; }
/** * Creates changelog report in string format. * * Example: * * Subject: changed from 'Foo' to 'Bar'. * Content: changed from 'Hello world' to 'Hi there'. * * @param \Cake\Datasource\EntityInterface $entity Entity instance * @return string */ protected function _getChangelog(EntityInterface $entity) { $result = ''; // plain changelog if entity is new if ($entity->isNew()) { return $result; } // get entity's modified fields $fields = $entity->extractOriginalChanged($entity->visibleProperties()); if (empty($fields)) { return $result; } // remove ignored fields foreach ($this->_ignoredFields as $field) { if (!array_key_exists($field, $fields)) { continue; } unset($fields[$field]); } if (empty($fields)) { return $result; } foreach ($fields as $k => $v) { $result .= sprintf(static::CHANGELOG, Inflector::humanize($k), $v, $entity->{$k}); } return $result; }
/** * Calculates the changes done to the entity and stores the audit log event object into the * log queue inside the `_auditQueue` key in $options. * * @param Cake\Event\Event The Model event that is enclosed inside a transaction * @param Cake\Datasource\EntityInterface $entity The entity that is to be saved * @param ArrayObject $options Options array containing the `_auditQueue` key * @return void */ public function afterSave(Event $event, EntityInterface $entity, $options) { if (!isset($options['_auditQueue'])) { return; } $config = $this->_config; if (empty($config['whitelist'])) { $config['whitelist'] = $this->_table->schema()->columns(); $config['whitelist'] = array_merge($config['whitelist'], $this->getAssociationProperties(array_keys($options['associated']))); } $config['whitelist'] = array_diff($config['whitelist'], $config['blacklist']); $changed = $entity->extract($config['whitelist'], true); if (!$changed) { return; } $original = $entity->extractOriginal(array_keys($changed)); $properties = $this->getAssociationProperties(array_keys($options['associated'])); foreach ($properties as $property) { unset($changed[$property], $original[$property]); } if (!$changed || $original === $changed && !$entity->isNew()) { return; } $primary = $entity->extract((array) $this->_table->primaryKey()); $auditEvent = $entity->isNew() ? AuditCreateEvent::class : AuditUpdateEvent::class; $transaction = $options['_auditTransaction']; $auditEvent = new $auditEvent($transaction, $primary, $this->_table->table(), $changed, $original); if (!empty($options['_sourceTable'])) { $auditEvent->setParentSourceName($options['_sourceTable']->table()); } $options['_auditQueue']->attach($entity, $auditEvent); }
/** * Perform the delete operation. * * Will delete the entity provided. Will remove rows from any * dependent associations, and clear out join tables for BelongsToMany associations. * * @param \Cake\DataSource\EntityInterface $entity The entity to delete. * @param \ArrayObject $options The options for the delete. * @throws \InvalidArgumentException if there are no primary key values of the * passed entity * @return bool success */ protected function _processDelete($entity, $options) { $eventManager = $this->getEventManager(); $event = new Event('Model.beforeDelete', $this, ['entity' => $entity, 'options' => $options]); $eventManager->dispatch($event); if ($event->isStopped()) { return $event->result; } if ($entity->isNew()) { return false; } $primaryKey = (array) $this->primaryKey(); $conditions = (array) $entity->extract($primaryKey); if (!array_filter($conditions, 'strlen')) { $msg = 'Deleting requires a primary key value'; throw new \InvalidArgumentException($msg); } $this->_associations->cascadeDelete($entity, $options->getArrayCopy()); $query = $this->query(); $statement = $query->delete()->where($conditions)->execute(); $success = $statement->rowCount() > 0; if (!$success) { return $success; } $event = new Event('Model.afterDelete', $this, ['entity' => $entity, 'options' => $options]); $eventManager->dispatch($event); return $success; }
/** * Merges `$data` into `$entity`. * * ### Options: * * * 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. * * @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); $isNew = $entity->isNew(); $keys = []; if (!$isNew) { $keys = $entity->extract((array) $this->_endpoint->primaryKey()); } if (isset($options['accessibleFields'])) { foreach ((array) $options['accessibleFields'] as $key => $value) { $entity->accessible($key, $value); } } $errors = $this->_validate($data + $keys, $options, $isNew); $properties = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { continue; } $properties[$key] = $value; } if (!isset($options['fieldList'])) { $entity->set($properties); $entity->errors($errors); return $entity; } foreach ((array) $options['fieldList'] as $field) { if (array_key_exists($field, $properties)) { $entity->set($field, $properties[$field]); } } $entity->errors($errors); return $entity; }
/** * _checkEntityBeforeSave * * @param \Cake\Datasource\EntityInterface $entity * @return void */ protected function _checkEntityBeforeSave(EntityInterface &$entity) { if ($entity->isNew()) { if (empty($entity->model)) { $entity->model = $this->table(); } if (empty($entity->adapter)) { $entity->adapter = $this->_defaultAdapter; } } }
/** * Merges `$data` into `$document`. * * ### Options: * * * fieldList: A whitelist of fields to be assigned to the entity. If not present * the accessible fields list in the entity will be used. * * @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); $errors = $this->_validate($data, $options, $entity->isNew()); $entity->errors($errors); foreach (array_keys($errors) as $badKey) { unset($data[$badKey]); } foreach ($this->type->embedded() as $embed) { $property = $embed->property(); if (in_array($embed->alias(), $options['associated']) && isset($data[$property])) { $data[$property] = $this->mergeNested($embed, $entity->{$property}, $data[$property]); } } if (!isset($options['fieldList'])) { $entity->set($data); return $entity; } foreach ((array) $options['fieldList'] as $field) { if (array_key_exists($field, $data)) { $entity->set($field, $data[$field]); } } return $entity; }
/** * 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|\ArrayObject $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 bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns * the saved entity * @see Table::save() * @see BelongsToMany::replaceLinks() */ public function saveAssociated(EntityInterface $entity, array $options = []) { $targetEntity = $entity->get($this->property()); $strategy = $this->saveStrategy(); $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; }
/** * 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); $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(); $options['isMerge'] = true; $propertyMap = $this->_buildPropertyMap($data, $options); $properties = $marshalledAssocs = []; foreach ($data as $key => $value) { if (!empty($errors[$key])) { if ($entity instanceof InvalidPropertyInterface) { $entity->invalid($key, $value); } continue; } $original = $entity->get($key); if (isset($propertyMap[$key])) { $value = $propertyMap[$key]($value, $entity); // Don't dirty scalar values and objects that didn't // change. Arrays will always be marked as dirty because // the original/updated list could contain references to the // same objects, even though those objects may have changed internally. if (is_scalar($value) && $original === $value || $value === null && $original === $value || is_object($value) && !$value instanceof EntityInterface && $original == $value) { continue; } } $properties[$key] = $value; } $entity->errors($errors); if (!isset($options['fieldList'])) { $entity->set($properties); foreach ($properties as $field => $value) { if ($value instanceof EntityInterface) { $entity->dirty($field, $value->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) { $entity->dirty($field, $properties[$field]->dirty()); } } } return $entity; }
/** * 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; }
/** * Persists an resource based on the fields that are marked as dirty and * returns the same resource after a successful save or false in case * of any error. * * @param \Cake\Datasource\EntityInterface $resource the resource to be saved * @param array|\ArrayAccess $options The options to use when saving. * * @return \Cake\Datasource\EntityInterface|bool */ public function save(EntityInterface $resource, $options = []) { $options = new ArrayObject($options + ['checkRules' => true]); $mode = $resource->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; if ($options['checkRules'] && !$this->checkRules($resource, $mode, $options)) { return false; } if ($resource->isNew()) { $query = $this->query()->create()->set($resource->toArray()); } else { $query = $this->query()->update()->where([$this->primaryKey() => $resource->get($this->primaryKey())]); $fieldsToUpdate = []; foreach ($resource as $field => $value) { if (!$resource->dirty($field)) { continue; } $fieldsToUpdate[$field] = $value; } $query->set($fieldsToUpdate); } $result = $query->execute(); if (!$result) { return false; } if ($resource->isNew() && $result instanceof EntityInterface) { return $result; } $className = get_class($resource); return new $className($resource->toArray(), ['markNew' => false, 'markClean' => true]); }
/** * After save listener. * * Manages updating level of descendents of currently saved entity. * * @param \Cake\Event\Event $event The beforeSave event that was fired * @param \Cake\Datasource\EntityInterface $entity the entity that is going to be saved * @return void */ public function afterSave(Event $event, EntityInterface $entity) { if (!$this->_config['level'] || $entity->isNew()) { return; } $this->_setChildrenLevel($entity); }
/** * Performs the actual saving of an entity based on the passed options. * * @param \Cake\Datasource\EntityInterface $entity the entity to be saved * @param \ArrayObject $options the options to use for the save operation * @return \Cake\Datasource\EntityInterface|bool * @throws \RuntimeException When an entity is missing some of the primary keys. */ protected function _processSave($entity, $options) { $primaryColumns = (array) $this->primaryKey(); if ($options['checkExisting'] && $primaryColumns && $entity->isNew() && $entity->has($primaryColumns)) { $alias = $this->alias(); $conditions = []; foreach ($entity->extract($primaryColumns) as $k => $v) { $conditions["{$alias}.{$k}"] = $v; } $entity->isNew(!$this->exists($conditions)); } $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) { return false; } $options['associated'] = $this->_associations->normalizeKeys($options['associated']); $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options')); if ($event->isStopped()) { return $event->result; } $saved = $this->_associations->saveParents($this, $entity, $options['associated'], ['_primary' => false] + $options->getArrayCopy()); if (!$saved && $options['atomic']) { return false; } $data = $entity->extract($this->schema()->columns(), true); $isNew = $entity->isNew(); if ($isNew) { $success = $this->_insert($entity, $data); } else { $success = $this->_update($entity, $data); } if ($success) { $success = $this->_associations->saveChildren($this, $entity, $options['associated'], ['_primary' => false] + $options->getArrayCopy()); if ($success || !$options['atomic']) { $entity->clean(); $this->dispatchEvent('Model.afterSave', compact('entity', 'options')); if (!$options['atomic'] && !$options['_primary']) { $entity->isNew(false); $entity->source($this->registryAlias()); } $success = true; } } if (!$success && $isNew) { $entity->unsetProperty($this->primaryKey()); $entity->isNew(true); } if ($success) { return $entity; } return false; }