Commit 6caeaf03 authored by webchick's avatar webchick

Issue #2002138 by yched, Jose Reyero, xjm, andypost, fago, msonnabaum, Berdir,...

Issue #2002138 by yched, Jose Reyero, xjm, andypost, fago, msonnabaum, Berdir, dixon_: Use adapters for supporting typed data.
parent e01fb4e9
......@@ -9,7 +9,6 @@
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TypedDataInterface;
......@@ -130,13 +129,6 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
*/
protected $entityKeys = array();
/**
* The instantiated entity data definition.
*
* @var \Drupal\Core\Entity\TypedData\EntityDataDefinition
*/
protected $dataDefinition;
/**
* Overrides Entity::__construct().
*/
......@@ -258,101 +250,11 @@ public function isTranslatable() {
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
}
/**
* {@inheritdoc}
*/
public function getDataDefinition() {
if (!$this->dataDefinition) {
$this->dataDefinition = EntityDataDefinition::create($this->getEntityTypeId());
$this->dataDefinition->setBundles(array($this->bundle()));
}
return $this->dataDefinition;
}
/**
* {@inheritdoc}
*/
public function getValue() {
// @todo: This does not make much sense, so remove once TypedDataInterface
// is removed. See https://drupal.org/node/2002138.
return $this->toArray();
}
/**
* {@inheritdoc}
*/
public function setValue($value, $notify = TRUE) {
// @todo: This does not make much sense, so remove once TypedDataInterface
// is removed. See https://drupal.org/node/2002138.
foreach ($value as $field_name => $field_value) {
$this->set($field_name, $field_value, $notify);
}
}
/**
* {@inheritdoc}
*/
public function getString() {
return (string) $this->label();
}
/**
* {@inheritdoc}
*/
public function validate() {
return $this->typedDataManager()->getValidator()->validate($this);
}
/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
foreach ($this->getProperties() as $property) {
$property->applyDefaultValue(FALSE);
}
}
/**
* {@inheritdoc}
*/
public function getConstraints() {
return array();
}
/**
* {@inheritdoc}
*/
public function getName() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function getRoot() {
return $this;
}
/**
* {@inheritdoc}
*/
public function getPropertyPath() {
return '';
}
/**
* {@inheritdoc}
*/
public function getParent() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function setContext($name = NULL, TypedDataInterface $parent = NULL) {
// As entities are always the root of the tree of typed data, we do not need
// to set any parent or name.
return $this->getTypedData()->validate();
}
/**
......@@ -376,7 +278,6 @@ public function __sleep() {
}
$this->fields = array();
$this->fieldDefinitions = NULL;
$this->dataDefinition = NULL;
$this->clearTranslationCache();
return parent::__sleep();
......@@ -413,11 +314,11 @@ public function hasField($field_name) {
/**
* {@inheritdoc}
*/
public function get($property_name) {
if (!isset($this->fields[$property_name][$this->activeLangcode])) {
return $this->getTranslatedField($property_name, $this->activeLangcode);
public function get($field_name) {
if (!isset($this->fields[$field_name][$this->activeLangcode])) {
return $this->getTranslatedField($field_name, $this->activeLangcode);
}
return $this->fields[$property_name][$this->activeLangcode];
return $this->fields[$field_name][$this->activeLangcode];
}
/**
......@@ -452,7 +353,8 @@ protected function getTranslatedField($name, $langcode) {
if (isset($this->values[$name][$langcode])) {
$value = $this->values[$name][$langcode];
}
$field = \Drupal::typedDataManager()->getPropertyInstance($this, $name, $value);
$entity_adapter = $this->getTypedData();
$field = \Drupal::typedDataManager()->getPropertyInstance($entity_adapter, $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
......@@ -481,21 +383,21 @@ public function set($name, $value, $notify = TRUE) {
/**
* {@inheritdoc}
*/
public function getProperties($include_computed = FALSE) {
$properties = array();
public function getFields($include_computed = FALSE) {
$fields = array();
foreach ($this->getFieldDefinitions() as $name => $definition) {
if ($include_computed || !$definition->isComputed()) {
$properties[$name] = $this->get($name);
$fields[$name] = $this->get($name);
}
}
return $properties;
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIterator() {
return new \ArrayIterator($this->getProperties());
return new \ArrayIterator($this->getFields());
}
/**
......@@ -525,27 +427,12 @@ public function getFieldDefinitions() {
*/
public function toArray() {
$values = array();
foreach ($this->getProperties() as $name => $property) {
foreach ($this->getFields() as $name => $property) {
$values[$name] = $property->getValue();
}
return $values;
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
if (!$this->isNew()) {
return FALSE;
}
foreach ($this->getProperties() as $property) {
if ($property->getValue() !== NULL) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
......@@ -644,8 +531,6 @@ public function onChange($name) {
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Entity\ContentEntityInterface
*/
public function getTranslation($langcode) {
// Ensure we always use the default language code when dealing with the
......@@ -825,15 +710,6 @@ public function getTranslationLanguages($include_default = TRUE) {
return array_intersect_key($this->languages, $translations);
}
/**
* Overrides Entity::translations().
*
* @todo: Remove once Entity::translations() gets removed.
*/
public function translations() {
return $this->getTranslationLanguages(FALSE);
}
/**
* Updates the original values with the interim changes.
*/
......@@ -957,8 +833,6 @@ public function createDuplicate() {
$duplicate->{$entity_type->getKey('revision')}->value = NULL;
}
$duplicate->entityKeys = array();
return $duplicate;
}
......@@ -969,6 +843,8 @@ public function __clone() {
// Avoid deep-cloning when we are initializing a translation object, since
// it will represent the same entity, only with a different active language.
if (!$this->translationInitialize) {
// The translation is a different object, and needs its own TypedData object.
$this->typedData = NULL;
$definitions = $this->getFieldDefinitions();
foreach ($this->fields as $name => $values) {
$this->fields[$name] = array();
......@@ -981,7 +857,7 @@ public function __clone() {
}
foreach ($values as $langcode => $items) {
$this->fields[$name][$langcode] = clone $items;
$this->fields[$name][$langcode]->setContext($name, $this);
$this->fields[$name][$langcode]->setContext($name, $this->getTypedData());
}
}
......@@ -1015,11 +891,11 @@ public function referencedEntities() {
$referenced_entities = array();
// Gather a list of referenced entities.
foreach ($this->getProperties() as $field_items) {
foreach ($this->getFields() as $field_items) {
foreach ($field_items as $field_item) {
// Loop over all properties of a field item.
foreach ($field_item->getProperties(TRUE) as $property) {
if ($property instanceof EntityReference && $entity = $property->getTarget()) {
if ($property instanceof EntityReference && $entity = $property->getValue()) {
$referenced_entities[] = $entity;
}
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Entity;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TranslatableInterface;
/**
......@@ -28,7 +27,7 @@
*
* @ingroup entity_api
*/
interface ContentEntityInterface extends EntityInterface, RevisionableInterface, TranslatableInterface, ComplexDataInterface {
interface ContentEntityInterface extends EntityInterface, RevisionableInterface, TranslatableInterface {
/**
* Marks the translation identified by the given language code as existing.
......@@ -152,4 +151,67 @@ public function getFieldDefinitions();
*/
public function toArray();
/**
* Gets a field item list.
*
* @param string $field_name
* The name of the field to get; e.g., 'title' or 'name'.
*
* @throws \InvalidArgumentException
* If an invalid field name is given.
*
* @return \Drupal\Core\Field\FieldItemListInterface
* The field item list, containing the field items.
*/
public function get($field_name);
/**
* Sets a field value.
*
* @param string $field_name
* The name of the field to set; e.g., 'title' or 'name'.
* @param mixed $value
* The value to set, or NULL to unset the field.
* @param bool $notify
* (optional) Whether to notify the entity of the change. Defaults to
* TRUE. If the update stems from the entity, set it to FALSE to avoid
* being notified again.
*
* @throws \InvalidArgumentException
* If the specified field does not exist.
*
* @return $this
*/
public function set($field_name, $value, $notify = TRUE);
/**
* Gets an array of field item lists.
*
* @param bool $include_computed
* If set to TRUE, computed fields are included. Defaults to FALSE.
*
* @return \Drupal\Core\Field\FieldItemListInterface[]
* An array of field item lists implementing, keyed by field name.
*/
public function getFields($include_computed = FALSE);
/**
* Reacts to changes to a field.
*
* Note that this is invoked after any changes have been applied.
*
* @param string $field_name
* The name of the field which is changed.
*/
public function onChange($field_name);
/**
* Validates the currently set values.
*
* @return \Symfony\Component\Validator\ConstraintViolationListInterface
* A list of constraint violations. If the list is empty, validation
* succeeded.
*/
public function validate();
}
......@@ -195,7 +195,7 @@ protected function invokeTranslationHooks(ContentEntityInterface $entity) {
protected function invokeFieldMethod($method, ContentEntityInterface $entity) {
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
$translation = $entity->getTranslation($langcode);
foreach ($translation->getProperties(TRUE) as $field) {
foreach ($translation->getFields(TRUE) as $field) {
$field->$method();
}
}
......
......@@ -12,8 +12,6 @@
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\Exception\ConfigEntityIdLengthException;
use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
use Drupal\Core\Entity\Exception\UndefinedLinkTemplateException;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
......@@ -24,7 +22,10 @@
* Defines a base entity class.
*/
abstract class Entity implements EntityInterface {
use DependencySerializationTrait;
use DependencySerializationTrait {
__sleep as traitSleep;
}
/**
* The entity type.
......@@ -40,6 +41,13 @@ abstract class Entity implements EntityInterface {
*/
protected $enforceIsNew;
/**
* A typed data object wrapping this entity.
*
* @var \Drupal\Core\TypedData\ComplexDataInterface
*/
protected $typedData;
/**
* Constructs an Entity object.
*
......@@ -547,4 +555,23 @@ public function toArray() {
return array();
}
/**
* {@inheritdoc}
*/
public function getTypedData() {
if (!isset($this->typedData)) {
$class = \Drupal::typedDataManager()->getDefinition('entity')['class'];
$this->typedData = $class::createFromEntity($this);
}
return $this->typedData;
}
/**
* {@inheritdoc}
*/
public function __sleep() {
$this->typedData = NULL;
return $this->traitSleep();
}
}
......@@ -361,6 +361,19 @@ public function setOriginalId($id);
*/
public function toArray();
/**
* Returns a typed data object for this entity object.
*
* The returned typed data object wraps this entity and allows dealing with
* entities based on the generic typed data API.
*
* @return \Drupal\Core\TypedData\ComplexDataInterface
* The typed data object for this entity.
*
* @see \Drupal\Core\TypedData\TypedDataInterface
*/
public function getTypedData();
/**
* The unique cache tag associated with this entity.
*
......
......@@ -83,7 +83,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
$this->derivatives[$entity_type_id] = array(
'label' => $entity_type->getLabel(),
'class' => $entity_type->getClass(),
'constraints' => array('EntityType' => $entity_type_id),
) + $base_plugin_definition;
......@@ -92,7 +91,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
if ($bundle !== $entity_type_id) {
$this->derivatives[$entity_type_id . ':' . $bundle] = array(
'label' => $bundle_info['label'],
'class' => $entity_type->getClass(),
'constraints' => array(
'EntityType' => $entity_type_id,
'Bundle' => $bundle,
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Plugin\DataType\Entity.
*/
namespace Drupal\Core\Entity\Plugin\DataType;
/**
* Defines the base plugin for deriving data types for entity types.
*
* Note that the class only registers the plugin, and is actually never used.
* \Drupal\Core\Entity\Entity is available for use as base class.
*
* @DataType(
* id = "entity",
* label = @Translation("Entity"),
* description = @Translation("All kind of entities, e.g. nodes, comments or users."),
* deriver = "\Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver",
* definition_class = "\Drupal\Core\Entity\TypedData\EntityDataDefinition"
* )
*/
abstract class Entity {
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Plugin\DataType\EntityAdapter.
*/
namespace Drupal\Core\Entity\Plugin\DataType;
use Drupal\Component\Utility\String;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\Exception\MissingDataException;
use Drupal\Core\TypedData\TypedData;
/**
* Defines the "entity" data type.
*
* Instances of this class wrap entity objects and allow to deal with entities
* based upon the Typed Data API.
*
* In addition to the "entity" data type, this exposes derived
* "entity:$entity_type" and "entity:$entity_type:$bundle" data types.
*
* @DataType(
* id = "entity",
* label = @Translation("Entity"),
* description = @Translation("All kind of entities, e.g. nodes, comments or users."),
* deriver = "\Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver",
* definition_class = "\Drupal\Core\Entity\TypedData\EntityDataDefinition"
* )
*/
class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexDataInterface {
/**
* The wrapped entity object.
*
* @var \Drupal\Core\Entity\EntityInterface|null
*/
protected $entity;
/**
* Creates an instance wrapping the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface|null $entity
* The entity object to wrap.
*
* @return static
*/
public static function createFromEntity(EntityInterface $entity) {
$definition = EntityDataDefinition::create()
->setEntityTypeId($entity->getEntityTypeId())
->setBundles([ $entity->bundle() ]);
$instance = new static($definition);
$instance->setValue($entity);
return $instance;
}
/**
* {@inheritdoc}
*/
public function getValue() {
return $this->entity;
}
/**
* {@inheritdoc}
*/
public function setValue($entity, $notify = TRUE) {
$this->entity = $entity;
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* {@inheritdoc}
*/
public function get($property_name) {
if (!isset($this->entity)) {
throw new MissingDataException(String::format('Unable to get property @name as no entity has been provided.', array('@name' => $property_name)));
}
if (!$this->entity instanceof ContentEntityInterface) {
// @todo: Add support for config entities in
// https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException(String::format('Unable to get unknown property @name.', array('@name' => $property_name)));
}
// This will throw an exception for unknown fields.
return $this->entity->get($property_name);
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value, $notify = TRUE) {
if (!isset($this->entity)) {
throw new MissingDataException(String::format('Unable to set property @name as no entity has been provided.', array('@name' => $property_name)));
}
if (!$this->entity instanceof ContentEntityInterface) {
// @todo: Add support for config entities in
// https://www.drupal.org/node/1818574.
throw new \InvalidArgumentException(String::format('Unable to set unknown property @name.', array('@name' => $property_name)));
}
// This will throw an exception for unknown fields.
return $this->entity->set($property_name, $value, $notify);
}
/**
* {@inheritdoc}
*/
public function getProperties($include_computed = FALSE) {
if (!isset($this->entity)) {
throw new MissingDataException(String::format('Unable to get properties as no entity has been provided.'));
}
if (!$this->entity instanceof ContentEntityInterface) {
// @todo: Add support for config entities in
// https://www.drupal.org/node/1818574.
return array();
}
return $this->entity->getFields($include_computed);
}
/**
* {@inheritdoc}
*/
public function toArray() {
if (!isset($this->entity)) {
throw new MissingDataException(String::format('Unable to get property values as no entity has been provided.'));
}
return $this->entity->toArray();
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return !isset($this->entity);
}
/**
* {@inheritdoc}
*/
public function onChange($property_name) {
if (isset($this->entity) && $this->entity instanceof ContentEntityInterface) {
// Let the entity know of any changes.
$this->entity->onChange($property_name);
}
}
/**
* {@inheritdoc}
*/
public function getDataDefinition() {
return $this->definition;
}
/**
* {@inheritdoc}
*/
public function getString() {
return isset($this->entity) ? $this->entity->label() : '';
}
/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
// Apply the default value of all properties.
foreach ($this->getProperties() as $property) {
$property->applyDefaultValue(FALSE);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getIterator() {
return isset($this->entity) ? $this->entity->getIterator() : new \ArrayIterator([]);
}
}
......@@ -60,9 +60,9 @@ public function getTargetDefinition() {
*/
public function getTarget() {
if (!isset($this->target) && isset($this->id)) {
// If we have a valid reference, return the entity object which is typed
// data itself.
$this->target = entity_load($this->getTargetDefinition()->getEntityTypeId(), $this->id);
// If we have a valid reference, return the entity's TypedData adapter.
$entity = entity_load($this->getTargetDefinition()->getEntityTypeId(), $this->id);
$this->target = isset($entity) ? $entity->getTypedData() : NULL;
}
return $this->target;
}
......@@ -79,14 +79,6 @@ public function getTargetIdentifier() {
}
}
/**
* {@inheritdoc}
*/
public function getValue() {
// Entities are already typed data, so just return that.
return $this->getTarget();
}
/**
* {@inheritdoc}
*/
......@@ -96,8 +88,11 @@ public function setValue($value, $notify = TRUE) {
// Both the entity ID and the entity object may be passed as value. The
// reference may also be unset by passing NULL as value.
if (!isset($value) || $value instanceof EntityInterface) {
$this->target = $value;