/** * Performs the existence 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 (is_string($this->_repository)) { $this->_repository = $options['repository']->association($this->_repository); } $source = !empty($options['repository']) ? $options['repository'] : $this->_repository; $source = $source instanceof Association ? $source->source() : $source; $target = $this->_repository instanceof Association ? $this->_repository->target() : $this->_repository; if (!empty($options['_sourceTable']) && $target === $options['_sourceTable']) { return true; } if (!$entity->extract($this->_fields, true)) { return true; } $nulls = 0; $schema = $source->schema(); foreach ($this->_fields as $field) { if ($schema->column($field) && $schema->isNullable($field) && $entity->get($field) === null) { $nulls++; } } if ($nulls === count($this->_fields)) { return true; } $primary = array_map([$this->_repository, 'aliasField'], (array) $this->_repository->primaryKey()); $conditions = array_combine($primary, $entity->extract($this->_fields)); return $this->_repository->exists($conditions); }
/** * 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); }
/** * Performs the existence 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. * @throws \RuntimeException When the rule refers to an undefined association. * @return bool */ public function __invoke(EntityInterface $entity, array $options) { if (is_string($this->_repository)) { $repository = $options['repository']->association($this->_repository); if (!$repository) { throw new RuntimeException(sprintf("ExistsIn rule for '%s' is invalid. The '%s' association is not defined.", implode(', ', $this->_fields), $this->_repository)); } $this->_repository = $repository; } $source = $target = $this->_repository; if (!empty($options['repository'])) { $source = $options['repository']; } if ($source instanceof Association) { $source = $source->source(); } if ($target instanceof Association) { $bindingKey = (array) $target->bindingKey(); $target = $target->target(); } else { $bindingKey = (array) $target->primaryKey(); } if (!empty($options['_sourceTable']) && $target === $options['_sourceTable']) { return true; } if (!$entity->extract($this->_fields, true)) { return true; } if ($this->_fieldsAreNull($entity, $source)) { return true; } $primary = array_map([$target, 'aliasField'], $bindingKey); $conditions = array_combine($primary, $entity->extract($this->_fields)); return $target->exists($conditions); }
/** * Performs the existence 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 (is_string($this->_repository)) { $this->_repository = $options['repository']->association($this->_repository); } if (!empty($options['_sourceTable'])) { $source = $this->_repository instanceof Association ? $this->_repository->target() : $this->_repository; if ($source === $options['_sourceTable']) { return true; } } if (!$entity->extract($this->_fields, true)) { return true; } $conditions = array_combine((array) $this->_repository->primaryKey(), $entity->extract($this->_fields)); return $this->_repository->exists($conditions); }
/** * 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); } }
/** * Save also related model data * * @param \Cake\Event\Event * @param \Cake\ORM\Entity; * @return void */ public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $options) { $relatedEntities = []; foreach ($this->config('fields') as $field => $mapped) { list($mappedTable, $mappedField) = explode('.', $mapped); if (!isset($this->_table->{$mappedTable}) || $this->_table->{$mappedTable}->isOwningSide($this->_table)) { throw new Exception(sprintf('Incorrect definition of related data to persist for %s', $mapped)); } $foreignKeys = $entity->extract((array) $this->_table->{$mappedTable}->foreignKey()); $dirtyForeignKeys = $entity->extract((array) $this->_table->{$mappedTable}->foreignKey(), true); if (!empty($dirtyForeignKeys)) { // get related entity if (empty($relatedEntities[$mappedTable])) { $relatedEntities[$mappedTable] = $this->_table->{$mappedTable}->get($foreignKeys); } // set field value $entity->set($field, $relatedEntities[$mappedTable]->get($mappedField)); } } }
/** * Cascade a delete to remove dependent records. * * This method does nothing if the association is not dependent. * * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(EntityInterface $entity, array $options = []) { if (!$this->dependent()) { return true; } $table = $this->target(); $foreignKey = (array) $this->foreignKey(); $bindingKey = (array) $this->bindingKey(); $conditions = array_combine($foreignKey, $entity->extract($bindingKey)); if ($this->_cascadeCallbacks) { foreach ($this->find()->where($conditions)->toList() as $related) { $table->delete($related, $options); } return true; } $conditions = array_merge($conditions, $this->conditions()); return $table->deleteAll($conditions); }
/** * 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\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 * @return bool|\Cake\Datasource\EntityInterface 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 saveAssociated(EntityInterface $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; $options['_sourceTable'] = $this->source(); foreach ($targetEntities as $k => $targetEntity) { if (!$targetEntity instanceof EntityInterface) { 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; }
/** * Updates counter cache for a single association * * @param \Cake\Event\Event $event Event instance. * @param \Cake\Datasource\EntityInterface $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, EntityInterface $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->extractOriginalChanged($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); } } }
/** * 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; }
/** * Returns the list of joint entities that exist between the source entity * and each of the passed target entities * * @param \Cake\Datasource\EntityInterface $sourceEntity The row belonging to the source side * of this association. * @param array $targetEntities The rows belonging to the target side of this * association. * @throws \InvalidArgumentException if any of the entities is lacking a primary * key value * @return array */ 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) { if (!$entity instanceof EntityInterface) { continue; } $joint = $entity->get($jointProperty); if (!$joint || !$joint instanceof EntityInterface) { $missing[] = $entity->extract($primary); continue; } $result[] = $joint; } if (empty($missing)) { return $result; } $belongsTo = $junction->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()); }
/** * 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\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 * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns * the saved entity * @see Table::save() */ public function saveAssociated(EntityInterface $entity, array $options = []) { $targetEntity = $entity->get($this->property()); if (empty($targetEntity) || !$targetEntity instanceof EntityInterface) { return $entity; } $properties = array_combine((array) $this->foreignKey(), $entity->extract((array) $this->bindingKey())); $targetEntity->set($properties, ['guard' => false]); if (!$this->target()->save($targetEntity, $options)) { $targetEntity->unsetProperty(array_keys($properties)); return false; } return $entity; }
/** * 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; }
/** * Persists all audit log events stored in the `_eventQueue` key inside $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 or deleted * @param ArrayObject $options Options array containing the `_auditQueue` key * @return void */ public function afterDelete(Event $event, EntityInterface $entity, $options) { if (!isset($options['_auditQueue'])) { return; } $transaction = $options['_auditTransaction']; $parent = isset($options['_sourceTable']) ? $options['_sourceTable']->table() : null; $primary = $entity->extract((array) $this->_table->primaryKey()); $auditEvent = new AuditDeleteEvent($transaction, $primary, $this->_table->table(), $parent); $options['_auditQueue']->attach($entity, $auditEvent); }
/** * Returns an array with foreignKey value. * * @param \Cake\Datasource\EntityInterface $entity Entity. * @return array */ protected function _extractForeignKey($entity) { $foreignKey = (array) $this->_config['foreignKey']; $primaryKey = (array) $this->_table->primaryKey(); $pkValue = $entity->extract($primaryKey); return array_combine($foreignKey, $pkValue); }
/** * 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\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 * @return bool|\Cake\Datasource\EntityInterface 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 saveAssociated(EntityInterface $entity, array $options = []) { $targetEntities = $entity->get($this->property()); if (empty($targetEntities) && $this->_saveStrategy !== self::SAVE_REPLACE) { 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); } $foreignKey = (array) $this->foreignKey(); $properties = array_combine($foreignKey, $entity->extract((array) $this->bindingKey())); $target = $this->target(); $original = $targetEntities; $options['_sourceTable'] = $this->source(); $unlinkSuccessful = null; if ($this->_saveStrategy === self::SAVE_REPLACE) { $unlinkSuccessful = $this->_unlinkAssociated($properties, $entity, $target, $targetEntities, $options); } if ($unlinkSuccessful === false) { return false; } foreach ($targetEntities as $k => $targetEntity) { if (!$targetEntity instanceof EntityInterface) { break; } if (!empty($options['atomic'])) { $targetEntity = clone $targetEntity; } if ($properties !== $targetEntity->extract($foreignKey)) { $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; }
/** * Auxiliary function to handle the update of an entity's data in the table * * @param \Cake\Datasource\EntityInterface $entity the subject entity from were $data was extracted * @param array $data The actual data that needs to be saved * @return \Cake\Datasource\EntityInterface|bool * @throws \InvalidArgumentException When primary key data is missing. */ protected function _update($entity, $data) { $primaryColumns = (array) $this->primaryKey(); $primaryKey = $entity->extract($primaryColumns); $data = array_diff_key($data, $primaryKey); if (empty($data)) { return $entity; } if (!$entity->has($primaryColumns)) { $message = 'All primary key value(s) are needed for updating'; throw new InvalidArgumentException($message); } $query = $this->query(); $statement = $query->update()->set($data)->where($primaryKey)->execute(); $success = false; if ($statement->errorCode() === '00000') { $success = $entity; } $statement->closeCursor(); return $success; }
public function __invoke(EntityInterface $row) { $primaryKey = array_values($row->extract((array) $this->table->primaryKey())); $row->_links = ['self' => ['href' => Router::url(['controller' => $row->source(), 'action' => 'view'] + $primaryKey)]]; return $this->enrich($row); }
/** * 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; }
/** * Performs the existence 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. * @throws \RuntimeException When the rule refers to an undefined association. * @return bool */ public function __invoke(EntityInterface $entity, array $options) { if (is_string($this->_repository)) { $repository = $options['repository']->association($this->_repository); if (!$repository) { throw new RuntimeException(sprintf("ExistsIn rule for '%s' is invalid. '%s' is not associated with '%s'.", implode(', ', $this->_fields), $this->_repository, get_class($options['repository']))); } $this->_repository = $repository; } $source = $target = $this->_repository; $isAssociation = $target instanceof Association; $bindingKey = $isAssociation ? (array) $target->bindingKey() : (array) $target->primaryKey(); $realTarget = $isAssociation ? $target->target() : $target; if (!empty($options['_sourceTable']) && $realTarget === $options['_sourceTable']) { return true; } if (!empty($options['repository'])) { $source = $options['repository']; } if ($source instanceof Association) { $source = $source->source(); } if (!$entity->extract($this->_fields, true)) { return true; } if ($this->_fieldsAreNull($entity, $source)) { return true; } if ($this->_options['allowNullableNulls']) { $schema = $source->schema(); foreach ($this->_fields as $i => $field) { if ($schema->column($field) && $schema->isNullable($field) && $entity->get($field) === null) { unset($bindingKey[$i]); unset($this->_fields[$i]); } } } $primary = array_map([$target, 'aliasField'], $bindingKey); $conditions = array_combine($primary, $entity->extract($this->_fields)); return $target->exists($conditions); }
/** * 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); } }
/** * 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; }