diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 83c1834..16a2eac 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..4d08a79 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -158,6 +159,23 @@ public function purgeFieldData(FieldDefinitionInterface $field_definition, $batc public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { } /** + * {@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); + + if ($this->entityType->isRevisionable()) { + $entity->setNewRevision(FALSE); + } + } + + /** * Checks translation statuses and invoke the related hooks if needed. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity @@ -183,30 +201,86 @@ 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. * - * @param string $method - * The method name. + * @param string|callable $method + * The name of the method to be invoked or a callable to which processed + * items will be passed. * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity object. */ protected function invokeFieldMethod($method, ContentEntityInterface $entity) { + $call = is_callable($method); foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { $translation = $entity->getTranslation($langcode); - foreach ($translation->getFields() as $field) { - $field->$method(); + foreach ($translation->getFields() as $name => $items) { + if ($call) { + call_user_func($method, $items, $name, $langcode); + } + else { + $items->$method(); + } } } } /** + * 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) { + $fields = []; + + $this->invokeFieldMethod( + function (FieldItemListInterface $items, $name) use (&$fields, $update) { + if ($items->postSave($update)) { + $fields[$name] = $name; + } + }, + $entity + ); + + + if ($fields) { + $this->doSaveFieldItems($entity, $fields); + } + } + + /** + * Writes entity field values to the storage. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity object. + * @param string[] $names + * The name of the fields to be written. + */ + abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names); + + /** * Checks whether the field values changed compared to the original entity. * * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index 55d5317..280adc7 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -410,21 +410,9 @@ public function save(EntityInterface $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); + // Execute post save logic and invoke the related hooks. + $this->doPostSave($entity, !$is_new); return $return; } @@ -444,6 +432,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 ? '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); + } + + /** * 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..bd09ea1 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -990,17 +990,64 @@ protected function doSave($id, EntityInterface $entity) { $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); + return $return; + } + + /** + * {@inheritdoc} + */ + protected function doSaveFieldItems(ContentEntityInterface $entity, array $names) { + $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; + } } - $entity->enforceIsNew(FALSE); - if ($this->revisionTable) { - $entity->setNewRevision(FALSE); + + // Update shared table records if necessary. + if ($shared_table_fields) { + $default_revision = $entity->isDefaultRevision(); + if ($default_revision) { + $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->baseTable); + $this->database + ->update($this->baseTable) + ->fields((array) $record) + ->condition($this->idKey, $record->{$this->idKey}) + ->execute(); + } + if ($this->revisionTable) { + $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) { + $this->saveToSharedTables($entity, $this->revisionDataTable, FALSE); + } + } + + // Update dedicated table records if necessary. + if ($dedicated_table_fields) { + $this->saveToDedicatedTables($entity, TRUE, $dedicated_table_fields); } - return $return; } /** @@ -1017,14 +1064,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 defaults + * 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 +1354,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[]|NULL $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 = NULL) { $vid = $entity->getRevisionId(); $id = $entity->id(); $bundle = $entity->bundle(); @@ -1317,7 +1373,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..8fd54bb 100644 --- a/core/lib/Drupal/Core/Field/FieldItemBase.php +++ b/core/lib/Drupal/Core/Field/FieldItemBase.php @@ -201,12 +201,9 @@ public function preSave() { } /** * {@inheritdoc} */ - public function insert() { } - - /** - * {@inheritdoc} - */ - public function update() { } + public function postSave($update) { + return TRUE; + } /** * {@inheritdoc} diff --git a/core/lib/Drupal/Core/Field/FieldItemInterface.php b/core/lib/Drupal/Core/Field/FieldItemInterface.php index a2339ac..afd1371 100644 --- a/core/lib/Drupal/Core/Field/FieldItemInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemInterface.php @@ -189,20 +189,19 @@ public function view($display_options = array()); 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 updating an entity, just before + * This method is called during the process of saving an entity, just after * 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. */ - 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..2b96495 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,11 @@ public function preSave() { /** * {@inheritdoc} */ - public function insert() { - $this->delegateMethod('insert'); - } - - /** - * {@inheritdoc} - */ - public function update() { - $this->delegateMethod('update'); + public function postSave($update) { + $result = $this->delegateMethod(function(FieldItemInterface $item) use ($update) { + return $item->postSave($update); + }); + return (bool) array_filter($result); } /** @@ -240,13 +234,20 @@ public function deleteRevision() { /** * Calls a method on each FieldItem. * - * @param string $method - * The name of the method. + * @param string|callable $method + * The name of the method to be invoked or a callable to which processed + * items will be passed. + * + * @return array + * An array of results keyed by delta. */ protected function delegateMethod($method) { - foreach ($this->list as $item) { - $item->{$method}(); + $result = []; + $call = is_callable($method); + foreach ($this->list as $delta => $item) { + $result[$delta] = $call ? $method($item) : $item->{$method}(); } + return $result; } /** diff --git a/core/lib/Drupal/Core/Field/FieldItemListInterface.php b/core/lib/Drupal/Core/Field/FieldItemListInterface.php index e4ea12c..18609cf 100644 --- a/core/lib/Drupal/Core/Field/FieldItemListInterface.php +++ b/core/lib/Drupal/Core/Field/FieldItemListInterface.php @@ -136,20 +136,19 @@ public function __unset($property_name); 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 + * values are written into storage. * - * This method is called after the save() method, and before 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. */ - public function update(); + public function postSave($update); /** * Defines custom delete behavior for field values. diff --git a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php index f8ee03c..5626ee5 100644 --- a/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php +++ b/core/modules/file/src/Plugin/Field/FieldType/FileFieldItemList.php @@ -23,61 +23,59 @@ public function defaultValuesForm(array &$form, FormStateInterface $form_state) /** * {@inheritdoc} */ - public function insert() { - parent::insert(); + public function postSave($update) { + $resave = parent::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()); + } } } + + return $resave; } /** diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php index c06271d..e6cef58 100644 --- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php +++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php @@ -55,29 +55,30 @@ public function preSave() { /** * {@inheritdoc} */ - public function insert() { - if ($this->alias) { - $entity = $this->getEntity(); + public function postSave($update) { + $resave = parent::postSave($update); - if ($path = \Drupal::service('path.alias_storage')->save($entity->urlInfo()->getInternalPath(), $this->alias, $this->getLangcode())) { - $this->pid = $path['pid']; + 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); + } } + + return $resave; } /**