Issue #2478459 by plach, mkalkbrenner, chx, yched, Berdir, dawehner, benjy: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks

8.0.x
Alex Pott 2015-07-05 15:29:22 +01:00
parent 760cd403bf
commit 883c209fb6
18 changed files with 907 additions and 412 deletions

View File

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Query\QueryException;
use Drupal\Core\Field\FieldDefinitionInterface;
/**
@ -85,25 +84,25 @@ class ContentEntityNullStorage extends ContentEntityStorageBase {
/**
* {@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) {
}
/**

View File

@ -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 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
*/
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 class ContentEntityStorageBase extends EntityStorageBase implements Dyn
*/
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 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
}
}
/**
* 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 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
}
}
/**
* 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";
}
}

View File

@ -382,6 +382,34 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
* {@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 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
$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 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
$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 @@ abstract class EntityStorageBase extends EntityHandlerBase implements EntityStor
*/
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.
*

View File

@ -79,7 +79,7 @@ interface EntityStorageInterface {
/**
* Load a specific entity revision.
*
* @param int $revision_id
* @param int|string $revision_id
* The revision id.
*
* @return \Drupal\Core\Entity\EntityInterface|null

View File

@ -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\EntityTypeInterface;
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;
/**
* The entity type's storage schema object.
*
@ -123,13 +114,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
protected $storageSchema;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* The language manager.
*
@ -176,10 +160,8 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* The language manager.
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) {
parent::__construct($entity_type);
parent::__construct($entity_type, $entity_manager, $cache);
$this->database = $database;
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
$this->languageManager = $language_manager;
$this->initTableLayout();
}
@ -414,8 +396,10 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$entities_from_cache = $this->getFromPersistentCache($ids);
// Load any remaining entities from the database.
$entities_from_storage = $this->getFromStorage($ids);
$this->setPersistentCache($entities_from_storage);
if ($entities_from_storage = $this->getFromStorage($ids)) {
$this->invokeStorageLoadHook($entities_from_storage);
$this->setPersistentCache($entities_from_storage);
}
return $entities_from_cache + $entities_from_storage;
}
@ -447,157 +431,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// Map the loaded records into entity objects and according fields.
if ($records) {
$entities = $this->mapFromStorageRecords($records);
// 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);
}
}
}
return $entities;
}
/**
* 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);
}
}
/**
* Invokes hook_entity_load_uncached().
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* List of entities, keyed on the entity ID.
*/
protected function invokeLoadUncachedHook(array &$entities) {
if (!empty($entities)) {
// Call hook_entity_load_uncached().
foreach ($this->moduleHandler()->getImplementations('entity_load_uncached') as $module) {
$function = $module . '_entity_load_uncached';
$function($entities, $this->entityTypeId);
}
// Call hook_TYPE_load_uncached().
foreach ($this->moduleHandler()->getImplementations($this->entityTypeId . '_load_uncached') as $module) {
$function = $module . '_' . $this->entityTypeId . '_load_uncached';
$function($entities);
}
}
}
/**
* {@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";
}
/**
* Maps from storage records to entity objects, and attaches fields.
*
@ -727,7 +566,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
/**
* {@inheritdoc}
*/
public function loadRevision($revision_id) {
protected function doLoadRevisionFieldItems($revision_id) {
$revision = NULL;
// Build and execute the query.
$query_result = $this->buildQuery(array(), $revision_id)->execute();
$records = $query_result->fetchAllAssoc($this->idKey);
@ -735,31 +576,20 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
if (!empty($records)) {
// Convert the raw records to entity objects.
$entities = $this->mapFromStorageRecords($records, TRUE);
$this->postLoad($entities);
$entity = reset($entities);
if ($entity) {
return $entity;
}
$revision = reset($entities) ?: NULL;
}
return $revision;
}
/**
* Implements \Drupal\Core\Entity\EntityStorageInterface::deleteRevision().
* {@inheritdoc}
*/
public function deleteRevision($revision_id) {
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->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->invokeFieldMethod('deleteRevision', $revision);
$this->deleteRevisionFromDedicatedTables($revision);
$this->invokeHook('revision_delete', $revision);
}
protected function doDeleteRevisionFieldItems(ContentEntityInterface $revision) {
$this->database->delete($this->revisionTable)
->condition($this->revisionKey, $revision->getRevisionId())
->execute();
$this->deleteRevisionFromDedicatedTables($revision);
}
/**
@ -878,7 +708,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
/**
* {@inheritdoc}
*/
protected function doDelete($entities) {
protected function doDeleteFieldItems($entities) {
$ids = array_keys($entities);
$this->database->delete($this->entityType->getBaseTable())
@ -904,7 +734,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->deleteFromDedicatedTables($entity);
}
}
@ -915,9 +744,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
public function save(EntityInterface $entity) {
$transaction = $this->database->startTransaction();
try {
// Sync the changes made in the fields array to the internal values array.
$entity->updateOriginalValues();
$return = parent::save($entity);
// Ignore replica server temporarily.
@ -934,75 +760,97 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
/**
* {@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->populateAffectedRevisionTranslations($entity);
$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);
}
if ($this->dataTable) {
$this->populateAffectedRevisionTranslations($entity);
$this->saveToSharedTables($entity);
}
if ($this->revisionDataTable) {
$this->saveToSharedTables($entity, $this->revisionDataTable);
}
}
$this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
$this->saveToDedicatedTables($entity, !$is_new);
$table_mapping = $this->getTableMapping();
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
$shared_table_fields = FALSE;
$dedicated_table_fields = [];
if (!$is_new && $this->dataTable) {
$this->invokeTranslationHooks($entity);
// 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) {
$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();
$this->saveToSharedTables($entity, $this->revisionDataTable, $new_revision);
}
}
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);
}
}
}
// Update dedicated table records if necessary.
if ($dedicated_table_fields) {
$names = is_array($dedicated_table_fields) ? $dedicated_table_fields : [];
$this->saveToDedicatedTables($entity, $update, $names);
}
return $return;
}
/**
@ -1019,14 +867,20 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* 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.
@ -1303,8 +1157,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* 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();
@ -1319,7 +1176,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$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;

View File

@ -201,12 +201,7 @@ abstract class FieldItemBase extends Map implements FieldItemInterface {
/**
* {@inheritdoc}
*/
public function insert() { }
/**
* {@inheritdoc}
*/
public function update() { }
public function postSave($update) { }
/**
* {@inheritdoc}

View File

@ -183,26 +183,39 @@ interface FieldItemInterface extends ComplexDataInterface {
/**
* 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.

View File

@ -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 @@ class FieldItemList extends ItemList implements FieldItemListInterface {
/**
* {@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 @@ class FieldItemList extends ItemList implements FieldItemListInterface {
/**
* 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;
}
/**

View File

@ -130,26 +130,29 @@ interface FieldItemListInterface extends ListInterface, AccessibleInterface {
/**
* 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.
*
* 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.
*
* @see \Drupal\Core\Field\FieldItemInterface::postSave()
*/
public function update();
public function postSave($update);
/**
* Defines custom delete behavior for field values.

View File

@ -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') {

View File

@ -23,59 +23,54 @@ class FileFieldItemList extends EntityReferenceFieldItemList {
/**
* {@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();
}
// 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) {
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());
}
return;
}
else {
// Get current target file entities and file IDs.
$files = $this->referencedEntities();
$ids = array();
// 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;
}
/** @var \Drupal\file\FileInterface $file */
foreach ($files as $file) {
$ids[] = $file->id();
}
// 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());
}
// 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;
}
// 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());
// 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;
}
// 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());
}
}
}
}

View File

@ -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();
}
}

View File

@ -55,28 +55,25 @@ class PathItem extends FieldItemBase {
/**
* {@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);
}
}
}

View File

@ -0,0 +1,110 @@
<?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;
}
}

View File

@ -9,6 +9,7 @@ use Drupal\Core\Access\AccessResult;
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().
*/

View File

@ -0,0 +1,118 @@
<?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,
),
),
);
}
/**
* {@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';
}
}

View File

@ -9,7 +9,7 @@ namespace Drupal\user;
use Drupal\Core\Database\Connection;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
@ -72,14 +72,14 @@ class UserStorage extends SqlContentEntityStorage implements UserStorageInterfac
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
// The anonymous user account is saved with the fixed user ID of 0.
// Therefore we need to check for NULL explicitly.
if ($entity->id() === NULL) {
$entity->uid->value = $this->database->nextId($this->database->query('SELECT MAX(uid) FROM {users}')->fetchField());
$entity->enforceIsNew();
}
return parent::save($entity);
return parent::doSaveFieldItems($entity, $names);
}
/**

View File

@ -1130,8 +1130,10 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager))
->setMethods(array('getFromStorage'))
->setMethods(array('getFromStorage', 'invokeStorageLoadHook'))
->getMock();
$entity_storage->method('invokeStorageLoadHook')
->willReturn(NULL);
$entity_storage->expects($this->once())
->method('getFromStorage')
->with(array($id))
@ -1180,8 +1182,10 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager))
->setMethods(array('getFromStorage'))
->setMethods(array('getFromStorage', 'invokeStorageLoadHook'))
->getMock();
$entity_storage->method('invokeStorageLoadHook')
->willReturn(NULL);
$entity_storage->expects($this->once())
->method('getFromStorage')
->with(array($id))