Commit 883c209f authored by alexpott's avatar alexpott

Issue #2478459 by plach, mkalkbrenner, chx, yched, Berdir, dawehner, benjy:...

Issue #2478459 by plach, mkalkbrenner, chx, yched, Berdir, dawehner, benjy: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks
parent 760cd403
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Query\QueryException;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
......@@ -85,25 +84,25 @@ protected function getQueryServiceName() {
/**
* {@inheritdoc}
*/
protected function doLoadFieldItems($entities, $age) {
protected function doLoadRevisionFieldItems($revision_id) {
}
/**
* {@inheritdoc}
*/
protected function doSaveFieldItems(EntityInterface $entity, $update) {
protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
}
/**
* {@inheritdoc}
*/
protected function doDeleteFieldItems(EntityInterface $entity) {
protected function doDeleteFieldItems($entities) {
}
/**
* {@inheritdoc}
*/
protected function doDeleteFieldItemsRevision(EntityInterface $entity) {
protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
}
/**
......
......@@ -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|string
* 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;
}
/**
......@@ -443,6 +451,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 bool $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.
*
......
......@@ -79,7 +79,7 @@ public function loadUnchanged($id);
/**
* Load a specific entity revision.
*
* @param int $revision_id
* @param int|string $revision_id
* The revision id.
*
* @return \Drupal\Core\Entity\EntityInterface|null
......
......@@ -201,12 +201,7 @@ public function preSave() { }
/**
* {@inheritdoc}
*/
public function insert() { }
/**
* {@inheritdoc}
*/
public function update() { }
public function postSave($update) { }
/**
* {@inheritdoc}
......
......@@ -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.
......
......@@ -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;
}
/**
......
......@@ -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.
......
......@@ -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') {
......
......@@ -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\FileInterface $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());
}
}
}
}
......
......@@ -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();
}
}
......@@ -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);
}
}
}
......
<?php
/**
* @file
* Contains Drupal\system\Tests\Field\FieldItemTest.
*/
namespace Drupal\system\Tests\Field;
use Drupal\Component\Utility\Unicode;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
/**
* Test field item methods.
*
* @group Field
*/
class FieldItemTest extends EntityUnitTestBase {
/**
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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;
}
}
......@@ -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;
......@@ -97,6 +98,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().
*/
......
<?php
/**
* @file
* Contains \Drupal\entity_test\Plugin\Field\FieldType\FieldTestItem.
*/
namespace Drupal\entity_test\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslationWrapper;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Defines the 'field_test' entity field type.
*
* @FieldType(
* id = "field_test",
* label = @Translation("Test field item"),
* description = @Translation("A field containing a plain string value."),
* category = @Translation("Field"),
* )
*/
class FieldTestItem extends FieldItemBase {
/**
* Counts how many times all items of this type are saved.
*
* @var int
*/
protected static $counter = [];
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
// This is called very early by the user entity roles field. Prevent
// early t() calls by using the TranslationWrapper.
$properties['value'] = DataDefinition::create('string')
->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,
),
),
);
}