Update to Drupal 8.0.0 beta 14. For more information, see https://drupal.org/node/2544542

This commit is contained in:
Pantheon Automation 2015-08-27 12:03:05 -07:00 committed by Greg Anderson
parent 3b2511d96d
commit 81ccda77eb
2155 changed files with 54307 additions and 46870 deletions

View file

@ -138,12 +138,33 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
protected $isDefaultRevision = TRUE;
/**
* Holds entity keys like the ID, bundle and revision ID.
* Holds translatable entity keys such as the ID, bundle and revision ID.
*
* @var array
*/
protected $entityKeys = array();
/**
* Holds translatable entity keys such as the label.
*
* @var array
*/
protected $translatableEntityKeys = array();
/**
* Whether entity validation was performed.
*
* @var bool
*/
protected $validated = FALSE;
/**
* Whether entity validation is required before saving the entity.
*
* @var bool
*/
protected $validationRequired = FALSE;
/**
* Overrides Entity::__construct().
*/
@ -165,14 +186,36 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$this->values = $values;
foreach ($this->getEntityType()->getKeys() as $key => $field_name) {
if (isset($this->values[$field_name])) {
if (is_array($this->values[$field_name]) && isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
if (is_array($this->values[$field_name])) {
// We store untranslatable fields into an entity key without using a
// langcode key.
if (!$this->getFieldDefinition($field_name)->isTranslatable()) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (is_array($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT])) {
if (isset($this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'])) {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT][0]['value'];
}
}
else {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
}
}
}
else {
$this->entityKeys[$key] = $this->values[$field_name][LanguageInterface::LANGCODE_DEFAULT];
// We save translatable fields such as the publishing status of a node
// into an entity key array keyed by langcode as a performance
// optimization, so we don't have to go through TypedData when we
// need these values.
foreach ($this->values[$field_name] as $langcode => $field_value) {
if (is_array($this->values[$field_name][$langcode])) {
if (isset($this->values[$field_name][$langcode][0]['value'])) {
$this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode][0]['value'];
}
}
else {
$this->translatableEntityKeys[$key][$langcode] = $this->values[$field_name][$langcode];
}
}
}
}
}
@ -220,7 +263,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
public function setNewRevision($value = TRUE) {
if (!$this->getEntityType()->hasKey('revision')) {
throw new \LogicException(SafeMarkup::format('Entity type @entity_type does not support revisions.', ['@entity_type' => $this->getEntityTypeId()]));
throw new \LogicException("Entity type {$this->getEntityTypeId()} does not support revisions.");
}
if ($value && !$this->newRevision) {
@ -302,6 +345,23 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
// An entity requiring validation should not be saved if it has not been
// actually validated.
if ($this->validationRequired && !$this->validated) {
// @todo Make this an assertion in https://www.drupal.org/node/2408013.
throw new \LogicException('Entity validation was skipped.');
}
else {
$this->validated = FALSE;
}
parent::preSave($storage);
}
/**
* {@inheritdoc}
*/
@ -312,10 +372,26 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* {@inheritdoc}
*/
public function validate() {
$this->validated = TRUE;
$violations = $this->getTypedData()->validate();
return new EntityConstraintViolationList($this, iterator_to_array($violations));
}
/**
* {@inheritdoc}
*/
public function isValidationRequired() {
return (bool) $this->validationRequired;
}
/**
* {@inheritdoc}
*/
public function setValidationRequired($required) {
$this->validationRequired = $required;
return $this;
}
/**
* Clear entity translation object cache to remove stale references.
*/
@ -388,15 +464,14 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected function getTranslatedField($name, $langcode) {
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
$message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
throw new \InvalidArgumentException(SafeMarkup::format($message, array('@langcode' => $this->activeLangcode)));
throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
}
// Populate $this->fields to speed-up further look-ups and to keep track of
// fields objects, possibly holding changes to field values.
if (!isset($this->fields[$name][$langcode])) {
$definition = $this->getFieldDefinition($name);
if (!$definition) {
throw new \InvalidArgumentException('Field ' . SafeMarkup::checkPlain($name) . ' is unknown.');
throw new \InvalidArgumentException("Field $name is unknown.");
}
// Non-translatable fields are always stored with
// LanguageInterface::LANGCODE_DEFAULT as key.
@ -413,7 +488,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
if (isset($this->values[$name][$langcode])) {
$value = $this->values[$name][$langcode];
}
$field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this, $name, $value);
$field = \Drupal::service('plugin.manager.field.field_type')->createFieldItemList($this->getTranslation($langcode), $name, $value);
if ($default) {
// $this->defaultLangcode might not be set if we are initializing the
// default language code cache, in which case there is no valid
@ -454,6 +529,19 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
return $fields;
}
/**
* {@inheritdoc}
*/
public function getTranslatableFields($include_computed = TRUE) {
$fields = [];
foreach ($this->getFieldDefinitions() as $name => $definition) {
if (($include_computed || !$definition->isComputed()) && $definition->isTranslatable()) {
$fields[$name] = $this->get($name);
}
}
return $fields;
}
/**
* {@inheritdoc}
*/
@ -537,12 +625,12 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// Get the language code if the property exists.
// Try to read the value directly from the list of entity keys which got
// initialized in __construct(). This avoids creating a field item object.
if (isset($this->entityKeys['langcode'])) {
$this->defaultLangcode = $this->entityKeys['langcode'];
if (isset($this->translatableEntityKeys['langcode'][$this->activeLangcode])) {
$this->defaultLangcode = $this->translatableEntityKeys['langcode'][$this->activeLangcode];
}
elseif ($this->hasField($this->langcodeKey) && ($item = $this->get($this->langcodeKey)) && isset($item->language)) {
$this->defaultLangcode = $item->language->getId();
$this->entityKeys['langcode'] = $this->defaultLangcode;
$this->translatableEntityKeys['langcode'][$this->activeLangcode] = $this->defaultLangcode;
}
if (empty($this->defaultLangcode)) {
@ -583,8 +671,13 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
// that check, as it ready only and must not change, unsetting it could
// lead to recursions.
if ($key = array_search($name, $this->getEntityType()->getKeys())) {
if (isset($this->entityKeys[$key]) && $key != 'bundle') {
unset($this->entityKeys[$key]);
if ($key != 'bundle') {
if (isset($this->entityKeys[$key])) {
unset($this->entityKeys[$key]);
}
elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
}
}
}
@ -663,8 +756,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
}
if (empty($translation)) {
$message = 'Invalid translation language (@langcode) specified.';
throw new \InvalidArgumentException(SafeMarkup::format($message, array('@langcode' => $langcode)));
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
}
return $translation;
@ -710,8 +802,6 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$translation->enforceIsNew = &$this->enforceIsNew;
$translation->newRevision = &$this->newRevision;
$translation->translationInitialize = FALSE;
// Reset language-dependent properties.
unset($translation->entityKeys['label']);
$translation->typedData = NULL;
return $translation;
@ -733,8 +823,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
public function addTranslation($langcode, array $values = array()) {
$this->getLanguages();
if (!isset($this->languages[$langcode]) || $this->hasTranslation($langcode)) {
$message = 'Invalid translation language (@langcode) specified.';
throw new \InvalidArgumentException(SafeMarkup::format($message, array('@langcode' => $langcode)));
throw new \InvalidArgumentException("Invalid translation language ($langcode) specified.");
}
// Instantiate a new empty entity so default values will be populated in the
@ -784,8 +873,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
$this->translations[$langcode]['status'] = static::TRANSLATION_REMOVED;
}
else {
$message = 'The specified translation (@langcode) cannot be removed.';
throw new \InvalidArgumentException(SafeMarkup::format($message, array('@langcode' => $langcode)));
throw new \InvalidArgumentException("The specified translation ($langcode) cannot be removed.");
}
}
@ -918,8 +1006,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
public function createDuplicate() {
if ($this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_REMOVED) {
$message = 'The entity object refers to a removed translation (@langcode) and cannot be manipulated.';
throw new \InvalidArgumentException(SafeMarkup::format($message, array('@langcode' => $this->activeLangcode)));
throw new \InvalidArgumentException("The entity object refers to a removed translation ({$this->activeLangcode}) and cannot be manipulated.");
}
$duplicate = clone $this;
@ -962,7 +1049,7 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
}
foreach ($values as $langcode => $items) {
$this->fields[$name][$langcode] = clone $items;
$this->fields[$name][$langcode]->setContext($name, $this->getTypedData());
$this->fields[$name][$langcode]->setContext($name, $this->getTranslation($langcode)->getTypedData());
}
}
@ -1020,18 +1107,34 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
* The value of the entity key, NULL if not defined.
*/
protected function getEntityKey($key) {
if (!isset($this->entityKeys[$key]) || !array_key_exists($key, $this->entityKeys)) {
if ($this->getEntityType()->hasKey($key)) {
$field_name = $this->getEntityType()->getKey($key);
$property = $this->getFieldDefinition($field_name)->getFieldStorageDefinition()->getMainPropertyName();
$this->entityKeys[$key] = $this->get($field_name)->$property;
// If the value is known already, return it.
if (isset($this->entityKeys[$key])) {
return $this->entityKeys[$key];
}
if (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
return $this->translatableEntityKeys[$key][$this->activeLangcode];
}
// Otherwise fetch the value by creating a field object.
$value = NULL;
if ($this->getEntityType()->hasKey($key)) {
$field_name = $this->getEntityType()->getKey($key);
$definition = $this->getFieldDefinition($field_name);
$property = $definition->getFieldStorageDefinition()->getMainPropertyName();
$value = $this->get($field_name)->$property;
// Put it in the right array, depending on whether it is translatable.
if ($definition->isTranslatable()) {
$this->translatableEntityKeys[$key][$this->activeLangcode] = $value;
}
else {
$this->entityKeys[$key] = NULL;
$this->entityKeys[$key] = $value;
}
}
return $this->entityKeys[$key];
else {
$this->entityKeys[$key] = $value;
}
return $value;
}
/**

View file

@ -86,9 +86,6 @@ abstract class ContentEntityConfirmFormBase extends ContentEntityForm implements
'submit' => array(
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#validate' => array(
array($this, 'validate'),
),
'#submit' => array(
array($this, 'submitForm'),
),
@ -121,9 +118,10 @@ abstract class ContentEntityConfirmFormBase extends ContentEntityForm implements
/**
* {@inheritdoc}
*/
public function validate(array $form, FormStateInterface $form_state) {
public function validateForm(array &$form, FormStateInterface $form_state) {
// Override the default validation implementation as it is not necessary
// nor possible to validate an entity in a confirmation form.
return $this->entity;
}
}

View file

@ -64,17 +64,28 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
/**
* {@inheritdoc}
*
* Note that extending classes should not override this method to add entity
* validation logic, but define further validation constraints using the
* entity validation API and/or provide a new validation constraint if
* necessary. This is the only way to ensure that the validation logic
* is correctly applied independently of form submissions; e.g., for REST
* requests.
* For more information about entity validation, see
* https://www.drupal.org/node/2015613.
*/
public function validate(array $form, FormStateInterface $form_state) {
public function buildEntity(array $form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = parent::buildEntity($form, $form_state);
// Mark the entity as requiring validation.
$entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
return $entity;
}
/**
* {@inheritdoc}
*
* Button-level validation handlers are highly discouraged for entity forms,
* as they will prevent entity validation from running. If the entity is going
* to be saved during the form submission, this method should be manually
* invoked from the button-level validation handler, otherwise an exception
* will be thrown.
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->buildEntity($form, $form_state);
@ -87,10 +98,10 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface
$this->flagViolations($violations, $form, $form_state);
// @todo Remove this.
// Execute legacy global validation handlers.
$form_state->setValidateHandlers([]);
\Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
// The entity was validated.
$entity->setValidationRequired(FALSE);
$form_state->setTemporaryValue('entity_validated', TRUE);
return $entity;
}

View file

@ -36,6 +36,8 @@ interface ContentEntityFormInterface extends EntityFormInterface {
* The form display that the current form operates with.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return $this
*/
public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state);
@ -56,9 +58,26 @@ interface ContentEntityFormInterface extends EntityFormInterface {
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return boolean
* @return bool
* Returns TRUE if the entity form language matches the entity one.
*/
public function isDefaultFormLangcode(FormStateInterface $form_state);
/**
* {@inheritdoc}
*
* Note that extending classes should not override this method to add entity
* validation logic, but define further validation constraints using the
* entity validation API and/or provide a new validation constraint if
* necessary. This is the only way to ensure that the validation logic
* is correctly applied independently of form submissions; e.g., for REST
* requests.
* For more information about entity validation, see
* https://www.drupal.org/node/2015613.
*
* @return \Drupal\Core\Entity\ContentEntityTypeInterface
* The built entity.
*/
public function validateForm(array &$form, FormStateInterface $form_state);
}

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

@ -7,7 +7,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 +22,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 +58,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')
);
}
@ -60,7 +82,7 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Dyn
$bundle = FALSE;
if ($this->bundleKey) {
if (!isset($values[$this->bundleKey])) {
throw new EntityStorageException(SafeMarkup::format('Missing bundle for entity type @type', array('@type' => $this->entityTypeId)));
throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
}
$bundle = $values[$this->bundleKey];
}
@ -157,6 +179,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 +341,101 @@ 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();
// For non translatable fields, there is only one field object instance
// across all translations and it has as parent entity the entity in the
// default entity translation. Therefore field methods on non translatable
// fields should be invoked only on the default entity translation.
$fields = $translation->isDefaultTranslation() ? $translation->getFields() : $translation->getTranslatableFields();
foreach ($fields 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 +490,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

@ -105,6 +105,7 @@ class EntityViewController implements ContainerInjectionInterface {
$page['#pre_render'][] = [$this, 'buildTitle'];
$page['#entity_type'] = $_entity->getEntityTypeId();
$page['#' . $page['#entity_type']] = $_entity;
return $page;

View file

@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
@ -24,6 +25,8 @@ use Drupal\Core\Url;
*/
abstract class Entity implements EntityInterface {
use RefinableCacheableDependencyTrait;
use DependencySerializationTrait {
__sleep as traitSleep;
}
@ -192,10 +195,7 @@ abstract class Entity implements EntityInterface {
$uri = call_user_func($uri_callback, $this);
}
else {
throw new UndefinedLinkTemplateException(SafeMarkup::format('No link template "@rel" found for the "@entity_type" entity type', array(
'@rel' => $rel,
'@entity_type' => $this->getEntityTypeId(),
)));
throw new UndefinedLinkTemplateException("No link template '$rel' found for the '{$this->getEntityTypeId()}' entity type");
}
}
@ -381,12 +381,7 @@ abstract class Entity implements EntityInterface {
if ($this->getEntityType()->getBundleOf()) {
// Throw an exception if the bundle ID is longer than 32 characters.
if (Unicode::strlen($this->id()) > EntityTypeInterface::BUNDLE_MAX_LENGTH) {
throw new ConfigEntityIdLengthException(SafeMarkup::format(
'Attempt to create a bundle with an ID longer than @max characters: @id.', array(
'@max' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'@id' => $this->id(),
)
));
throw new ConfigEntityIdLengthException("Attempt to create a bundle with an ID longer than " . EntityTypeInterface::BUNDLE_MAX_LENGTH . " characters: $this->id().");
}
}
}
@ -440,23 +435,36 @@ abstract class Entity implements EntityInterface {
* {@inheritdoc}
*/
public function getCacheContexts() {
return [];
return $this->cacheContexts;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
public function getCacheTagsToInvalidate() {
// @todo Add bundle-specific listing cache tag?
// https://www.drupal.org/node/2145751
if ($this->isNew()) {
return [];
}
return [$this->entityTypeId . ':' . $this->id()];
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
if ($this->cacheTags) {
return Cache::mergeTags($this->getCacheTagsToInvalidate(), $this->cacheTags);
}
return $this->getCacheTagsToInvalidate();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
return Cache::PERMANENT;
return $this->cacheMaxAge;
}
/**
@ -511,7 +519,7 @@ abstract class Entity implements EntityInterface {
}
if ($update) {
// An existing entity was updated, also invalidate its unique cache tag.
$tags = Cache::mergeTags($tags, $this->getCacheTags());
$tags = Cache::mergeTags($tags, $this->getCacheTagsToInvalidate());
}
Cache::invalidateTags($tags);
}
@ -532,7 +540,7 @@ abstract class Entity implements EntityInterface {
// other pages than the one it's on. The one it's on is handled by its own
// cache tag, but subsequent list pages would not be invalidated, hence we
// must invalidate its list cache tags as well.)
$tags = Cache::mergeTags($tags, $entity->getCacheTags());
$tags = Cache::mergeTags($tags, $entity->getCacheTagsToInvalidate());
}
Cache::invalidateTags($tags);
}

View file

@ -81,9 +81,6 @@ abstract class EntityConfirmFormBase extends EntityForm implements ConfirmFormIn
'submit' => array(
'#type' => 'submit',
'#value' => $this->getConfirmText(),
'#validate' => array(
array($this, 'validate'),
),
'#submit' => array(
array($this, 'submitForm'),
),

View file

@ -97,7 +97,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
public function applyUpdates() {
$change_list = $this->getChangeList();
if ($change_list) {
// getChangeList() only disables the cache and does not invalidate.
// self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions();
}
@ -107,18 +107,7 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
// to revisionable and at the same time add revisionable fields to the
// entity type.
if (!empty($change_list['entity_type'])) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
switch ($change_list['entity_type']) {
case static::DEFINITION_CREATED:
$this->entityManager->onEntityTypeCreate($entity_type);
break;
case static::DEFINITION_UPDATED:
$original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
break;
}
$this->doEntityUpdate($change_list['entity_type'], $entity_type_id);
}
// Process field storage definition changes.
@ -127,24 +116,105 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte
$original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id);
foreach ($change_list['field_storage_definitions'] as $field_name => $change) {
switch ($change) {
case static::DEFINITION_CREATED:
$this->entityManager->onFieldStorageDefinitionCreate($storage_definitions[$field_name]);
break;
case static::DEFINITION_UPDATED:
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]);
break;
case static::DEFINITION_DELETED:
$this->entityManager->onFieldStorageDefinitionDelete($original_storage_definitions[$field_name]);
break;
}
$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;
$this->doFieldUpdate($change, $storage_definition, $original_storage_definition);
}
}
}
}
/**
* {@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;
}
/**
* {@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;
}
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();
}
$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;
$this->doFieldUpdate($op, $storage_definition, $original_storage_definition);
return TRUE;
}
/**
* Performs an entity type definition update.
*
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id
* The entity type ID.
*/
protected function doEntityUpdate($op, $entity_type_id) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
switch ($op) {
case static::DEFINITION_CREATED:
$this->entityManager->onEntityTypeCreate($entity_type);
break;
case static::DEFINITION_UPDATED:
$original = $this->entityManager->getLastInstalledDefinition($entity_type_id);
$this->entityManager->onEntityTypeUpdate($entity_type, $original);
break;
}
}
/**
* Performs a field storage definition update.
*
* @param string $op
* The operation to perform, possible values are static::DEFINITION_CREATED,
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param array|null $storage_definition
* The new field storage definition.
* @param array|null $original_storage_definition
* The original field storage definition.
*/
protected function doFieldUpdate($op, $storage_definition = NULL, $original_storage_definition = NULL) {
switch ($op) {
case static::DEFINITION_CREATED:
$this->entityManager->onFieldStorageDefinitionCreate($storage_definition);
break;
case static::DEFINITION_UPDATED:
$this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original_storage_definition);
break;
case static::DEFINITION_DELETED:
$this->entityManager->onFieldStorageDefinitionDelete($original_storage_definition);
break;
}
}
/**
* Gets a list of changes to entity type and field storage definitions.
*

View file

@ -83,4 +83,68 @@ interface EntityDefinitionUpdateManagerInterface {
*/
public function applyUpdates();
/**
* Performs a single entity definition update.
*
* 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.
*
* @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.
*
* @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.
*/
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE);
/**
* Performs a single field storage definition update.
*
* 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.
*/
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE);
}

View file

@ -11,7 +11,6 @@ use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Component\Utility\SafeMarkup;
/**
* Provides a common base class for entity view and form displays.
@ -262,7 +261,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
// 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(SafeMarkup::format('Missing bundle entity, entity type %type, entity id %bundle.', array('%type' => $bundle_entity_type_id, '%bundle' => $this->bundle)));
throw new \LogicException("Missing bundle entity, entity type $bundle_entity_type_id, entity id {$this->bundle}.");
}
$this->addDependency('config', $bundle_entity->getConfigDependencyName());
}

View file

@ -101,6 +101,12 @@ class EntityForm extends FormBase implements EntityFormInterface {
$this->init($form_state);
}
// Ensure that edit forms have the correct cacheability metadata so they can
// be cached.
if (!$this->entity->isNew()) {
\Drupal::service('renderer')->addCacheableDependency($form, $this->entity);
}
// Retrieve the form array using the possibly updated entity in form state.
$form = $this->form($form, $form_state);
@ -219,7 +225,6 @@ class EntityForm extends FormBase implements EntityFormInterface {
$actions['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Save'),
'#validate' => array('::validate'),
'#submit' => array('::submitForm', '::save'),
);
@ -244,16 +249,6 @@ class EntityForm extends FormBase implements EntityFormInterface {
return $actions;
}
/**
* {@inheritdoc}
*/
public function validate(array $form, FormStateInterface $form_state) {
// @todo Remove this.
// Execute legacy global validation handlers.
$form_state->setValidateHandlers([]);
\Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
}
/**
* {@inheritdoc}
*

View file

@ -9,12 +9,20 @@ namespace Drupal\Core\Entity;
/**
* Builds entity forms.
*
* This is like \Drupal\Core\Form\FormBuilderInterface but instead of looking
* up the form class by class name, it looks up the form class based on the
* entity type and operation.
*/
interface EntityFormBuilderInterface {
/**
* Gets the built and processed entity form for the given entity.
*
* The form may also be retrieved from the cache if the form was built in a
* previous page load. The form is then passed on for processing, validation,
* and submission if there is proper input.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be created or edited.
* @param string $operation
@ -23,7 +31,15 @@ interface EntityFormBuilderInterface {
* @code
* _entity_form: node.book_outline
* @endcode
* where "book_outline" is the value of $operation.
* where "book_outline" is the value of $operation. The class name for the
* form for each operation (edit, delete, etc.) can be found in the form
* section of the handlers entity annotation. For example:
* @code
* handlers = {
* "form" = {
* "delete" = "Drupal\node\Form\NodeDeleteForm",
* @endcode
* Alternatively, the form class can be set from hook_entity_type_build().
* @param array $form_state_additions
* (optional) An associative array used to build the current state of the
* form. Use this to pass additional information to the form, such as the
@ -36,6 +52,11 @@ interface EntityFormBuilderInterface {
*
* @return array
* The processed form for the given entity and operation.
*
* @see \Drupal\Core\Form\FormBuilderInterface::getForm()
* @see \Drupal\Core\Entity\EntityTypeInterface::getFormClass()
* @see \Drupal\Core\Entity\EntityTypeInterface::setFormClass()
* @see system_entity_type_build()
*/
public function getForm(EntityInterface $entity, $operation = 'default', array $form_state_additions = array());

View file

@ -94,19 +94,6 @@ interface EntityFormInterface extends BaseFormIdInterface {
*/
public function buildEntity(array $form, FormStateInterface $form_state);
/**
* Validates the submitted form values of the entity form.
*
* @param array $form
* A nested array form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Entity\ContentEntityTypeInterface
* The built entity.
*/
public function validate(array $form, FormStateInterface $form_state);
/**
* Form submission handler for the 'save' action.
*

View file

@ -9,13 +9,14 @@ namespace Drupal\Core\Entity;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
/**
* Defines a common interface for all entity objects.
*
* @ingroup entity_api
*/
interface EntityInterface extends AccessibleInterface, CacheableDependencyInterface {
interface EntityInterface extends AccessibleInterface, CacheableDependencyInterface, RefinableCacheableDependencyInterface {
/**
* Gets the entity UUID (Universally Unique Identifier).
@ -348,6 +349,19 @@ interface EntityInterface extends AccessibleInterface, CacheableDependencyInterf
*/
public function getOriginalId();
/**
* Returns the cache tags that should be used to invalidate caches.
*
* This will not return additional cache tags added through addCacheTags().
*
* @return string[]
* Set of cache tags.
*
* @see \Drupal\Core\Cache\RefinableCacheableDependencyInterface::addCacheTags()
* @see \Drupal\Core\Cache\CacheableDependencyInterface::getCacheTags()
*/
public function getCacheTagsToInvalidate();
/**
* Sets the original ID.
*

View file

@ -41,8 +41,7 @@ interface EntityListBuilderInterface {
* An associative array of operation link data for this list, keyed by
* operation name, containing the following key-value pairs:
* - title: The localized title of the operation.
* - href: The path for the operation.
* - options: An array of URL options for the path.
* - url: An instance of \Drupal\Core\Url for the operation URL.
* - weight: The weight of this operation.
*/
public function getOperations(EntityInterface $entity);

View file

@ -9,12 +9,12 @@ namespace Drupal\Core\Entity;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
use Drupal\Core\Entity\Exception\InvalidLinkTemplateException;
use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@ -226,6 +226,21 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
$this->handlers = array();
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
/** @var \Drupal\Core\Entity\EntityTypeInterface $definition */
parent::processDefinition($definition, $plugin_id);
// All link templates must have a leading slash.
foreach ((array) $definition->getLinkTemplates() as $link_relation_name => $link_template) {
if ($link_template[0] != '/') {
throw new InvalidLinkTemplateException("Link template '$link_relation_name' for entity type '$plugin_id' must start with a leading slash, the current link template is '$link_template'");
}
}
}
/**
* {@inheritdoc}
*/
@ -409,7 +424,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// Fail with an exception for non-fieldable entity types.
if (!$entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
throw new \LogicException(SafeMarkup::format('Getting the base fields is not supported for entity type @type.', array('@type' => $entity_type->getLabel())));
throw new \LogicException("Getting the base fields is not supported for entity type {$entity_type->getLabel()}.");
}
// Retrieve base field definitions.
@ -477,28 +492,19 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
// translatable values.
foreach (array_intersect_key($keys, array_flip(['id', 'revision', 'uuid', 'bundle'])) as $key => $field_name) {
if (!isset($base_field_definitions[$field_name])) {
throw new \LogicException(SafeMarkup::format('The @field field definition does not exist and it is used as @key entity key.', array(
'@field' => $field_name,
'@key' => $key,
)));
throw new \LogicException("The $field_name field definition does not exist and it is used as $key entity key.");
}
if ($base_field_definitions[$field_name]->isRevisionable()) {
throw new \LogicException(SafeMarkup::format('The @field field cannot be revisionable as it is used as @key entity key.', array(
'@field' => $base_field_definitions[$field_name]->getLabel(),
'@key' => $key,
)));
throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be revisionable as it is used as $key entity key.");
}
if ($base_field_definitions[$field_name]->isTranslatable()) {
throw new \LogicException(SafeMarkup::format('The @field field cannot be translatable as it is used as @key entity key.', array(
'@field' => $base_field_definitions[$field_name]->getLabel(),
'@key' => $key,
)));
throw new \LogicException("The {$base_field_definitions[$field_name]->getLabel()} field cannot be translatable as it is used as $key entity key.");
}
}
// Make sure translatable entity types define the "langcode" field properly.
if ($entity_type->isTranslatable() && (!isset($keys['langcode']) || !isset($base_field_definitions[$keys['langcode']]) || !$base_field_definitions[$keys['langcode']]->isTranslatable())) {
throw new \LogicException(SafeMarkup::format('The @entity_type entity type cannot be translatable as it does not define a translatable "langcode" field.', array('@entity_type' => $entity_type->getLabel())));
throw new \LogicException("The {$entity_type->getLabel()} entity type cannot be translatable as it does not define a translatable \"langcode\" field.");
}
return $base_field_definitions;
@ -969,6 +975,7 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
if (empty($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
$entity->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]);
}
// Retrieve language fallback candidates to perform the entity language

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\Query\QueryInterface;
/**
@ -382,6 +381,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,14 +416,12 @@ 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) {
throw new EntityStorageException(SafeMarkup::format('@type entity with ID @id already exists.', array('@type' => $this->entityTypeId, '@id' => $id)));
if ($id_exists && $entity->isNew()) {
throw new EntityStorageException("'{$this->entityTypeId}' entity with ID '$id' already exists.");
}
// Load the original entity, if any.
@ -408,25 +433,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 +450,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

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Exception\EntityTypeIdLengthException;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@ -245,12 +244,7 @@ class EntityType implements EntityTypeInterface {
public function __construct($definition) {
// Throw an exception if the entity type ID is longer than 32 characters.
if (Unicode::strlen($definition['id']) > static::ID_MAX_LENGTH) {
throw new EntityTypeIdLengthException(SafeMarkup::format(
'Attempt to create an entity type with an ID longer than @max characters: @id.', array(
'@max' => static::ID_MAX_LENGTH,
'@id' => $definition['id'],
)
));
throw new EntityTypeIdLengthException('Attempt to create an entity type with an ID longer than ' . static::ID_MAX_LENGTH . " characters: {$definition['id']}.");
}
foreach ($definition as $property => $value) {

View file

@ -117,14 +117,10 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
* {@inheritdoc}
*/
public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
if (!isset($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
}
$build_list = array(
'#sorted' => TRUE,
'#pre_render' => array(array($this, 'buildMultiple')),
'#langcode' => $langcode,
'#langcode' => $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(),
);
$weight = 0;
foreach ($entities as $key => $entity) {
@ -133,9 +129,10 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
$entity = $this->entityManager->getTranslationFromContext($entity, $langcode);
// Set build defaults.
$build_list[$key] = $this->getBuildDefaults($entity, $view_mode, $langcode);
$entity_langcode = $entity->language()->getId();
$build_list[$key] = $this->getBuildDefaults($entity, $view_mode, $entity_langcode);
$entityType = $this->entityTypeId;
$this->moduleHandler()->alter(array($entityType . '_build_defaults', 'entity_build_defaults'), $build_list[$key], $entity, $view_mode, $langcode);
$this->moduleHandler()->alter(array($entityType . '_build_defaults', 'entity_build_defaults'), $build_list[$key], $entity, $view_mode, $entity_langcode);
$build_list[$key]['#weight'] = $weight++;
}
@ -368,7 +365,8 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf
if (isset($entities)) {
$tags = [];
foreach ($entities as $entity) {
$tags = Cache::mergeTags($tags, $entity->getCacheTags(), $entity->getEntityType()->getListCacheTags());
$tags = Cache::mergeTags($tags, $entity->getCacheTags());
$tags = Cache::mergeTags($tags, $entity->getEntityType()->getListCacheTags());
}
Cache::invalidateTags($tags);
}

View file

@ -0,0 +1,14 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Exception\InvalidLinkTemplateException.
*/
namespace Drupal\Core\Entity\Exception;
/**
* Indicates that a link template does not follow the required pattern.
*/
class InvalidLinkTemplateException extends \Exception {
}

View file

@ -175,7 +175,7 @@ interface FieldableEntityInterface extends EntityInterface {
public function set($field_name, $value, $notify = TRUE);
/**
* Gets an array of field item lists.
* Gets an array of all field item lists.
*
* @param bool $include_computed
* If set to TRUE, computed fields are included. Defaults to TRUE.
@ -185,6 +185,17 @@ interface FieldableEntityInterface extends EntityInterface {
*/
public function getFields($include_computed = TRUE);
/**
* Gets an array of field item lists for translatable fields.
*
* @param bool $include_computed
* If set to TRUE, computed fields are included. Defaults to TRUE.
*
* @return \Drupal\Core\Field\FieldItemListInterface[]
* An array of field item lists implementing, keyed by field name.
*/
public function getTranslatableFields($include_computed = TRUE);
/**
* Reacts to changes to a field.
*
@ -212,4 +223,22 @@ interface FieldableEntityInterface extends EntityInterface {
*/
public function validate();
/**
* Checks whether entity validation is required before saving the entity.
*
* @return bool
* TRUE if validation is required, FALSE if not.
*/
public function isValidationRequired();
/**
* Sets whether entity validation is required before saving the entity.
*
* @param bool $required
* TRUE if validation is required, FALSE otherwise.
*
* @return $this
*/
public function setValidationRequired($required);
}

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity\KeyValueStore;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\FieldableEntityInterface;
@ -167,10 +166,7 @@ class KeyValueEntityStorage extends EntityStorageBase {
// @todo This is not config-specific, but serial IDs will likely never hit
// this limit. Consider renaming the exception class.
if (strlen($entity->id()) > static::MAX_ID_LENGTH) {
throw new ConfigEntityIdLengthException(SafeMarkup::format('Entity ID @id exceeds maximum allowed length of @length characters.', array(
'@id' => $entity->id(),
'@length' => static::MAX_ID_LENGTH,
)));
throw new ConfigEntityIdLengthException("Entity ID {$entity->id()} exceeds maximum allowed length of " . static::MAX_ID_LENGTH . ' characters.');
}
return parent::save($entity);
}

View file

@ -81,12 +81,12 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
*/
public function get($property_name) {
if (!isset($this->entity)) {
throw new MissingDataException(SafeMarkup::format('Unable to get property @name as no entity has been provided.', array('@name' => $property_name)));
throw new MissingDataException("Unable to get property $property_name as no entity has been provided.");
}
if (!$this->entity instanceof FieldableEntityInterface) {
// @todo: Add support for config entities in
// https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException(SafeMarkup::format('Unable to get unknown property @name.', array('@name' => $property_name)));
throw new \InvalidArgumentException("Unable to get unknown property $property_name.");
}
// This will throw an exception for unknown fields.
return $this->entity->get($property_name);
@ -97,12 +97,12 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
*/
public function set($property_name, $value, $notify = TRUE) {
if (!isset($this->entity)) {
throw new MissingDataException(SafeMarkup::format('Unable to set property @name as no entity has been provided.', array('@name' => $property_name)));
throw new MissingDataException("Unable to set property $property_name as no entity has been provided.");
}
if (!$this->entity instanceof FieldableEntityInterface) {
// @todo: Add support for config entities in
// https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException(SafeMarkup::format('Unable to set unknown property @name.', array('@name' => $property_name)));
throw new \InvalidArgumentException("Unable to set unknown property $property_name.");
}
// This will throw an exception for unknown fields.
$this->entity->set($property_name, $value, $notify);
@ -129,7 +129,7 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
*/
public function toArray() {
if (!isset($this->entity)) {
throw new MissingDataException(SafeMarkup::format('Unable to get property values as no entity has been provided.'));
throw new MissingDataException('Unable to get property values as no entity has been provided.');
}
return $this->entity->toArray();
}
@ -151,13 +151,6 @@ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexData
}
}
/**
* {@inheritdoc}
*/
public function getDataDefinition() {
return $this->definition;
}
/**
* {@inheritdoc}
*/

View file

@ -36,15 +36,27 @@ abstract class ConditionFundamentals {
*/
protected $query;
/**
* List of potential namespaces of the classes belonging to this condition.
*
* @var array
*/
protected $namespaces = array();
/**
* Constructs a Condition object.
*
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
* @param QueryInterface $query
* The entity query this condition belongs to.
* @param array $namespaces
* List of potential namespaces of the classes belonging to this condition.
*/
public function __construct($conjunction, QueryInterface $query) {
public function __construct($conjunction, QueryInterface $query, $namespaces = []) {
$this->conjunction = $conjunction;
$this->query = $query;
$this->namespaces = $namespaces;
}
/**

View file

@ -7,10 +7,10 @@
namespace Drupal\Core\Entity\Query\Sql;
use Drupal\Core\Entity\Query\ConditionBase;
use Drupal\Core\Entity\Query\ConditionInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Query\Condition as SqlCondition;
use Drupal\Core\Entity\Query\ConditionBase;
use Drupal\Core\Entity\Query\ConditionInterface;
/**
* Implements entity query conditions for SQL databases.
@ -28,6 +28,7 @@ class Condition extends ConditionBase {
* {@inheritdoc}
*/
public function compile($conditionContainer) {
// If this is not the top level condition group then the sql query is
// added to the $conditionContainer object by this function itself. The
// SQL query object is only necessary to pass to Query::addField() so it
@ -41,13 +42,21 @@ class Condition extends ConditionBase {
// Add the SQL query to the object before calling this method again.
$sql_condition->sqlQuery = $sql_query;
$condition['field']->compile($sql_condition);
$sql_query->condition($sql_condition);
$conditionContainer->condition($sql_condition);
}
else {
$type = strtoupper($this->conjunction) == 'OR' || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER';
$field = $tables->addField($condition['field'], $type, $condition['langcode']);
$condition['real_field'] = $field;
static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
$conditionContainer->condition($field, $condition['value'], $condition['operator']);
// Add the translated conditions back to the condition container.
if (isset($condition['where']) && isset($condition['where_args'])) {
$conditionContainer->where($condition['where'], $condition['where_args']);
}
else {
$conditionContainer->condition($field, $condition['value'], $condition['operator']);
}
}
}
}
@ -80,10 +89,11 @@ class Condition extends ConditionBase {
* @see \Drupal\Core\Database\Query\ConditionInterface::condition()
*/
public static function translateCondition(&$condition, SelectInterface $sql_query, $case_sensitive) {
// There is nothing we can do for IN ().
// // There is nothing we can do for IN ().
if (is_array($condition['value'])) {
return;
}
// Ensure that the default operator is set to simplify the cases below.
if (empty($condition['operator'])) {
$condition['operator'] = '=';

View file

@ -11,6 +11,7 @@ use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Query\ConditionAggregateBase;
use Drupal\Core\Entity\Query\ConditionAggregateInterface;
use Drupal\Core\Database\Query\Condition as SqlCondition;
use Drupal\Core\Entity\Query\QueryBase;
/**
* Defines the aggregate condition for sql based storage.
@ -39,7 +40,8 @@ class ConditionAggregate extends ConditionAggregateBase {
else {
$type = ((strtoupper($this->conjunction) == 'OR') || ($condition['operator'] == 'IS NULL')) ? 'LEFT' : 'INNER';
$field = $tables->addField($condition['field'], $type, $condition['langcode']);
Condition::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
$condition_class = QueryBase::getClass($this->namespaces, 'Condition');
$condition_class::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field']));
$function = $condition['function'];
$placeholder = ':db_placeholder_' . $conditionContainer->nextPlaceholder();
$conditionContainer->having("$function($field) {$condition['operator']} $placeholder", array($placeholder => $condition['value']));

View file

@ -52,7 +52,8 @@ class QueryAggregate extends Query implements QueryAggregateInterface {
* Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditionAggregateGroupFactory().
*/
public function conditionAggregateGroupFactory($conjunction = 'AND') {
return new ConditionAggregate($conjunction, $this);
$class = static::getClass($this->namespaces, 'ConditionAggregate');
return new $class($conjunction, $this, $this->namespaces);
}
/**

View file

@ -215,7 +215,7 @@ class Tables implements TablesInterface {
$index_prefix .= "$next_index_prefix.";
}
else {
throw new QueryException(format_string('Invalid specifier @next.', array('@next' => $relationship_specifier)));
throw new QueryException("Invalid specifier '$relationship_specifier'");
}
}
}
@ -247,7 +247,7 @@ class Tables implements TablesInterface {
return $this->entityTables[$index_prefix . $table];
}
}
throw new QueryException(format_string('@property not found', array('@property' => $property)));
throw new QueryException("'$property' not found");
}
/**

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\Sql\pgsql\Condition.
*/
namespace Drupal\Core\Entity\Query\Sql\pgsql;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\Query\Sql\Condition as BaseCondition;
/**
* Implements entity query conditions for PostgreSQL databases.
*/
class Condition extends BaseCondition {
/**
* {@inheritdoc}
*/
public static function translateCondition(&$condition, SelectInterface $sql_query, $case_sensitive) {
if (is_array($condition['value']) && $case_sensitive === FALSE) {
$condition['where'] = 'LOWER(' . $sql_query->escapeField($condition['real_field']) . ') ' . $condition['operator'] . ' (';
$condition['where_args'] = [];
$n = 1;
// Only use the array values in case an associative array is passed as an
// argument following similar pattern in
// \Drupal\Core\Database\Connection::expandArguments().
foreach ($condition['value'] as $value) {
$condition['where'] .= 'LOWER(:value' . $n . '),';
$condition['where_args'][':value' . $n] = $value;
$n++;
}
$condition['where'] = trim($condition['where'], ',');
$condition['where'] .= ')';
return;
}
parent::translateCondition($condition, $sql_query, $case_sensitive);
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory.
*/
namespace Drupal\Core\Entity\Query\Sql\pgsql;
use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory;
/**
* PostgreSQL specific entity query implementation.
*
* To add a new query implementation extending the default SQL one, add
* a service definition like pgsql.entity.query.sql and a factory class like
* this. The system will automatically find the relevant Query, QueryAggregate,
* Condition, ConditionAggregate, Tables classes in this namespace, in the
* namespace of the parent class and so on. So after creating an empty query
* factory class like this, it is possible to just drop in a class extending
* the base class in this namespace and it will be used automatically but it
* is optional: if a class is not extended the relevant default is used.
*
* @see \Drupal\Core\Entity\Query\QueryBase::getNamespaces()
* @see \Drupal\Core\Entity\Query\QueryBase::getClass()
*/
class QueryFactory extends BaseQueryFactory {
}

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
@ -178,7 +177,7 @@ class DefaultTableMapping implements TableMappingInterface {
}
if (!isset($result)) {
throw new SqlContentEntityStorageException(SafeMarkup::format('Table information not available for the "@field_name" field.', array('@field_name' => $field_name)));
throw new SqlContentEntityStorageException("Table information not available for the '$field_name' field.");
}
return $result;
@ -211,7 +210,7 @@ class DefaultTableMapping implements TableMappingInterface {
$column_name = !in_array($property_name, $this->getReservedColumns()) ? $field_name . '_' . $property_name : $property_name;
}
else {
throw new SqlContentEntityStorageException(SafeMarkup::format('Column information not available for the "@field_name" field.', array('@field_name' => $field_name)));
throw new SqlContentEntityStorageException("Column information not available for the '$field_name' field.");
}
return $column_name;

View file

@ -7,8 +7,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 +20,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 +106,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 +113,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
*/
protected $storageSchema;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* The language manager.
*
@ -176,10 +159,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();
}
@ -284,7 +265,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$this->initTableLayout();
}
else {
throw new EntityStorageException(SafeMarkup::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
}
}
@ -414,8 +395,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 +430,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 +565,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 +575,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 +707,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 +733,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
}
foreach ($entities as $entity) {
$this->invokeFieldMethod('delete', $entity);
$this->deleteFromDedicatedTables($entity);
}
}
@ -915,9 +743,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 +759,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 +866,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.
@ -1070,7 +923,7 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
if (empty($this->getFieldStorageDefinitions()[$field_name])) {
throw new EntityStorageException(SafeMarkup::format('Table mapping contains invalid field %field.', array('%field' => $field_name)));
throw new EntityStorageException("Table mapping contains invalid field $field_name.");
}
$definition = $this->getFieldStorageDefinitions()[$field_name];
$columns = $table_mapping->getColumnNames($field_name);
@ -1303,8 +1156,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 +1175,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;
@ -1760,9 +1622,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
$or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $property_name));
}
$query->condition($or);
$query
->fields('t', array($this->idKey))
->distinct(TRUE);
if (!$as_bool) {
$query
->fields('t', array($this->idKey))
->distinct(TRUE);
}
}
// @todo Find a way to count field data also for fields having custom
@ -1772,7 +1636,9 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
// If we are performing the query just to check if the field has data
// limit the number of rows.
if ($as_bool) {
$query->range(0, 1);
$query
->range(0, 1)
->addExpression('1');
}
else {
// Otherwise count the number of rows.

View file

@ -7,7 +7,6 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Entity\ContentEntityTypeInterface;
@ -286,7 +285,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// If a migration is required, we can't proceed.
if ($this->requiresEntityDataMigration($entity_type, $original)) {
throw new EntityStorageException(SafeMarkup::format('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 with data.');
}
// If we have no data just recreate the entity schema from scratch.
@ -467,7 +466,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
*/
protected function checkEntityType(EntityTypeInterface $entity_type) {
if ($entity_type->id() != $this->entityType->id()) {
throw new EntityStorageException(SafeMarkup::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
}
return TRUE;
}
@ -530,7 +529,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
}
foreach ($table_mapping->getFieldNames($table_name) as $field_name) {
if (!isset($storage_definitions[$field_name])) {
throw new FieldException(SafeMarkup::format('Field storage definition for "@field_name" could not be found.', array('@field_name' => $field_name)));
throw new FieldException("Field storage definition for '$field_name' could not be found.");
}
// Add the schema for base field definitions.
elseif ($table_mapping->allowsSharedTableStorage($storage_definitions[$field_name])) {
@ -906,7 +905,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$schema = array(
'description' => "The data table for $entity_type_id entities.",
'primary key' => array($id_key, $entity_type->getKey('langcode')),
'indexes' => array(),
'indexes' => array(
$entity_type_id . '__id__default_langcode__langcode' => array($id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')),
),
'foreign keys' => array(
$entity_type_id => array(
'table' => $this->storage->getBaseTable(),
@ -942,7 +943,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$schema = array(
'description' => "The revision data table for $entity_type_id entities.",
'primary key' => array($revision_key, $entity_type->getKey('langcode')),
'indexes' => array(),
'indexes' => array(
$entity_type_id . '__id__default_langcode__langcode' => array($id_key, $entity_type->getKey('default_langcode'), $entity_type->getKey('langcode')),
),
'foreign keys' => array(
$entity_type_id => array(
'table' => $this->storage->getBaseTable(),
@ -1017,6 +1020,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* A partial schema array for the base table.
*/
protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
// Marking the respective fields as NOT NULL makes the indexes more
// performant.
$schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
}
/**
@ -1031,6 +1037,9 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
* A partial schema array for the base table.
*/
protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
// Marking the respective fields as NOT NULL makes the indexes more
// performant.
$schema['fields'][$entity_type->getKey('default_langcode')]['not null'] = TRUE;
}
/**
@ -1420,7 +1429,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
// Check that the schema does not include forbidden column names.
if (array_intersect(array_keys($field_schema['columns']), $this->storage->getTableMapping()->getReservedColumns())) {
throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName())));
throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
}
$field_name = $storage_definition->getName();
@ -1642,7 +1651,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage
$properties = $storage_definition->getPropertyDefinitions();
$table_mapping = $this->storage->getTableMapping();
if (array_intersect(array_keys($schema['columns']), $table_mapping->getReservedColumns())) {
throw new FieldException(format_string('Illegal field column names on @field_name', array('@field_name' => $storage_definition->getName())));
throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
}
// Add field columns.

View file

@ -1808,13 +1808,15 @@ function hook_entity_field_storage_info_alter(&$fields, \Drupal\Core\Entity\Enti
*
* @return array
* An operations array as returned by
* \Drupal\Core\Entity\EntityListBuilderInterface::getOperations().
* EntityListBuilderInterface::getOperations().
*
* @see \Drupal\Core\Entity\EntityListBuilderInterface::getOperations()
*/
function hook_entity_operation(\Drupal\Core\Entity\EntityInterface $entity) {
$operations = array();
$operations['translate'] = array(
'title' => t('Translate'),
'route_name' => 'foo_module.entity.translate',
'url' => \Drupal\Core\Url::fromRoute('foo_module.entity.translate'),
'weight' => 50,
);