diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 83c1834..0fc6035 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -91,7 +91,7 @@ protected function doLoadFieldItems($entities, $age) { /** * {@inheritdoc} */ - protected function doSaveFieldItems(EntityInterface $entity, $update) { + protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { } /** diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 25d4a5a..3ce4802 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -158,6 +158,67 @@ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batc public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { } /** + * {@inheritdoc} + */ + protected function doSave($id, EntityInterface $entity) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + + $is_new = $entity->isNew(); + if (!$is_new) { + // @todo, should a different value be returned when saving an entity with + // $isDefaultRevision = FALSE? + $return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE; + } + else { + // Ensure the entity is still seen as new after assigning it an id, while + // storing its data. + $entity->enforceIsNew(); + if ($this->entityType->isRevisionable()) { + $entity->setNewRevision(); + } + $return = SAVED_NEW; + } + + $this->doSaveFieldItems($entity); + + return $return; + } + + /** + * Writes entity field values to the storage. + * + * This method is responsible for allocating entity and revision identifiers + * and update the entity object with their values. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity object. + * @param string[] $names + * (optional) The name of the fields to be written to the storage. If an + * empty value is passed all field values are saved. + * + * @api + */ + abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []); + + /** + * {@inheritdoc} + */ + protected function doPostSave(EntityInterface $entity, $update) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + + if ($update && $this->entityType->isTranslatable()) { + $this->invokeTranslationHooks($entity); + } + + parent::doPostSave($entity, $update); + + // The revision is stored, it should no longer be marked as new now. + if ($this->entityType->isRevisionable()) { + $entity->setNewRevision(FALSE); + } + } + + /** * Checks translation statuses and invoke the related hooks if needed. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity @@ -183,27 +244,71 @@ protected function invokeTranslationHooks(ContentEntityInterface $entity) { * {@inheritdoc} */ protected function invokeHook($hook, EntityInterface $entity) { - if ($hook == 'presave') { - $this->invokeFieldMethod('preSave', $entity); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + + switch ($hook) { + case 'presave': + $this->invokeFieldMethod('preSave', $entity); + break; + + case 'insert': + $this->invokeFieldPostSave($entity, FALSE); + break; + + case 'update': + $this->invokeFieldPostSave($entity, TRUE); + break; } + parent::invokeHook($hook, $entity); } /** * Invokes a method on the Field objects within an entity. * + * Any argument passed will be forwarded to the invoked method. + * * @param string $method - * The method name. + * The name of the method to be invoked. * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity object. + * + * @return array + * A multidimensional associative array of results, keyed by entity + * translation language code and field name. */ protected function invokeFieldMethod($method, ContentEntityInterface $entity) { + $result = []; + $args = array_slice(func_get_args(), 2); foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { $translation = $entity->getTranslation($langcode); - foreach ($translation->getFields() as $field) { - $field->$method(); + foreach ($translation->getFields() as $name => $items) { + // call_user_func_array is way slower than a direct call so we avoid + // using it if have no parameters. + $result[$langcode][$name] = $args ? call_user_func_array([$items, $method], $args) : $items->{$method}(); } } + return $result; + } + + /** + * Invokes the post save method on the Field objects within an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity object. + * @param bool $update + * Specifies whether the entity is being updated or created. + */ + protected function invokeFieldPostSave(ContentEntityInterface $entity, $update) { + // For each entity translation this returns an array of resave flags keyed + // by field name, thus we merge them to obtain a list of fields to resave. + $resave = []; + foreach ($this->invokeFieldMethod('postSave', $entity, $update) as $results) { + $resave += array_filter($results); + } + if ($resave) { + $this->doSaveFieldItems($entity, array_keys($resave)); + } } /** diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 55d5317..61b6511 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -382,6 +382,34 @@ public function delete(array $entities) { * {@inheritdoc} */ public function save(EntityInterface $entity) { + // Track if this entity is new. + $is_new = $entity->isNew(); + + // Execute presave logic and invoke the related hooks. + $id = $this->doPreSave($entity); + + // Perform the save and reset the static cache for the changed entity. + $return = $this->doSave($id, $entity); + + // Execute post save logic and invoke the related hooks. + $this->doPostSave($entity, !$is_new); + + return $return; + } + + /** + * Performs presave entity processing. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The saved entity. + * + * @return int + * The processed entity identifier. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * If the entity identifier is invalid. + */ + protected function doPreSave(EntityInterface $entity) { $id = $entity->id(); // Track the original ID. @@ -389,13 +417,11 @@ public function save(EntityInterface $entity) { $id = $entity->getOriginalId(); } - // Track if this entity is new. - $is_new = $entity->isNew(); // Track if this entity exists already. $id_exists = $this->has($id, $entity); // A new entity should not already exist. - if ($id_exists && $is_new) { + if ($id_exists && $entity->isNew()) { throw new EntityStorageException(SafeMarkup::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id))); } @@ -408,25 +434,7 @@ public function save(EntityInterface $entity) { $entity->preSave($this); $this->invokeHook('presave', $entity); - // Perform the save and reset the static cache for the changed entity. - $return = $this->doSave($id, $entity); - $this->resetCache(array($id)); - - // The entity is no longer new. - $entity->enforceIsNew(FALSE); - - // Allow code to run after saving. - $entity->postSave($this, !$is_new); - $this->invokeHook($is_new ? 'insert' : 'update', $entity); - - // After saving, this is now the "original entity", and subsequent saves - // will be updates instead of inserts, and updates must always be able to - // correctly identify the original entity. - $entity->setOriginalId($entity->id()); - - unset($entity->original); - - return $return; + return $id; } /** @@ -444,6 +452,32 @@ public function save(EntityInterface $entity) { abstract protected function doSave($id, EntityInterface $entity); /** + * Performs post save entity processing. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The saved entity. + * @param $update + * Specifies whether the entity is being updated or created. + */ + protected function doPostSave(EntityInterface $entity, $update) { + $this->resetCache(array($entity->id())); + + // The entity is no longer new. + $entity->enforceIsNew(FALSE); + + // Allow code to run after saving. + $entity->postSave($this, $update); + $this->invokeHook($update ? 'update' : 'insert', $entity); + + // After saving, this is now the "original entity", and subsequent saves + // will be updates instead of inserts, and updates must always be able to + // correctly identify the original entity. + $entity->setOriginalId($entity->id()); + + unset($entity->original); + } + + /** * Builds an entity query. * * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index c780267..07456aa 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -934,73 +934,97 @@ public function save(EntityInterface $entity) { /** * {@inheritdoc} */ - protected function doSave($id, EntityInterface $entity) { - // Create the storage record to be saved. - $record = $this->mapToStorageRecord($entity); + protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { + $full_save = empty($names); + $update = !$full_save || !$entity->isNew(); - $is_new = $entity->isNew(); - if (!$is_new) { - if ($entity->isDefaultRevision()) { - $this->database - ->update($this->baseTable) - ->fields((array) $record) - ->condition($this->idKey, $record->{$this->idKey}) - ->execute(); - $return = SAVED_UPDATED; - } - else { - // @todo, should a different value be returned when saving an entity - // with $isDefaultRevision = FALSE? - $return = FALSE; - } - if ($this->revisionTable) { - $entity->{$this->revisionKey}->value = $this->saveRevision($entity); - } - if ($this->dataTable) { - $this->saveToSharedTables($entity); - } - if ($this->revisionDataTable) { - $this->saveToSharedTables($entity, $this->revisionDataTable); - } + if ($full_save) { + $shared_table_fields = TRUE; + $dedicated_table_fields = TRUE; } else { - // Ensure the entity is still seen as new after assigning it an id, - // while storing its data. - $entity->enforceIsNew(); - $insert_id = $this->database - ->insert($this->baseTable, array('return' => Database::RETURN_INSERT_ID)) - ->fields((array) $record) - ->execute(); - // Even if this is a new entity the ID key might have been set, in which - // case we should not override the provided ID. An ID key that is not set - // to any value is interpreted as NULL (or DEFAULT) and thus overridden. - if (!isset($record->{$this->idKey})) { - $record->{$this->idKey} = $insert_id; - } - $return = SAVED_NEW; - $entity->{$this->idKey}->value = (string) $record->{$this->idKey}; - if ($this->revisionTable) { - $entity->setNewRevision(); - $record->{$this->revisionKey} = $this->saveRevision($entity); + $table_mapping = $this->getTableMapping(); + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId); + $shared_table_fields = FALSE; + $dedicated_table_fields = []; + + // Collect the name of fields to be written in dedicated tables and check + // whether shared table records need to be updated. + foreach ($names as $name) { + $storage_definition = $storage_definitions[$name]; + if ($table_mapping->allowsSharedTableStorage($storage_definition)) { + $shared_table_fields = TRUE; + } + elseif ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + $dedicated_table_fields[] = $name; + } } - if ($this->dataTable) { - $this->saveToSharedTables($entity); + } + + // Update shared table records if necessary. + if ($shared_table_fields) { + $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); + // Create the storage record to be saved. + if ($update) { + $default_revision = $entity->isDefaultRevision(); + if ($default_revision) { + $this->database + ->update($this->baseTable) + ->fields((array) $record) + ->condition($this->idKey, $record->{$this->idKey}) + ->execute(); + } + if ($this->revisionTable) { + if ($full_save) { + $entity->{$this->revisionKey} = $this->saveRevision($entity); + } + else { + $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable); + $entity->preSaveRevision($this, $record); + $this->database + ->update($this->revisionTable) + ->fields((array) $record) + ->condition($this->revisionKey, $record->{$this->revisionKey}) + ->execute(); + } + } + if ($default_revision && $this->dataTable) { + $this->saveToSharedTables($entity); + } + if ($this->revisionDataTable) { + $new_revision = $full_save ? $entity->isNewRevision() : FALSE; + $this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision); + } } - if ($this->revisionDataTable) { - $this->saveToSharedTables($entity, $this->revisionDataTable); + else { + $insert_id = $this->database + ->insert($this->baseTable, array('return' => Database::RETURN_INSERT_ID)) + ->fields((array) $record) + ->execute(); + // Even if this is a new entity the ID key might have been set, in which + // case we should not override the provided ID. An ID key that is not set + // to any value is interpreted as NULL (or DEFAULT) and thus overridden. + if (!isset($record->{$this->idKey})) { + $record->{$this->idKey} = $insert_id; + } + $entity->{$this->idKey} = (string) $record->{$this->idKey}; + if ($this->revisionTable) { + $record->{$this->revisionKey} = $this->saveRevision($entity); + } + if ($this->dataTable) { + $this->saveToSharedTables($entity); + } + if ($this->revisionDataTable) { + $this->saveToSharedTables($entity, $this->revisionDataTable); + } } } - $this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity); - $this->saveToDedicatedTables($entity, !$is_new); - if (!$is_new && $this->dataTable) { - $this->invokeTranslationHooks($entity); + // Update dedicated table records if necessary. + if ($dedicated_table_fields) { + $names = is_array($dedicated_table_fields) ? $dedicated_table_fields : []; + $this->saveToDedicatedTables($entity, $update, $names); } - $entity->enforceIsNew(FALSE); - if ($this->revisionTable) { - $entity->setNewRevision(FALSE); - } - return $return; } /** @@ -1017,14 +1041,20 @@ protected function has($id, EntityInterface $entity) { * The entity object. * @param string $table_name * (optional) The table name to save to. Defaults to the data table. + * @param bool $new_revision + * (optional) Whether we are dealing with a new revision. By default fetches + * the information from the entity object. */ - protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL) { + protected function saveToSharedTables(ContentEntityInterface $entity, $table_name = NULL, $new_revision = NULL) { if (!isset($table_name)) { $table_name = $this->dataTable; } + if (!isset($new_revision)) { + $new_revision = $entity->isNewRevision(); + } $revision = $table_name != $this->dataTable; - if (!$revision || !$entity->isNewRevision()) { + if (!$revision || !$new_revision) { $key = $revision ? $this->revisionKey : $this->idKey; $value = $revision ? $entity->getRevisionId() : $entity->id(); // Delete and insert to handle removed values. @@ -1301,8 +1331,11 @@ protected function loadFromDedicatedTables(array &$values, $load_from_revision) * The entity. * @param bool $update * TRUE if the entity is being updated, FALSE if it is being inserted. + * @param string[] $names + * (optional) The names of the fields to be stored. Defaults to all the + * available fields. */ - protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE) { + protected function saveToDedicatedTables(ContentEntityInterface $entity, $update = TRUE, $names = array()) { $vid = $entity->getRevisionId(); $id = $entity->id(); $bundle = $entity->bundle(); @@ -1317,7 +1350,13 @@ protected function saveToDedicatedTables(ContentEntityInterface $entity, $update $original = !empty($entity->original) ? $entity->original: NULL; - foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) { + // Determine which fields should be actually stored. + $definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle); + if ($names) { + $definitions = array_intersect_key($definitions, array_flip($names)); + } + + foreach ($definitions as $field_name => $field_definition) { $storage_definition = $field_definition->getFieldStorageDefinition(); if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) { continue; diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php index fd521a9..8e50c6a 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -201,12 +201,7 @@ public function preSave() { } /** * {@inheritdoc} */ - public function insert() { } - - /** - * {@inheritdoc} - */ - public function update() { } + public function postSave($update) { } /** * {@inheritdoc} diff --git a/core/lib/Drupal/Core/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Field/FieldItemInterface.php index a2339ac..c6b94f0 100644 --- a/core/lib/Drupal/Core/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemInterface.php @@ -183,26 +183,39 @@ public function view($display_options = array()); /** * Defines custom presave behavior for field values. * - * This method is called before insert() and update() methods, and before - * values are written into storage. + * This method is called during the process of saving an entity, just before + * values are written into storage. When storing a new entity, its identifier + * will not be available yet. This should be used to massage item property + * values or perform any other operation that needs to happen before values + * are stored. For instance this is the proper phase to auto-create a new + * entity for an entity reference field item, because this way it will be + * possible to store the referenced entity identifier. */ public function preSave(); /** - * Defines custom insert behavior for field values. + * Defines custom post-save behavior for field values. * - * This method is called during the process of inserting an entity, just - * before values are written into storage. - */ - public function insert(); - - /** - * Defines custom update behavior for field values. + * This method is called during the process of saving an entity, just after + * values are written into storage. This is useful mostly when the business + * logic to be implemented always requires the entity identifier, even when + * storing a new entity. For instance, when implementing circular entity + * references, the referenced entity will be created on pre-save with a dummy + * value for the referring entity identifier, which will be updated with the + * actual one on post-save. * - * This method is called during the process of updating an entity, just before - * values are written into storage. + * In the rare cases where item properties depend on the entity identifier, + * massaging logic will have to be implemented on post-save and returning TRUE + * will allow them to be rewritten to the storage with the updated values. + * + * @param bool $update + * Specifies whether the entity is being updated or created. + * + * @return bool + * Whether field items should be rewritten to the storage as a consequence + * of the logic implemented by the custom behavior. */ - public function update(); + public function postSave($update); /** * Defines custom delete behavior for field values. diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php index aa10c6f..5605f11 100644 --- a/core/lib/Drupal/Core/Field/FieldItemList.php +++ b/core/lib/Drupal/Core/Field/FieldItemList.php @@ -7,14 +7,12 @@ namespace Drupal\Core\Field; -use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\Plugin\DataType\ItemList; -use Drupal\Core\TypedData\TypedDataInterface; /** * Represents an entity field; that is, a list of field item objects. @@ -212,15 +210,9 @@ public function preSave() { /** * {@inheritdoc} */ - public function insert() { - $this->delegateMethod('insert'); - } - - /** - * {@inheritdoc} - */ - public function update() { - $this->delegateMethod('update'); + public function postSave($update) { + $result = $this->delegateMethod('postSave', $update); + return (bool) array_filter($result); } /** @@ -240,13 +232,23 @@ public function deleteRevision() { /** * Calls a method on each FieldItem. * + * Any argument passed will be forwarded to the invoked method. + * * @param string $method - * The name of the method. + * The name of the method to be invoked. + * + * @return array + * An array of results keyed by delta. */ protected function delegateMethod($method) { - foreach ($this->list as $item) { - $item->{$method}(); + $result = []; + $args = array_slice(func_get_args(), 1); + foreach ($this->list as $delta => $item) { + // call_user_func_array is way slower than a direct call so we avoid using + // it if have no parameters. + $result[$delta] = $args ? call_user_func_array([$item, $method], $args) : $item->{$method}(); } + return $result; } /** diff --git a/core/lib/Drupal/Core/Field/FieldItemListInterface.php b/core/lib/Drupal/Core/Field/FieldItemListInterface.php index e4ea12c..df595e5 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -130,26 +130,29 @@ public function __unset($property_name); /** * Defines custom presave behavior for field values. * - * This method is called before either insert() or update() methods, and - * before values are written into storage. + * This method is called during the process of saving an entity, just before + * item values are written into storage. + * + * @see \Drupal\Core\Field\FieldItemInterface::preSave() */ public function preSave(); /** - * Defines custom insert behavior for field values. + * Defines custom post-save behavior for field values. * - * This method is called after the save() method, and before values are - * written into storage. - */ - public function insert(); - - /** - * Defines custom update behavior for field values. + * This method is called during the process of saving an entity, just after + * item values are written into storage. + * + * @param bool $update + * Specifies whether the entity is being updated or created. + * + * @return bool + * Whether field items should be rewritten to the storage as a consequence + * of the logic implemented by the custom behavior. * - * This method is called after the save() method, and before values are - * written into storage. + * @see \Drupal\Core\Field\FieldItemInterface::postSave() */ - public function update(); + public function postSave($update); /** * Defines custom delete behavior for field values. diff --git a/core/modules/block_content/tests/modules/block_content_test/block_content_test.module b/core/modules/block_content/tests/modules/block_content_test/block_content_test.module index d714cb2..3450b5f 100644 --- a/core/modules/block_content/tests/modules/block_content_test/block_content_test.module +++ b/core/modules/block_content/tests/modules/block_content_test/block_content_test.module @@ -60,6 +60,7 @@ function block_content_test_block_content_insert(BlockContent $block_content) { // Set the block_content title to the block_content ID and save. if ($block_content->label() == 'new') { $block_content->setInfo('BlockContent ' . $block_content->id()); + $block_content->setNewRevision(FALSE); $block_content->save(); } if ($block_content->label() == 'fail_creation') { diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php index f8ee03c..8b0aac1 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php @@ -23,59 +23,54 @@ public function defaultValuesForm(array &$form, FormStateInterface $form_state) /** * {@inheritdoc} */ - public function insert() { - parent::insert(); + public function postSave($update) { $entity = $this->getEntity(); - // Add a new usage for newly uploaded files. - foreach ($this->referencedEntities() as $file) { - \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); - } - } - - /** - * {@inheritdoc} - */ - public function update() { - parent::update(); - $entity = $this->getEntity(); - - // Get current target file entities and file IDs. - $files = $this->referencedEntities(); - $fids = array(); - - foreach ($files as $file) { - $fids[] = $file->id(); + if (!$update) { + // Add a new usage for newly uploaded files. + foreach ($this->referencedEntities() as $file) { + \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); + } } + else { + // Get current target file entities and file IDs. + $files = $this->referencedEntities(); + $ids = array(); - // On new revisions, all files are considered to be a new usage and no - // deletion of previous file usages are necessary. - if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) { + /** @var \Drupal\file\Entity\File $file */ foreach ($files as $file) { - \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); + $ids[] = $file->id(); } - return; - } - // Get the file IDs attached to the field before this update. - $field_name = $this->getFieldDefinition()->getName(); - $original_fids = array(); - $original_items = $entity->original->getTranslation($this->getLangcode())->$field_name; - foreach ($original_items as $item) { - $original_fids[] = $item->target_id; - } + // On new revisions, all files are considered to be a new usage and no + // deletion of previous file usages are necessary. + if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) { + foreach ($files as $file) { + \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); + } + return; + } - // Decrement file usage by 1 for files that were removed from the field. - $removed_fids = array_filter(array_diff($original_fids, $fids)); - $removed_files = \Drupal::entityManager()->getStorage('file')->loadMultiple($removed_fids); - foreach ($removed_files as $file) { - \Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id()); - } + // Get the file IDs attached to the field before this update. + $field_name = $this->getFieldDefinition()->getName(); + $original_ids = array(); + $original_items = $entity->original->getTranslation($this->getLangcode())->$field_name; + foreach ($original_items as $item) { + $original_ids[] = $item->target_id; + } - // Add new usage entries for newly added files. - foreach ($files as $file) { - if (!in_array($file->id(), $original_fids)) { - \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); + // Decrement file usage by 1 for files that were removed from the field. + $removed_ids = array_filter(array_diff($original_ids, $ids)); + $removed_files = \Drupal::entityManager()->getStorage('file')->loadMultiple($removed_ids); + foreach ($removed_files as $file) { + \Drupal::service('file.usage')->delete($file, 'file', $entity->getEntityTypeId(), $entity->id()); + } + + // Add new usage entries for newly added files. + foreach ($files as $file) { + if (!in_array($file->id(), $original_ids)) { + \Drupal::service('file.usage')->add($file, 'file', $entity->getEntityTypeId(), $entity->id()); + } } } } diff --git a/core/modules/node/tests/modules/node_test/node_test.module b/core/modules/node/tests/modules/node_test/node_test.module index ed9af44..7f2cd53 100644 --- a/core/modules/node/tests/modules/node_test/node_test.module +++ b/core/modules/node/tests/modules/node_test/node_test.module @@ -170,6 +170,7 @@ function node_test_node_insert(NodeInterface $node) { // Set the node title to the node ID and save. if ($node->getTitle() == 'new') { $node->setTitle('Node '. $node->id()); + $node->setNewRevision(FALSE); $node->save(); } } diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index c06271d..4bfe0d0 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -55,28 +55,25 @@ public function preSave() { /** * {@inheritdoc} */ - public function insert() { - if ($this->alias) { - $entity = $this->getEntity(); - - if ($path = \Drupal::service('path.alias_storage')->save($entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) { - $this->pid = $path['pid']; + public function postSave($update) { + if (!$update) { + if ($this->alias) { + $entity = $this->getEntity(); + if ($path = \Drupal::service('path.alias_storage')->save($entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) { + $this->pid = $path['pid']; + } } } - } - - /** - * {@inheritdoc} - */ - public function update() { - // Delete old alias if user erased it. - if ($this->pid && !$this->alias) { - \Drupal::service('path.alias_storage')->delete(array('pid' => $this->pid)); - } - // Only save a non-empty alias. - elseif ($this->alias) { - $entity = $this->getEntity(); - \Drupal::service('path.alias_storage')->save($entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode(), $this->pid); + else { + // Delete old alias if user erased it. + if ($this->pid && !$this->alias) { + \Drupal::service('path.alias_storage')->delete(array('pid' => $this->pid)); + } + // Only save a non-empty alias. + elseif ($this->alias) { + $entity = $this->getEntity(); + \Drupal::service('path.alias_storage')->save($entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode(), $this->pid); + } } } diff --git a/core/modules/system/src/Tests/Field/FieldItemTest.php b/core/modules/system/src/Tests/Field/FieldItemTest.php new file mode 100644 index 0000000..85f7736 --- /dev/null +++ b/core/modules/system/src/Tests/Field/FieldItemTest.php @@ -0,0 +1,110 @@ +container->get('state')->set('entity_test.field_test_item', TRUE); + $this->entityManager->clearCachedDefinitions(); + + $entity_type_id = 'entity_test_mulrev'; + $this->installEntitySchema($entity_type_id); + + $this->fieldName = Unicode::strtolower($this->randomMachineName()); + + /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */ + FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'type' => 'field_test', + 'entity_type' => $entity_type_id, + 'cardinality' => 1, + ])->save(); + + FieldConfig::create([ + 'entity_type' => $entity_type_id, + 'field_name' => $this->fieldName, + 'bundle' => $entity_type_id, + 'label' => 'Test field', + ])->save(); + + $this->entityManager->clearCachedDefinitions(); + $definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $this->assertTrue(!empty($definitions[$this->fieldName])); + } + + /** + * Tests the field item save workflow. + */ + public function testSaveWorkflow() { + $entity = EntityTestMulRev::create([ + 'name' => $this->randomString(), + 'field_test_item' => $this->randomString(), + $this->fieldName => $this->randomString(), + ]); + + // Save a new entity and verify that the initial field value is overwritten + // with a value containing the entity id, which implies a resave. Check that + // the entity data structure and the stored values match. + $this->assertSavedFieldItemValue($entity, "field_test:{$this->fieldName}:1:1"); + + // Update the entity and verify that the field value is overwritten on + // presave if it is not resaved. + $this->assertSavedFieldItemValue($entity, 'overwritten'); + + // Flag the field value as needing to be resaved and verify it actually is. + $entity->field_test_item->value = $entity->{$this->fieldName}->value = 'resave'; + $this->assertSavedFieldItemValue($entity, "field_test:{$this->fieldName}:1:3"); + } + + /** + * Checks that the saved field item value matches the expected one. + * + * @param \Drupal\entity_test\Entity\EntityTest $entity + * The test entity. + * @param $expected_value + * The expected field item value. + * + * @return bool + * TRUE if the item value matches expectations, FALSE otherwise. + */ + protected function assertSavedFieldItemValue(EntityTest $entity, $expected_value) { + $entity->setNewRevision(TRUE); + $entity->save(); + $base_field_expected_value = str_replace($this->fieldName, 'field_test_item', $expected_value); + $result = $this->assertEqual($entity->field_test_item->value, $base_field_expected_value); + $result = $result && $this->assertEqual($entity->{$this->fieldName}->value, $expected_value); + $entity = $this->reloadEntity($entity); + $result = $result && $this->assertEqual($entity->field_test_item->value, $base_field_expected_value); + $result = $result && $this->assertEqual($entity->{$this->fieldName}->value, $expected_value); + return $result; + } + +} diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index dd44c1f..28f1e99 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; @@ -98,6 +99,23 @@ function entity_test_entity_type_alter(array &$entity_types) { } /** + * Implements hook_entity_base_field_info(). + */ +function entity_test_entity_base_field_info(EntityTypeInterface $entity_type) { + $fields = []; + + if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.field_test_item')) { + $fields['field_test_item'] = BaseFieldDefinition::create('field_test') + ->setLabel(t('Field test')) + ->setDescription(t('A field test.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE); + } + + return $fields; +} + +/** * Implements hook_entity_base_field_info_alter(). */ function entity_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php new file mode 100644 index 0000000..582ea94 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/FieldTestItem.php @@ -0,0 +1,118 @@ +setLabel(new TranslationWrapper('Test value')) + ->setRequired(TRUE); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function __construct(DataDefinitionInterface $definition, $name = NULL, TypedDataInterface $parent = NULL) { + parent::__construct($definition, $name, $parent); + + $name = $this->getFieldDefinition()->getName(); + static::$counter[$name] = 0; + } + + /** + * {@inheritdoc} + */ + public function preSave() { + $name = $this->getFieldDefinition()->getName(); + static::$counter[$name]++; + + // Overwrite the field value unless it is going to be overridden, in which + // case its final value will already be different from the current one. + if (!$this->getEntity()->isNew() && !$this->mustResave()) { + $this->setValue('overwritten'); + } + } + + /** + * {@inheritdoc} + */ + public function postSave($update) { + // Determine whether the field value should be rewritten to the storage. We + // always rewrite on create as we need to store a value including the entity + // id. + $resave = !$update || $this->mustResave(); + + if ($resave) { + $entity = $this->getEntity(); + $definition = $this->getFieldDefinition(); + $name = $definition->getName(); + $value = 'field_test:' . $name . ':' . $entity->id() . ':' . static::$counter[$name]; + $this->setValue($value); + } + + return $resave; + } + + /** + * Checks whether the field item value should be resaved. + * + * @return bool + * TRUE if the item should be resaved, FALSE otherwise. + */ + protected function mustResave() { + return $this->getValue()['value'] == 'resave'; + } + +}