Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023

This commit is contained in:
Pantheon Automation 2015-09-04 13:20:09 -07:00 committed by Greg Anderson
parent 2720a9ec4b
commit f3791f1da3
1898 changed files with 54300 additions and 11481 deletions

View file

@ -801,6 +801,8 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$translation->translations = &$this->translations;
$translation->enforceIsNew = &$this->enforceIsNew;
$translation->newRevision = &$this->newRevision;
$translation->entityKeys = &$this->entityKeys;
$translation->translatableEntityKeys = &$this->translatableEntityKeys;
$translation->translationInitialize = FALSE;
$translation->typedData = NULL;

View file

@ -49,7 +49,7 @@ class EntityFormDisplay extends EntityDisplayBase implements EntityFormDisplayIn
* Returns the entity_form_display object used to build an entity form.
*
* Depending on the configuration of the form mode for the entity bundle, this
* can be either the display object associated to the form mode, or the
* can be either the display object associated with the form mode, or the
* 'default' display.
*
* This method should only be used internally when rendering an entity form.

View file

@ -46,8 +46,8 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn
* Returns the display objects used to render a set of entities.
*
* Depending on the configuration of the view mode for each bundle, this can
* be either the display object associated to the view mode, or the 'default'
* display.
* be either the display object associated with the view mode, or the
* 'default' display.
*
* This method should only be used internally when rendering an entity. When
* assigning suggested display options for a component in a given view mode,

View file

@ -9,6 +9,7 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@ -95,13 +96,13 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
* {@inheritdoc}
*/
public function applyUpdates() {
$change_list = $this->getChangeList();
if ($change_list) {
$complete_change_list = $this->getChangeList();
if ($complete_change_list) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
foreach ($change_list as $entity_type_id => $change_list) {
foreach ($complete_change_list as $entity_type_id => $change_list) {
// Process entity type definition changes before storage definitions ones
// this is necessary when you change an entity type from non-revisionable
// to revisionable and at the same time add revisionable fields to the
@ -127,42 +128,76 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
/**
* {@inheritdoc}
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) {
return FALSE;
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
$this->doEntityUpdate($op, $entity_type_id);
return TRUE;
public function getEntityType($entity_type_id) {
$entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
return $entity_type ? clone $entity_type : NULL;
}
/**
* {@inheritdoc}
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) {
$change_list = $this->getChangeList();
if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) {
return FALSE;
public function installEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeCreate($entity_type);
}
/**
* {@inheritdoc}
*/
public function updateEntityType(EntityTypeInterface $entity_type) {
$original = $this->getEntityType($entity_type->id());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function uninstallEntityType(EntityTypeInterface $entity_type) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onEntityTypeDelete($entity_type);
}
/**
* {@inheritdoc}
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) {
// @todo Pass a mutable field definition interface when we have one. See
// https://www.drupal.org/node/2346329.
if ($storage_definition instanceof BaseFieldDefinition) {
$storage_definition
->setName($name)
->setTargetEntityTypeId($entity_type_id)
->setProvider($provider)
->setTargetBundle(NULL);
}
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
}
if ($reset_cached_definitions) {
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
public function getFieldStorageDefinition($name, $entity_type_id) {
$storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL;
}
$storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
$storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL;
$original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL;
/**
* {@inheritdoc}
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId());
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original);
}
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
return TRUE;
/**
* {@inheritdoc}
*/
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
$this->entityManager->clearCachedDefinitions();
$this->entityManager->onFieldStorageDefinitionDelete($storage_definition);
}
/**

View file

@ -7,6 +7,8 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines an interface for managing entity definition updates.
*
@ -25,12 +27,22 @@ namespace Drupal\Core\Entity;
* report the differences or when to apply each update. This interface is for
* managing that.
*
* This interface also provides methods to retrieve instances of the definitions
* to be updated ready to be manipulated. In fact when definitions change in
* code the system needs to be notified about that and the definitions stored in
* state need to be reconciled with the ones living in code. This typically
* happens in Update API functions, which need to take the system from a known
* state to another known state. Relying on the definitions living in code might
* prevent this, as the system might transition directly to the last available
* state, and thus skipping the intermediate steps. Manipulating the definitions
* in state allows to avoid this and ensures that the various steps of the
* update process are predictable and repeatable.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityTypeListenerInterface
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
* @see hook_update_N()
*/
interface EntityDefinitionUpdateManagerInterface {
@ -75,6 +87,9 @@ interface EntityDefinitionUpdateManagerInterface {
/**
* Applies all the detected valid changes.
*
* Use this with care, as it will apply updates for any module, which will
* lead to unpredictable results.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
@ -84,67 +99,92 @@ interface EntityDefinitionUpdateManagerInterface {
public function applyUpdates();
/**
* Performs a single entity definition update.
* Returns an entity type definition ready to be manipulated.
*
* This method should be used from hook_update_N() functions to process
* entity definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the entity
* definition update. All remaining entity definition updates will be run
* automatically after the hook_update_N() implementations.
* When needing to apply updates to existing entity type definitions, this
* method should always be used to retrieve a definition ready to be
* manipulated.
*
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id
* The entity type to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* The entity type identifier.
*
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @return \Drupal\Core\Entity\EntityTypeInterface
* The entity type definition.
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
public function getEntityType($entity_type_id);
/**
* Performs a single field storage definition update.
* Installs a new entity type definition.
*
* This method should be used from hook_update_N() functions to process field
* storage definition updates as part of the update function. This is only
* necessary if the hook_update_N() implementation relies on the field storage
* definition update. All remaining field storage definition updates will be
* run automatically after the hook_update_N() implementations.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param string $entity_type_id
* The entity type to update.
* @param string $field_name
* The field name to update.
* @param bool $reset_cached_definitions
* (optional). Determines whether to clear the Entity Manager's cached
* definitions before applying the update. Defaults to TRUE. Can be used
* to prevent unnecessary cache invalidation when a hook_update_N() makes
* multiple calls to this method.
* @return bool
* TRUE if the entity update is processed, FALSE if not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
public function installEntityType(EntityTypeInterface $entity_type);
/**
* Applies any change performed to the passed entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function updateEntityType(EntityTypeInterface $entity_type);
/**
* Uninstalls an entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
public function uninstallEntityType(EntityTypeInterface $entity_type);
/**
* Returns a field storage definition ready to be manipulated.
*
* When needing to apply updates to existing field storage definitions, this
* method should always be used to retrieve a storage definition ready to be
* manipulated.
*
* @param string $name
* The field name.
* @param string $entity_type_id
* The entity type identifier.
*
* @return \Drupal\Core\Field\FieldStorageDefinitionInterface
* The field storage definition.
*
* @todo Make this return a mutable storage definition interface when we have
* one. See https://www.drupal.org/node/2346329.
*/
public function getFieldStorageDefinition($name, $entity_type_id);
/**
* Installs a new field storage definition.
*
* @param string $name
* The field storage definition name.
* @param string $entity_type_id
* The target entity type identifier.
* @param string $provider
* The name of the definition provider.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition);
/**
* Applies any change performed to the passed field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
/**
* Uninstalls a field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*/
public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition);
}

View file

@ -256,19 +256,9 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
parent::calculateDependencies();
$target_entity_type = $this->entityManager()->getDefinition($this->targetEntityType);
$bundle_entity_type_id = $target_entity_type->getBundleEntityType();
if ($bundle_entity_type_id != 'bundle') {
// If the target entity type uses entities to manage its bundles then
// depend on the bundle entity.
if (!$bundle_entity = $this->entityManager()->getStorage($bundle_entity_type_id)->load($this->bundle)) {
throw new \LogicException("Missing bundle entity, entity type $bundle_entity_type_id, entity id {$this->bundle}.");
}
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
}
else {
// Depend on the provider of the entity type.
$this->addDependency('module', $target_entity_type->getProvider());
}
// Create dependency on the bundle.
$bundle_config_dependency = $target_entity_type->getBundleConfigDependency($this->bundle);
$this->addDependency($bundle_config_dependency['type'], $bundle_config_dependency['name']);
// If field.module is enabled, add dependencies on 'field_config' entities
// for both displayed and hidden fields. We intentionally leave out base

View file

@ -14,7 +14,10 @@ use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Provides a base class for entity handlers.
*
* @todo Deprecate this in https://www.drupal.org/node/2471663.
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0.
* Implement the container injection pattern of
* \Drupal\Core\Entity\EntityHandlerInterface::createInstance() to obtain the
* module handler service for your class.
*/
abstract class EntityHandlerBase {
use StringTranslationTrait;

View file

@ -9,7 +9,6 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Defines a generic implementation to build a listing of entities.
@ -40,9 +39,12 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
protected $entityType;
/**
* The number of entities to list per page.
* The number of entities to list per page, or FALSE to list all entities.
*
* @var int
* For example, set this to FALSE if the list uses client-side filters that
* require all entities to be listed (like the views overview).
*
* @var int|false
*/
protected $limit = 50;
@ -92,25 +94,31 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
* An array of entity IDs.
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery();
$keys = $this->entityType->getKeys();
return $query
->sort($keys['id'])
->pager($this->limit)
->execute();
$query = $this->getStorage()->getQuery()
->sort($this->entityType->getKey('id'));
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
/**
* Gets the escaped label of an entity.
* Gets the label of an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being listed.
*
* @return string
* The escaped entity label.
* The entity label.
*
* @deprecated in Drupal 8.0.x, will be removed before Drupal 9.0.0
* Use $entity->label() instead. This method used to escape the entity
* label. The render system's autoescape is now relied upon.
*/
protected function getLabel(EntityInterface $entity) {
return SafeMarkup::checkPlain($entity->label());
return $entity->label();
}
/**
@ -227,9 +235,13 @@ class EntityListBuilder extends EntityHandlerBase implements EntityListBuilderIn
$build['table']['#rows'][$entity->id()] = $row;
}
}
$build['pager'] = array(
'#type' => 'pager',
);
// Only add the pager if a limit is specified.
if ($this->limit) {
$build['pager'] = array(
'#type' => 'pager',
);
}
return $build;
}

View file

@ -423,7 +423,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$keys = array_filter($entity_type->getKeys());
// Fail with an exception for non-fieldable entity types.
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}.");
}
@ -655,7 +655,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// bundles, and we do not expect to have so many different entity
// types for this to become a bottleneck.
foreach ($this->getDefinitions() as $entity_type_id => $entity_type) {
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
$bundles = array_keys($this->getBundleInfo($entity_type_id));
foreach ($this->getBaseFieldDefinitions($entity_type_id) as $field_name => $base_field_definition) {
$this->fieldMap[$entity_type_id][$field_name] = [
@ -1205,7 +1205,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$this->eventDispatcher->dispatch(EntityTypeEvents::CREATE, new EntityTypeEvent($entity_type));
$this->setLastInstalledDefinition($entity_type);
if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
$this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id));
}
}

View file

@ -119,7 +119,7 @@ class EntityType implements EntityTypeInterface {
*
* @var string
*/
protected $bundle_entity_type = 'bundle';
protected $bundle_entity_type = NULL;
/**
* The name of the entity type for which bundles are provided.
@ -232,6 +232,13 @@ class EntityType implements EntityTypeInterface {
*/
protected $constraints = array();
/**
* Any additional properties and values.
*
* @var array
*/
protected $additional = [];
/**
* Constructs a new EntityType.
*
@ -248,7 +255,7 @@ class EntityType implements EntityTypeInterface {
}
foreach ($definition as $property => $value) {
$this->{$property} = $value;
$this->set($property, $value);
}
// Ensure defaults.
@ -279,14 +286,25 @@ class EntityType implements EntityTypeInterface {
* {@inheritdoc}
*/
public function get($property) {
return isset($this->{$property}) ? $this->{$property} : NULL;
if (property_exists($this, $property)) {
$value = isset($this->{$property}) ? $this->{$property} : NULL;
}
else {
$value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
}
return $value;
}
/**
* {@inheritdoc}
*/
public function set($property, $value) {
$this->{$property} = $value;
if (property_exists($this, $property)) {
$this->{$property} = $value;
}
else {
$this->additional[$property] = $value;
}
return $this;
}
@ -764,4 +782,30 @@ class EntityType implements EntityTypeInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function getBundleConfigDependency($bundle) {
// If this entity type uses entities to manage its bundles then depend on
// the bundle entity.
if ($bundle_entity_type_id = $this->getBundleEntityType()) {
if (!$bundle_entity = \Drupal::entityManager()->getStorage($bundle_entity_type_id)->load($bundle)) {
throw new \LogicException(sprintf('Missing bundle entity, entity type %s, entity id %s.', $bundle_entity_type_id, $bundle));
}
$config_dependency = [
'type' => 'config',
'name' => $bundle_entity->getConfigDependencyName(),
];
}
else {
// Depend on the provider of the entity type.
$config_dependency = [
'type' => 'module',
'name' => $this->getProvider(),
];
}
return $config_dependency;
}
}

View file

@ -732,4 +732,17 @@ interface EntityTypeInterface {
*/
public function addConstraint($constraint_name, $options = NULL);
/**
* Gets the config dependency info for this entity, if any exists.
*
* @param string $bundle
* The bundle name.
*
* @return array
* An associative array containing the following keys:
* - 'type': The config dependency type (e.g. 'module', 'config').
* - 'name': The name of the config dependency.
*/
public function getBundleConfigDependency($bundle);
}

View file

@ -465,7 +465,7 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
// series of fields individually for cases such as views tables.
$entity_type_id = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . crc32(serialize($display_options));
$key = $entity_type_id . ':' . $bundle . ':' . $field_name . ':' . hash('crc32b', serialize($display_options));
if (!isset($this->singleFieldDisplays[$key])) {
$this->singleFieldDisplays[$key] = EntityViewDisplay::create(array(
'targetEntityType' => $entity_type_id,

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity\Plugin\DataType;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
@ -114,7 +113,7 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
*/
public function getProperties($include_computed = FALSE) {
if (!isset($this->entity)) {
throw new MissingDataException(SafeMarkup::format('Unable to get properties as no entity has been provided.'));
throw new MissingDataException('Unable to get properties as no entity has been provided.');
}
if (!$this->entity instanceof FieldableEntityInterface) {
// @todo: Add support for config entities in

View file

@ -189,9 +189,10 @@ class DefaultTableMapping implements TableMappingInterface {
public function getColumnNames($field_name) {
if (!isset($this->columnMapping[$field_name])) {
$this->columnMapping[$field_name] = array();
$storage_definition = $this->fieldStorageDefinitions[$field_name];
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name);
if (isset($this->fieldStorageDefinitions[$field_name])) {
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
}
}
}
return $this->columnMapping[$field_name];

View file

@ -10,6 +10,8 @@ namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityBundleListenerInterface;
@ -1351,7 +1353,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
});
}
/**
@ -1364,14 +1368,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// definition.
$this->initTableLayout();
// Let the schema handler adapt to possible table layout changes.
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
$this->wrapSchemaException(function () use ($entity_type, $original) {
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
});
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
$this->wrapSchemaException(function () use ($entity_type) {
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
});
}
/**
@ -1386,14 +1394,18 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
$this->tableMapping = NULL;
}
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
$this->wrapSchemaException(function () use ($storage_definition, $original) {
$this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original);
});
}
/**
@ -1421,7 +1433,31 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
// Update the field schema.
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
$this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition);
});
}
/**
* Wraps a database schema exception into an entity storage exception.
*
* @param callable $callback
* The callback to be executed.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* When a database schema exception is thrown.
*/
protected function wrapSchemaException(callable $callback) {
$message = 'Exception thrown while performing a schema update.';
try {
$callback();
}
catch (SchemaException $e) {
throw new EntityStorageException($message, 0, $e);
}
catch (DatabaseExceptionWrapper $e) {
throw new EntityStorageException($message, 0, $e);
}
}
/**
@ -1598,10 +1634,12 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
foreach ($storage_definition->getColumns() as $column_name => $data) {
$or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
}
$query
->condition($or)
->fields('t', array('entity_id'))
->distinct(TRUE);
$query->condition($or);
if (!$as_bool) {
$query
->fields('t', array('entity_id'))
->distinct(TRUE);
}
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
// Ascertain the table this field is mapped too.

View file

@ -186,27 +186,44 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
return TRUE;
}
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
return $this->getDedicatedTableSchema($storage_definition) != $this->loadFieldSchemaData($original);
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
$field_name = $storage_definition->getName();
$schema = array();
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
}
}
return $schema != $this->loadFieldSchemaData($original);
}
else {
if ($storage_definition->hasCustomStorage()) {
// The field has custom storage, so we don't know if a schema change is
// needed or not, but since per the initial checks earlier in this
// function, nothing about the definition changed that we manage, we
// return FALSE.
return FALSE;
}
return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
}
/**
* Gets the schema data for the given field storage definition.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition. The field that must not have custom
* storage, i.e. the storage must take care of storing the field.
*
* @return array
* The schema data.
*/
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
assert('!$storage_definition->hasCustomStorage();');
$table_mapping = $this->storage->getTableMapping();
$schema = [];
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$schema = $this->getDedicatedTableSchema($storage_definition);
}
elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
$field_name = $storage_definition->getName();
foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
}
}
}
return $schema;
}
/**
@ -285,7 +302,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// If a migration is required, we can't proceed.
if ($this->requiresEntityDataMigration($entity_type, $original)) {
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type with data.');
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type->id() . ') with data.');
}
// If we have no data just recreate the entity schema from scratch.
@ -311,36 +328,12 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
$schema_handler = $this->database->schema();
// Drop original indexes and unique keys.
foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) {
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $name => $specifier) {
$schema_handler->dropIndex($table_name, $name);
}
}
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $name => $specifier) {
$schema_handler->dropUniqueKey($table_name, $name);
}
}
}
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
// Create new indexes and unique keys.
$entity_schema = $this->getEntitySchema($entity_type, TRUE);
foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $name => $specifier) {
$schema_handler->addIndex($table_name, $name, $specifier);
}
}
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $name => $specifier) {
$schema_handler->addUniqueKey($table_name, $name, $specifier);
}
}
}
$this->createEntitySchemaIndexes($entity_schema);
// Store the updated entity schema.
$this->saveEntitySchemaData($entity_type, $entity_schema);
@ -411,7 +404,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// @todo Add purging to all fields: https://www.drupal.org/node/2282119.
try {
if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) {
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field with data that cannot be purged.');
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
}
}
catch (DatabaseException $e) {
@ -1138,9 +1131,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
if (!$schema_handler->indexExists($table_name, $name)) {
$schema_handler->addIndex($table_name, $name, $specifier);
}
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
@ -1154,7 +1145,14 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
}
$this->saveFieldSchemaData($storage_definition, $schema);
if (!$only_save) {
// Make sure any entity index involving this field is re-created if
// needed.
$this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition);
}
}
/**
@ -1185,6 +1183,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* The storage definition of the field being deleted.
*/
protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
// Make sure any entity index involving this field is dropped.
$this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition);
$deleted_field_name = $storage_definition->getName();
$table_mapping = $this->storage->getTableMapping(
$this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id())
@ -1263,8 +1264,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
if ($storage_definition->getColumns() != $original->getColumns()) {
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
if ($this->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
}
// There is data, so there are no column changes. Drop all the prior
// indexes and create all the new ones, except for all the priors that
@ -1273,9 +1274,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$table = $table_mapping->getDedicatedDataTableName($original);
$revision_table = $table_mapping->getDedicatedRevisionTableName($original);
// Get the field schemas.
$schema = $storage_definition->getSchema();
$original_schema = $original->getSchema();
// Gets the SQL schema for a dedicated tables.
$actual_schema = $this->getDedicatedTableSchema($storage_definition);
foreach ($original_schema['indexes'] as $name => $columns) {
if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
$real_name = $this->getFieldIndexName($storage_definition, $name);
@ -1302,8 +1307,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$real_columns[] = $table_mapping->getFieldColumnName($storage_definition, $column_name);
}
}
$this->database->schema()->addIndex($table, $real_name, $real_columns);
$this->database->schema()->addIndex($revision_table, $real_name, $real_columns);
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
$this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
$this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
}
}
$this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition));
@ -1348,8 +1355,8 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
else {
if ($storage_definition->getColumns() != $original->getColumns()) {
throw new FieldStorageDefinitionUpdateForbiddenException("The SQL storage cannot change the schema for an existing field with data.");
if ($this->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
}
$updated_field_name = $storage_definition->getName();
@ -1366,6 +1373,20 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
if ($field_name == $updated_field_name) {
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
// Handle NOT NULL constraints.
foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
$not_null = !empty($specifier['not null']);
$original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
if ($not_null !== $original_not_null) {
if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) {
throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values.");
}
$column_schema = $original_schema[$table_name]['fields'][$column_name];
$column_schema['not null'] = $not_null;
$schema_handler->changeField($table_name, $field_name, $field_name, $column_schema);
}
}
// Drop original indexes and unique keys.
if (!empty($original_schema[$table_name]['indexes'])) {
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
@ -1380,7 +1401,10 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// Create new indexes and unique keys.
if (!empty($schema[$table_name]['indexes'])) {
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
$schema_handler->addIndex($table_name, $name, $specifier);
// Check if the index exists because it might already have been
// created as part of the earlier entity type update event.
$this->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
@ -1397,6 +1421,120 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
}
/**
* Creates the specified entity schema indexes and keys.
*
* @param array $entity_schema
* The entity schema definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
* (optional) If a field storage definition is specified, only indexes and
* keys involving its columns will be processed. Otherwise all defined
* entity indexes and keys will be processed.
*/
protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database->schema();
if ($storage_definition) {
$table_mapping = $this->storage->getTableMapping();
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
}
$index_keys = [
'indexes' => 'addIndex',
'unique keys' => 'addUniqueKey',
];
foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
// Add fields schema because database driver may depend on this data to
// perform index normalization.
$schema['fields'] = $entity_schema[$table_name]['fields'];
foreach ($index_keys as $key => $add_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
// If a set of field columns were specified we process only indexes
// involving them. Only indexes for which all columns exist are
// actually created.
$create = FALSE;
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$create = TRUE;
foreach ($specifier_columns as $specifier_column_name) {
// This may happen when adding more than one field in the same
// update run. Eventually when all field columns have been
// created the index will be created too.
if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
$create = FALSE;
break;
}
}
}
if ($create) {
$this->{$add_method}($table_name, $name, $specifier, $schema);
}
}
}
}
}
}
/**
* Deletes the specified entity schema indexes and keys.
*
* @param array $entity_schema_data
* The entity schema data definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition
* (optional) If a field storage definition is specified, only indexes and
* keys involving its columns will be processed. Otherwise all defined
* entity indexes and keys will be processed.
*/
protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database->schema();
if ($storage_definition) {
$table_mapping = $this->storage->getTableMapping();
$column_names = $table_mapping->getColumnNames($storage_definition->getName());
}
$index_keys = [
'indexes' => 'dropIndex',
'unique keys' => 'dropUniqueKey',
];
foreach ($entity_schema_data as $table_name => $schema) {
foreach ($index_keys as $key => $drop_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
$specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$schema_handler->{$drop_method}($table_name, $name);
}
}
}
}
}
}
/**
* Checks whether a field property has NULL values.
*
* @param string $table_name
* The name of the table to inspect.
* @param string $column_name
* The name of the column holding the field property data.
*
* @return bool
* TRUE if NULL data is found, FALSE otherwise.
*/
protected function hasNullFieldPropertyData($table_name, $column_name) {
$query = $this->database->select($table_name, 't')
->fields('t', [$column_name])
->range(0, 1);
$query->isNull('t.' . $column_name);
$result = $query->execute()->fetchAssoc();
return (bool) $result;
}
/**
* Gets the schema for a single field definition.
*
@ -1738,6 +1876,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
return $storage_definition->getName() . '_' . $index;
}
/**
* Checks whether a database table is non-existent or empty.
*
@ -1758,4 +1897,91 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
->fetchField();
}
/**
* Compares schemas to check for changes in the column definitions.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* Current field storage definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
* The original field storage definition.
*
* @return bool
* Returns TRUE if there are schema changes in the column definitions.
*/
protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if ($storage_definition->getColumns() != $original->getColumns()) {
// Base field definitions have schema data stored in the original
// definition.
return TRUE;
}
if (!$storage_definition->hasCustomStorage()) {
$keys = array_flip($this->getColumnSchemaRelevantKeys());
$definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
foreach ($table_schema['fields'] as $name => $spec) {
$definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
$stored_spec = array_intersect_key($spec, $keys);
if ($definition_spec != $stored_spec) {
return TRUE;
}
}
}
}
return FALSE;
}
/**
* Returns a list of column schema keys affecting data storage.
*
* When comparing schema definitions, only changes in certain properties
* actually affect how data is stored and thus, if applied, may imply data
* manipulation.
*
* @return string[]
* An array of key names.
*/
protected function getColumnSchemaRelevantKeys() {
return ['type', 'size', 'length', 'unsigned'];
}
/**
* Creates an index, dropping it if already existing.
*
* @param string $table
* The table name.
* @param string $name
* The index name.
* @param array $specifier
* The fields to index.
* @param array $schema
* The table specification.
*
* @see \Drupal\Core\Database\Schema::addIndex()
*/
protected function addIndex($table, $name, array $specifier, array $schema) {
$schema_handler = $this->database->schema();
$schema_handler->dropIndex($table, $name);
$schema_handler->addIndex($table, $name, $specifier, $schema);
}
/**
* Creates a unique key, dropping it if already existing.
*
* @param string $table
* The table name.
* @param string $name
* The index name.
* @param array $specifier
* The unique fields.
*
* @see \Drupal\Core\Database\Schema::addUniqueKey()
*/
protected function addUniqueKey($table, $name, array $specifier) {
$schema_handler = $this->database->schema();
$schema_handler->dropUniqueKey($table, $name);
$schema_handler->addUniqueKey($table, $name, $specifier);
}
}