Commit 883c209f authored by alexpott's avatar alexpott
Browse files

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) {
}
/**
......
......@@ -8,6 +8,8 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -21,16 +23,35 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
*/
protected $bundleKey = FALSE;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* Constructs a ContentEntityStorageBase object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend to be used.
*/
public function __construct(EntityTypeInterface $entity_type) {
public function __construct(EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
parent::__construct($entity_type);
$this->bundleKey = $this->entityType->getKey('bundle');
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
}
/**
......@@ -38,7 +59,9 @@ public function __construct(EntityTypeInterface $entity_type) {
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type
$entity_type,
$container->get('entity.manager'),
$container->get('cache.entity')
);
}
......@@ -157,6 +180,146 @@ abstract protected function purgeFieldItems(ContentEntityInterface $entity, Fiel
*/
public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { }
/**
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
$revision = $this->doLoadRevisionFieldItems($revision_id);
if ($revision) {
$entities = [$revision->id() => $revision];
$this->invokeStorageLoadHook($entities);
$this->postLoad($entities);
}
return $revision;
}
/**
* Actually loads revision field item values from the storage.
*
* @param int|string $revision_id
* The revision identifier.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* The specified entity revision or NULL if not found.
*/
abstract protected function doLoadRevisionFieldItems($revision_id);
/**
* {@inheritdoc}
*/
protected function doSave($id, EntityInterface $entity) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($entity->isNew()) {
// 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;
}
else {
// @todo Consider returning a different value when saving a non-default
// entity revision. See https://www.drupal.org/node/2509360.
$return = $entity->isDefaultRevision() ? SAVED_UPDATED : FALSE;
}
$this->populateAffectedRevisionTranslations($entity);
$this->doSaveFieldItems($entity);
return $return;
}
/**
* Writes entity field values to the storage.
*
* This method is responsible for allocating entity and revision identifiers
* and updating 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.
*/
abstract protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []);
/**
* {@inheritdoc}
*/
protected function doPreSave(EntityInterface $entity) {
/** @var \Drupal\Core\Entity\ContentEntityBase $entity */
// Sync the changes made in the fields array to the internal values array.
$entity->updateOriginalValues();
return parent::doPreSave($entity);
}
/**
* {@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);
}
}
/**
* {@inheritdoc}
*/
protected function doDelete($entities) {
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
}
$this->doDeleteFieldItems($entities);
}
/**
* Deletes entity field values from the storage.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* An array of entity objects to be deleted.
*/
abstract protected function doDeleteFieldItems($entities);
/**
* {@inheritdoc}
*/
public function deleteRevision($revision_id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
if ($revision = $this->loadRevision($revision_id)) {
// Prevent deletion if this is the default revision.
if ($revision->isDefaultRevision()) {
throw new EntityStorageException('Default revision can not be deleted');
}
$this->invokeFieldMethod('deleteRevision', $revision);
$this->doDeleteRevisionFieldItems($revision);
$this->invokeHook('revision_delete', $revision);
}
}
/**
* Deletes field values of an entity revision from the storage.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $revision
* An entity revision object to be deleted.
*/
abstract protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision);
/**
* Checks translation statuses and invoke the related hooks if needed.
*
......@@ -179,31 +342,96 @@ protected function invokeTranslationHooks(ContentEntityInterface $entity) {
}
}
/**
* Invokes hook_entity_storage_load().
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* List of entities, keyed on the entity ID.
*/
protected function invokeStorageLoadHook(array &$entities) {
if (!empty($entities)) {
// Call hook_entity_storage_load().
foreach ($this->moduleHandler()->getImplementations('entity_storage_load') as $module) {
$function = $module . '_entity_storage_load';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_storage_load().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_storage_load') as $module) {
$function = $module . '_' . $this->entityTypeId . '_storage_load';
$function($entities);
}
}
}
/**
* {@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 $translation_results) {
$resave += array_filter($translation_results);
}
if ($resave) {
$this->doSaveFieldItems($entity, array_keys($resave));
}
}
/**
......@@ -258,4 +486,118 @@ protected function populateAffectedRevisionTranslations(ContentEntityInterface $
}
}
/**
* Ensures integer entity IDs are valid.
*
* The identifier sanitization provided by this method has been introduced
* as Drupal used to rely on the database to facilitate this, which worked
* correctly with MySQL but led to errors with other DBMS such as PostgreSQL.
*
* @param array $ids
* The entity IDs to verify.
*
* @return array
* The sanitized list of entity IDs.
*/
protected function cleanIds(array $ids) {
$definitions = $this->entityManager->getBaseFieldDefinitions($this->entityTypeId);
$id_definition = $definitions[$this->entityType->getKey('id')];
if ($id_definition->getType() == 'integer') {
$ids = array_filter($ids, function ($id) {
return is_numeric($id) && $id == (int) $id;
});
$ids = array_map('intval', $ids);
}
return $ids;
}
/**
* Gets entities from the persistent cache backend.
*
* @param array|null &$ids
* If not empty, return entities that match these IDs. IDs that were found
* will be removed from the list.
*
* @return \Drupal\Core\Entity\ContentEntityInterface[]
* Array of entities from the persistent cache.
*/
protected function getFromPersistentCache(array &$ids = NULL) {
if (!$this->entityType->isPersistentlyCacheable() || empty($ids)) {
return array();
}
$entities = array();
// Build the list of cache entries to retrieve.
$cid_map = array();
foreach ($ids as $id) {
$cid_map[$id] = $this->buildCacheId($id);
}
$cids = array_values($cid_map);
if ($cache = $this->cacheBackend->getMultiple($cids)) {
// Get the entities that were found in the cache.
foreach ($ids as $index => $id) {
$cid = $cid_map[$id];
if (isset($cache[$cid])) {
$entities[$id] = $cache[$cid]->data;
unset($ids[$index]);
}
}
}
return $entities;
}
/**
* Stores entities in the persistent cache backend.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* Entities to store in the cache.
*/
protected function setPersistentCache($entities) {
if (!$this->entityType->isPersistentlyCacheable()) {
return;
}
$cache_tags = array(
$this->entityTypeId . '_values',
'entity_field_info',
);
foreach ($entities as $id => $entity) {
$this->cacheBackend->set($this->buildCacheId($id), $entity, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
}
}
/**
* {@inheritdoc}
*/
public function resetCache(array $ids = NULL) {
if ($ids) {
$cids = array();
foreach ($ids as $id) {
unset($this->entities[$id]);
$cids[] = $this->buildCacheId($id);
}
if ($this->entityType->isPersistentlyCacheable()) {
$this->cacheBackend->deleteMultiple($cids);
}
}
else {
$this->entities = array();
if ($this->entityType->isPersistentlyCacheable()) {
Cache::invalidateTags(array($this->entityTypeId . '_values'));
}
}
}
/**
* Builds the cache ID for the passed in entity ID.
*
* @param int $id
* Entity ID for which the cache ID should be built.
*
* @return string
* Cache ID that can be passed to the cache backend.
*/
protected function buildCacheId($id) {
return "values:{$this->entityTypeId}:$id";
}
}
......@@ -382,6 +382,34 @@ abstract protected function doDelete($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
......
......@@ -8,7 +8,6 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
......@@ -22,7 +21,6 @@
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\FieldStorageConfigInterface;
......@@ -109,13 +107,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
protected $database;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;