Commit 2090a96a authored by effulgentsia's avatar effulgentsia

Issue #2382675 by plach, yched, alexpott: hook_entity_create() affects the...

Issue #2382675 by plach, yched, alexpott: hook_entity_create() affects the data of new translations of existing entities in unexpected and undocumented ways
parent 83fdfe97
......@@ -69,9 +69,6 @@ public function __construct($definition) {
// Always add a default 'uuid' key.
$this->entity_keys['uuid'] = 'uuid';
$this->entity_keys['langcode'] = 'langcode';
if (isset($this->handlers['storage'])) {
$this->checkStorageClass($this->handlers['storage']);
}
$this->handlers += array(
'storage' => 'Drupal\Core\Config\Entity\ConfigEntityStorage',
);
......@@ -135,23 +132,12 @@ public function getConfigDependencyKey() {
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Config\Entity\ConfigEntityStorage.
*
* @throws \Drupal\Core\Config\Entity\Exception\ConfigEntityStorageClassException
* Exception thrown when the provided class is not an instance of
* \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
public function setStorageClass($class) {
$this->checkStorageClass($class);
parent::setStorageClass($class);
}
/**
* Checks that the provided class is an instance of ConfigEntityStorage.
*
* @param string $class
* The class to check.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityStorage.
*/
protected function checkStorageClass($class) {
if (!is_a($class, 'Drupal\Core\Config\Entity\ConfigEntityStorage', TRUE)) {
throw new ConfigEntityStorageClassException("$class is not \\Drupal\\Core\\Config\\Entity\\ConfigEntityStorage or it does not extend it");
......
......@@ -808,6 +808,13 @@ public function hasTranslation($langcode) {
return !empty($this->translations[$langcode]['status']);
}
/**
* {@inheritdoc}
*/
public function isNewTranslation() {
return $this->translations[$this->activeLangcode]['status'] == static::TRANSLATION_CREATED;
}
/**
* {@inheritdoc}
*/
......@@ -822,37 +829,11 @@ public function addTranslation($langcode, array $values = array()) {
throw new \InvalidArgumentException("The entity cannot be translated since it is language neutral ({$this->defaultLangcode}).");
}
// Instantiate a new empty entity so default values will be populated in the
// specified language.
$entity_type = $this->getEntityType();
$default_values = array(
$entity_type->getKey('bundle') => $this->bundle(),
$this->langcodeKey => $langcode,
);
$entity = $this->entityManager()
->getStorage($this->getEntityTypeId())
->create($default_values);
foreach ($entity as $name => $field) {
if (!isset($values[$name]) && !$field->isEmpty()) {
$values[$name] = $field->getValue();
}
}
$values[$this->langcodeKey] = $langcode;
$values[$this->defaultLangcodeKey] = FALSE;
// Initialize the translation object.
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityManager()->getStorage($this->getEntityTypeId());
$this->translations[$langcode]['status'] = static::TRANSLATION_CREATED;
$translation = $this->getTranslation($langcode);
$definitions = $translation->getFieldDefinitions();
foreach ($values as $name => $value) {
if (isset($definitions[$name]) && $definitions[$name]->isTranslatable()) {
$translation->values[$name][$langcode] = $value;
}
}
return $translation;
return $storage->createTranslation($this, $langcode, $values);
}
/**
......
......@@ -13,7 +13,10 @@
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class ContentEntityStorageBase extends EntityStorageBase implements DynamicallyFieldableEntityStorageInterface {
/**
* Base class for content entity storage handlers.
*/
abstract class ContentEntityStorageBase extends EntityStorageBase implements ContentEntityStorageInterface, DynamicallyFieldableEntityStorageInterface {
/**
* The entity bundle key.
......@@ -87,13 +90,32 @@ protected function doCreate(array $values) {
$bundle = $values[$this->bundleKey];
}
$entity = new $this->entityClass(array(), $this->entityTypeId, $bundle);
$this->initFieldValues($entity, $values);
return $entity;
}
/**
* Initializes field values.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* An entity object.
* @param array $values
* (optional) An associative array of initial field values keyed by field
* name. If none is provided default values will be applied.
* @param array $field_names
* (optional) An associative array of field names to be initialized. If none
* is provided all fields will be initialized.
*/
protected function initFieldValues(ContentEntityInterface $entity, array $values = [], array $field_names = []) {
// Populate field values.
foreach ($entity as $name => $field) {
if (isset($values[$name])) {
$entity->$name = $values[$name];
}
elseif (!array_key_exists($name, $values)) {
$entity->get($name)->applyDefaultValue();
if (!$field_names || isset($field_names[$name])) {
if (isset($values[$name])) {
$entity->$name = $values[$name];
}
elseif (!array_key_exists($name, $values)) {
$entity->get($name)->applyDefaultValue();
}
}
unset($values[$name]);
}
......@@ -102,7 +124,23 @@ protected function doCreate(array $values) {
foreach ($values as $name => $value) {
$entity->$name = $value;
}
return $entity;
// Make sure modules can alter field initial values.
$this->invokeHook('field_values_init', $entity);
}
/**
* {@inheritdoc}
*/
public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
$translation = $entity->getTranslation($langcode);
$definitions = array_filter($translation->getFieldDefinitions(), function(FieldDefinitionInterface $definition) { return $definition->isTranslatable(); });
$field_names = array_map(function(FieldDefinitionInterface $definition) { return $definition->getName(); }, $definitions);
$values[$this->langcodeKey] = $langcode;
$values[$this->getEntityType()->getKey('default_langcode')] = FALSE;
$this->initFieldValues($translation, $values, $field_names);
$this->invokeHook('translation_create', $entity);
return $translation;
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\ContentEntityStorageInterface.
*/
namespace Drupal\Core\Entity;
/**
* A storage that supports content entity types.
*/
interface ContentEntityStorageInterface extends EntityStorageInterface {
/**
* Constructs a new entity translation object, without permanently saving it.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity object being translated.
* @param string $langcode
* The translation language code.
* @param array $values
* (optional) An associative array of initial field values keyed by field
* name. If none is provided default values will be applied.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* A new entity translation object.
*/
public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []);
}
......@@ -30,4 +30,20 @@ public function getConfigDependencyKey() {
return 'content';
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Entity\ContentEntityStorageInterface.
*
* @throws \InvalidArgumentException
* If the provided class does not implement
* \Drupal\Core\Entity\ContentEntityStorageInterface.
*/
protected function checkStorageClass($class) {
$required_interface = ContentEntityStorageInterface::class;
if (!is_subclass_of($class, $required_interface)) {
throw new \InvalidArgumentException("$class does not implement $required_interface");
}
}
}
......@@ -276,6 +276,9 @@ public function __construct($definition) {
$this->handlers += array(
'access' => 'Drupal\Core\Entity\EntityAccessControlHandler',
);
if (isset($this->handlers['storage'])) {
$this->checkStorageClass($this->handlers['storage']);
}
// Automatically add the EntityChanged constraint if the entity type tracks
// the changed time.
......@@ -459,9 +462,20 @@ public function getStorageClass() {
* {@inheritdoc}
*/
public function setStorageClass($class) {
$this->checkStorageClass($class);
$this->handlers['storage'] = $class;
}
/**
* Checks that the provided class is an instance of ConfigEntityStorage.
*
* @param string $class
* The class to check.
*/
protected function checkStorageClass($class) {
// Nothing to check by default.
}
/**
* {@inheritdoc}
*/
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\KeyValueStore\KeyValueContentEntityStorage.
*/
namespace Drupal\Core\Entity\KeyValueStore;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Provides a key value backend for content entities.
*/
class KeyValueContentEntityStorage extends KeyValueEntityStorage implements ContentEntityStorageInterface {
/**
* {@inheritdoc}
*/
public function createTranslation(ContentEntityInterface $entity, $langcode, array $values = []) {
// @todo
}
}
......@@ -780,10 +780,9 @@ function hook_entity_bundle_delete($entity_type_id, $bundle) {
}
/**
* Act on a newly created entity.
* Acts when creating a new entity.
*
* This hook runs after a new entity object has just been instantiated. It can
* be used to set initial values, e.g. to provide defaults.
* This hook runs after a new entity object has just been instantiated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
......@@ -792,16 +791,13 @@ function hook_entity_bundle_delete($entity_type_id, $bundle) {
* @see hook_ENTITY_TYPE_create()
*/
function hook_entity_create(\Drupal\Core\Entity\EntityInterface $entity) {
if ($entity instanceof FieldableEntityInterface && !$entity->foo->value) {
$entity->foo->value = 'some_initial_value';
}
\Drupal::logger('example')->info('Entity created: @label', ['@label' => $entity->label()]);
}
/**
* Act on a newly created entity of a specific type.
* Acts when creating a new entity of a specific type.
*
* This hook runs after a new entity object has just been instantiated. It can
* be used to set initial values, e.g. to provide defaults.
* This hook runs after a new entity object has just been instantiated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
......@@ -810,9 +806,7 @@ function hook_entity_create(\Drupal\Core\Entity\EntityInterface $entity) {
* @see hook_entity_create()
*/
function hook_ENTITY_TYPE_create(\Drupal\Core\Entity\EntityInterface $entity) {
if (!$entity->foo->value) {
$entity->foo->value = 'some_initial_value';
}
\Drupal::logger('example')->info('ENTITY_TYPE created: @label', ['@label' => $entity->label()]);
}
/**
......@@ -1011,6 +1005,38 @@ function hook_ENTITY_TYPE_update(Drupal\Core\Entity\EntityInterface $entity) {
->execute();
}
/**
* Acts when creating a new entity translation.
*
* This hook runs after a new entity translation object has just been
* instantiated.
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity object.
*
* @ingroup entity_crud
* @see hook_ENTITY_TYPE_translation_create()
*/
function hook_entity_translation_create(\Drupal\Core\Entity\EntityInterface $translation) {
\Drupal::logger('example')->info('Entity translation created: @label', ['@label' => $translation->label()]);
}
/**
* Acts when creating a new entity translation of a specific type.
*
* This hook runs after a new entity translation object has just been
* instantiated.
*
* @param \Drupal\Core\Entity\EntityInterface $translation
* The entity object.
*
* @ingroup entity_crud
* @see hook_entity_translation_create()
*/
function hook_ENTITY_TYPE_translation_create(\Drupal\Core\Entity\EntityInterface $translation) {
\Drupal::logger('example')->info('ENTITY_TYPE translation created: @label', ['@label' => $translation->label()]);
}
/**
* Respond to creation of a new entity translation.
*
......@@ -1886,6 +1912,44 @@ function hook_entity_field_access_alter(array &$grants, array $context) {
}
}
/**
* Acts when initializing a fieldable entity object.
*
* This hook runs after a new entity object or a new entity translation object
* has just been instantiated. It can be used to set initial values, e.g. to
* provide defaults.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object.
*
* @ingroup entity_crud
* @see hook_ENTITY_TYPE_field_values_init()
*/
function hook_entity_field_values_init(\Drupal\Core\Entity\FieldableEntityInterface $entity) {
if ($entity instanceof \Drupal\Core\Entity\ContentEntityInterface && !$entity->foo->value) {
$entity->foo->value = 'some_initial_value';
}
}
/**
* Acts when initializing a fieldable entity object.
*
* This hook runs after a new entity object or a new entity translation object
* has just been instantiated. It can be used to set initial values, e.g. to
* provide defaults.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
* The entity object.
*
* @ingroup entity_crud
* @see hook_entity_field_values_init()
*/
function hook_ENTITY_TYPE_field_values_init(\Drupal\Core\Entity\FieldableEntityInterface $entity) {
if (!$entity->foo->value) {
$entity->foo->value = 'some_initial_value';
}
}
/**
* Exposes "pseudo-field" components on content entities.
*
......
......@@ -28,6 +28,14 @@ public function language();
*/
public function isDefaultTranslation();
/**
* Checks whether the translation is new.
*
* @return bool
* TRUE if the translation is new, FALSE otherwise.
*/
public function isNewTranslation();
/**
* Returns the languages the data is translated to.
*
......
......@@ -7,12 +7,12 @@
namespace Drupal\aggregator;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for aggregator feed entity storage classes.
*/
interface FeedStorageInterface extends EntityStorageInterface {
interface FeedStorageInterface extends ContentEntityStorageInterface {
/**
* Returns the fids of feeds that need to be refreshed.
......
......@@ -7,12 +7,12 @@
namespace Drupal\aggregator;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for aggregator item entity storage classes.
*/
interface ItemStorageInterface extends EntityStorageInterface {
interface ItemStorageInterface extends ContentEntityStorageInterface {
/**
* Returns the count of the items in a feed.
......
......@@ -8,13 +8,13 @@
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
/**
* Defines an interface for comment entity storage classes.
*/
interface CommentStorageInterface extends EntityStorageInterface {
interface CommentStorageInterface extends ContentEntityStorageInterface {
/**
* Gets the maximum encoded thread value for the top level comments.
......
......@@ -7,12 +7,12 @@
namespace Drupal\file;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for file entity storage classes.
*/
interface FileStorageInterface extends EntityStorageInterface {
interface FileStorageInterface extends ContentEntityStorageInterface {
/**
* Determines total disk space used by a single user or the whole filesystem.
......
......@@ -7,14 +7,14 @@
namespace Drupal\node;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an interface for node entity storage classes.
*/
interface NodeStorageInterface extends EntityStorageInterface {
interface NodeStorageInterface extends ContentEntityStorageInterface {
/**
* Gets a list of node revision IDs for a specific node.
......
......@@ -328,6 +328,7 @@ protected function doTestEntityTranslationAPI($entity_type) {
// Verify that we obtain the entity object itself when we attempt to
// retrieve a translation referring to it.
$translation = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->assertFalse($translation->isNewTranslation(), 'Existing translations are not marked as new.');
$this->assertIdentical($entity, $translation, 'The translation object corresponding to a non-default language is the entity object itself when the entity is language-neutral.');
$entity->{$langcode_key}->value = $default_langcode;
$translation = $entity->getTranslation($default_langcode);
......@@ -353,6 +354,7 @@ protected function doTestEntityTranslationAPI($entity_type) {
$entity->name->value = $name;
$name_translated = $langcode . '_' . $this->randomMachineName();
$translation = $entity->addTranslation($langcode);
$this->assertTrue($translation->isNewTranslation(), 'Newly added translations are marked as new.');
$this->assertNotIdentical($entity, $translation, 'The entity and the translation object differ from one another.');
$this->assertTrue($entity->hasTranslation($langcode), 'The new translation exists.');
$this->assertEqual($translation->language()->getId(), $langcode, 'The translation language matches the specified one.');
......
......@@ -11,7 +11,7 @@
function keyvalue_test_entity_type_alter(array &$entity_types) {
/** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
if (isset($entity_types['entity_test_label'])) {
$entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueEntityStorage');
$entity_types['entity_test_label']->setStorageClass('Drupal\Core\Entity\KeyValueStore\KeyValueContentEntityStorage');
$entity_keys = $entity_types['entity_test_label']->getKeys();
$entity_types['entity_test_label']->set('entity_keys', $entity_keys + array('uuid' => 'uuid'));
}
......
......@@ -8,12 +8,12 @@
namespace Drupal\taxonomy;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for taxonomy_term entity storage classes.
*/
interface TermStorageInterface extends EntityStorageInterface {
interface TermStorageInterface extends ContentEntityStorageInterface {
/**
* Removed reference to terms from term_hierarchy.
......
......@@ -7,13 +7,13 @@
namespace Drupal\user;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an interface for user entity storage classes.
*/
interface UserStorageInterface extends EntityStorageInterface{
interface UserStorageInterface extends ContentEntityStorageInterface {
/**
* Update the last login timestamp of the user.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment