Commit c96966f0 authored by catch's avatar catch

Issue #1869250 by fago, Berdir, das-peter, YesCT, mradcliffe, fubhy: Various...

Issue #1869250 by fago, Berdir, das-peter, YesCT, mradcliffe, fubhy: Various EntityNG and TypedData API improvements.
parent 85c8d12e
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config\Entity;
use Drupal\Core\Entity\Entity;
use Drupal\Core\TypedData\ContextAwareInterface;
/**
* Defines a base configuration entity class.
......@@ -126,4 +127,53 @@ public function getExportProperties() {
return $properties;
}
/**
* Implements Drupal\Core\Entity\EntityInterface::getBCEntity().
*/
public function getBCEntity() {
return $this;
}
/**
* Implements Drupal\Core\Entity\EntityInterface::getOriginalEntity().
*/
public function getOriginalEntity() {
return $this;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getName().
*/
public function getName() {
return NULL;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getRoot().
*/
public function getRoot() {
return $this;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getPropertyPath().
*/
public function getPropertyPath() {
return '';
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getParent().
*/
public function getParent() {
return NULL;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::setContext().
*/
public function setContext($name = NULL, ContextAwareInterface $parent = NULL) {
// As entities are always the root of the tree, we do not need to set any
// context.
}
}
......@@ -55,6 +55,13 @@ class DatabaseStorageController implements EntityStorageControllerInterface {
*/
protected $entityFieldInfo;
/**
* Static cache of field definitions per bundle.
*
* @var array
*/
protected $fieldDefinitions;
/**
* Additional arguments to pass to hook_TYPE_load().
*
......@@ -687,15 +694,17 @@ public function getFieldDefinitions(array $constraints) {
}
}
$definitions = $this->entityFieldInfo['definitions'];
$bundle = !empty($constraints['bundle']) ? $constraints['bundle'] : FALSE;
// Add in per-bundle properties.
// @todo: Should this be statically cached as well?
if (!empty($constraints['bundle']) && isset($this->entityFieldInfo['bundle map'][$constraints['bundle']])) {
$definitions += array_intersect_key($this->entityFieldInfo['optional'], array_flip($this->entityFieldInfo['bundle map'][$constraints['bundle']]));
}
if (!isset($this->fieldDefinitions[$bundle])) {
$this->fieldDefinitions[$bundle] = $this->entityFieldInfo['definitions'];
return $definitions;
if ($bundle && isset($this->entityFieldInfo['bundle map'][$constraints['bundle']])) {
$this->fieldDefinitions[$bundle] += array_intersect_key($this->entityFieldInfo['optional'], array_flip($this->entityFieldInfo['bundle map'][$constraints['bundle']]));
}
}
return $this->fieldDefinitions[$bundle];
}
/**
......
......@@ -77,20 +77,17 @@ public function __construct($entityType) {
* A new entity object.
*/
public function create(array $values) {
$entity = new $this->entityClass(array(), $this->entityType);
// We have to determine the bundle first.
$bundle = $this->bundleKey ? $values[$this->bundleKey] : FALSE;
$entity = new $this->entityClass(array(), $this->entityType, $bundle);
// Make sure to set the bundle first.
if ($this->bundleKey) {
$entity->{$this->bundleKey} = $values[$this->bundleKey];
unset($values[$this->bundleKey]);
}
// Set all other given values.
foreach ($values as $name => $value) {
$entity->$name = $value;
}
// Assign a new UUID if there is none yet.
if ($this->uuidKey && !isset($entity->{$this->uuidKey})) {
if ($this->uuidKey && !isset($entity->{$this->uuidKey}->value)) {
$uuid = new Uuid();
$entity->{$this->uuidKey}->value = $uuid->generate();
}
......@@ -109,19 +106,20 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Attach fields.
if ($this->entityInfo['fieldable']) {
// Prepare BC compatible entities for field API.
$bc_entities = array();
foreach ($queried_entities as $key => $entity) {
$bc_entities[$key] = $entity->getBCEntity();
}
if ($load_revision) {
field_attach_load_revision($this->entityType, $queried_entities);
field_attach_load_revision($this->entityType, $bc_entities);
}
else {
field_attach_load($this->entityType, $queried_entities);
field_attach_load($this->entityType, $bc_entities);
}
}
// Loading is finished, so disable compatibility mode now.
foreach ($queried_entities as $entity) {
$entity->setCompatibilityMode(FALSE);
}
// Call hook_entity_load().
foreach (module_implements('entity_load') as $module) {
$function = $module . '_entity_load';
......@@ -150,13 +148,14 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
protected function mapFromStorageRecords(array $records, $load_revision = FALSE) {
foreach ($records as $id => $record) {
$entity = new $this->entityClass(array(), $this->entityType);
$entity->setCompatibilityMode(TRUE);
$values = array();
foreach ($record as $name => $value) {
$entity->{$name}[LANGUAGE_DEFAULT][0]['value'] = $value;
// Avoid unnecessary array hierarchies to save memory.
$values[$name][LANGUAGE_DEFAULT] = $value;
}
$records[$id] = $entity;
$bundle = $this->bundleKey ? $record->{$this->bundleKey} : FALSE;
// Turn the record into an entity class.
$records[$id] = new $this->entityClass($values, $this->entityType, $bundle);
}
return $records;
}
......@@ -179,11 +178,6 @@ public function save(EntityInterface $entity) {
// Create the storage record to be saved.
$record = $this->maptoStorageRecord($entity);
// Update the original values so that the compatibility mode works with
// the update values, what is required by field API attachers.
// @todo Once field API has been converted to use the Field API, move
// this after insert/update hooks.
$entity->updateOriginalValues();
if (!$entity->isNew()) {
if ($entity->isDefaultRevision()) {
......@@ -215,6 +209,7 @@ public function save(EntityInterface $entity) {
$this->postSave($entity, FALSE);
$this->invokeHook('insert', $entity);
}
$entity->updateOriginalValues();
// Ignore slave server temporarily.
db_ignore_slave();
......@@ -270,8 +265,7 @@ protected function saveRevision(EntityInterface $entity) {
/**
* Overrides DatabaseStorageController::invokeHook().
*
* Invokes field API attachers in compatibility mode and disables it
* afterwards.
* Invokes field API attachers with a BC entity.
*/
protected function invokeHook($hook, EntityInterface $entity) {
$function = 'field_attach_' . $hook;
......@@ -281,9 +275,7 @@ protected function invokeHook($hook, EntityInterface $entity) {
$function = 'field_attach_delete_revision';
}
if (!empty($this->entityInfo['fieldable']) && function_exists($function)) {
$entity->setCompatibilityMode(TRUE);
$function($this->entityType, $entity);
$entity->setCompatibilityMode(FALSE);
$function($this->entityType, $entity->getBCEntity());
}
// Invoke the hook.
......@@ -311,7 +303,6 @@ protected function mapToRevisionStorageRecord(EntityInterface $entity) {
foreach ($this->entityInfo['schema_fields_sql']['revision_table'] as $name) {
$record->$name = $entity->$name->value;
}
return $record;
}
}
......@@ -9,6 +9,7 @@
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\ContextAwareInterface;
use IteratorAggregate;
/**
......@@ -379,4 +380,53 @@ public function getExportProperties() {
return array();
}
/**
* Implements Drupal\Core\Entity\EntityInterface::getBCEntity().
*/
public function getBCEntity() {
return $this;
}
/**
* Implements Drupal\Core\Entity\EntityInterface::getOriginalEntity().
*/
public function getOriginalEntity() {
return $this;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getName().
*/
public function getName() {
return NULL;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getRoot().
*/
public function getRoot() {
return $this;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getPropertyPath().
*/
public function getPropertyPath() {
return '';
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::getParent().
*/
public function getParent() {
return NULL;
}
/**
* Implements \Drupal\Core\TypedData\ContextAwareInterface::setContext().
*/
public function setContext($name = NULL, ContextAwareInterface $parent = NULL) {
// As entities are always the root of the tree, we do not need to set any
// context.
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityBCDecorator.
*/
namespace Drupal\Core\Entity;
use IteratorAggregate;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
/**
* Implements a decorator providing backwards compatible entity field access.
*
* Allows using entities converted to the new Entity Field API with the Drupal 7
* way of accessing fields or properties.
*
* Note: We access the protected 'values' and 'fields' properties of the entity
* via the magic getter - which returns them by reference for us. We do so, as
* providing references to this arrays makes $entity->values and $entity->fields
* to references itself as well, which is problematic during __clone() (this is
* something that would not be easy to fix as an unset() on the variable is
* problematic with the magic getter/setter then).
*/
class EntityBCDecorator implements IteratorAggregate, EntityInterface {
/**
* The EntityInterface object being decorated.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $decorated;
/**
* Constructs a Drupal\Core\Entity\EntityCompatibilityDecorator object.
*
* @param \Drupal\Core\Entity\EntityInterface $decorated
* The decorated entity.
*/
function __construct(EntityNG $decorated) {
$this->decorated = $decorated;
}
/**
* Overrides Entity::getOriginalEntity().
*/
public function getOriginalEntity() {
return $this->decorated;
}
/**
* Overrides Entity::getBCEntity().
*/
public function getBCEntity() {
return $this;
}
/**
* Implements the magic method for getting object properties.
*
* Directly accesses the plain field values, as done in Drupal 7.
*/
public function &__get($name) {
// Make sure $this->decorated->values reflects the latest values.
if (!empty($this->decorated->fields[$name])) {
foreach ($this->decorated->fields[$name] as $langcode => $field) {
$this->decorated->values[$name][$langcode] = $field->getValue();
}
// Values might be changed by reference, so remove the field object to
// avoid them becoming out of sync.
unset($this->decorated->fields[$name]);
}
// Allow accessing field values in entity default languages other than
// LANGUAGE_DEFAULT by mapping the values to LANGUAGE_DEFAULT.
$langcode = $this->decorated->language()->langcode;
if ($langcode != LANGUAGE_DEFAULT && isset($this->decorated->values[$name][LANGUAGE_DEFAULT]) && !isset($this->decorated->values[$name][$langcode])) {
$this->decorated->values[$name][$langcode] = &$this->decorated->values[$name][LANGUAGE_DEFAULT];
}
if (!isset($this->decorated->values[$name])) {
$this->decorated->values[$name] = NULL;
}
return $this->decorated->values[$name];
}
/**
* Implements the magic method for setting object properties.
*
* Directly writes to the plain field values, as done by Drupal 7.
*/
public function __set($name, $value) {
if (is_array($value) && $definition = $this->decorated->getPropertyDefinition($name)) {
// If field API sets a value with a langcode in entity language, move it
// to LANGUAGE_DEFAULT.
foreach ($value as $langcode => $data) {
if ($langcode != LANGUAGE_DEFAULT && $langcode == $this->decorated->language()->langcode) {
$value[LANGUAGE_DEFAULT] = $data;
unset($value[$langcode]);
}
}
}
$this->decorated->values[$name] = $value;
unset($this->decorated->fields[$name]);
}
/**
* Implements the magic method for isset().
*/
public function __isset($name) {
$value = $this->__get($name);
return isset($value);
}
/**
* Implements the magic method for unset().
*/
public function __unset($name) {
$value = &$this->__get($name);
$value = array();
}
/**
* Forwards the call to the decorated entity.
*/
public function access($operation = 'view', \Drupal\user\Plugin\Core\Entity\User $account = NULL) {
return $this->decorated->access($account);
}
/**
* Forwards the call to the decorated entity.
*/
public function get($property_name) {
return $this->decorated->get($property_name);
}
/**
* Forwards the call to the decorated entity.
*/
public function set($property_name, $value) {
return $this->decorated->set($property_name, $value);
}
/**
* Forwards the call to the decorated entity.
*/
public function getProperties($include_computed = FALSE) {
return $this->decorated->getProperties($include_computed);
}
/**
* Forwards the call to the decorated entity.
*/
public function getPropertyValues() {
return $this->decorated->getPropertyValues();
}
/**
* Forwards the call to the decorated entity.
*/
public function setPropertyValues($values) {
return $this->decorated->setPropertyValues($values);
}
/**
* Forwards the call to the decorated entity.
*/
public function getPropertyDefinition($name) {
return $this->decorated->getPropertyDefinition($name);
}
/**
* Forwards the call to the decorated entity.
*/
public function getPropertyDefinitions() {
return $this->decorated->getPropertyDefinitions();
}
/**
* Forwards the call to the decorated entity.
*/
public function isEmpty() {
return $this->decorated->isEmpty();
}
/**
* Forwards the call to the decorated entity.
*/
public function getIterator() {
return $this->decorated->getIterator();
}
/**
* Forwards the call to the decorated entity.
*/
public function id() {
return $this->decorated->id();
}
/**
* Forwards the call to the decorated entity.
*/
public function uuid() {
return $this->decorated->uuid();
}
/**
* Forwards the call to the decorated entity.
*/
public function isNew() {
return $this->decorated->isNew();
}
/**
* Forwards the call to the decorated entity.
*/
public function isNewRevision() {
return $this->decorated->isNewRevision();
}
/**
* Forwards the call to the decorated entity.
*/
public function setNewRevision($value = TRUE) {
return $this->decorated->setNewRevision($value);
}
/**
* Forwards the call to the decorated entity.
*/
public function enforceIsNew($value = TRUE) {
return $this->decorated->enforceIsNew($value);
}
/**
* Forwards the call to the decorated entity.
*/
public function entityType() {
return $this->decorated->entityType();
}
/**
* Forwards the call to the decorated entity.
*/
public function bundle() {
return $this->decorated->bundle();
}
/**
* Forwards the call to the decorated entity.
*/
public function label($langcode = NULL) {
return $this->decorated->label($langcode);
}
/**
* Forwards the call to the decorated entity.
*/
public function uri() {
return $this->decorated->uri();
}
/**
* Forwards the call to the decorated entity.
*/
public function save() {
return $this->decorated->save();
}
/**
* Forwards the call to the decorated entity.
*/
public function delete() {
return $this->decorated->delete();
}
/**
* Forwards the call to the decorated entity.
*/
public function createDuplicate() {
return $this->decorated->createDuplicate();
}
/**
* Forwards the call to the decorated entity.
*/
public function entityInfo() {
return $this->decorated->entityInfo();
}
/**
* Forwards the call to the decorated entity.
*/
public function getRevisionId() {
return $this->decorated->getRevisionId();
}
/**
* Forwards the call to the decorated entity.
*/
public function isDefaultRevision($new_value = NULL) {
return $this->decorated->isDefaultRevision($new_value);
}
/**
* Forwards the call to the decorated entity.
*/
public function language() {
return $this->decorated->language();
}
/**
* Forwards the call to the decorated entity.
*/
public function getTranslationLanguages($include_default = TRUE) {
return $this->decorated->getTranslationLanguages($include_default);
}
/**
* Forwards the call to the decorated entity.
*/
public function getTranslation($langcode, $strict = TRUE) {
return $this->decorated->getTranslation($langcode, $strict);
}
/**
* Forwards the call to the decorated entity.
*/
public function getName() {
return $this->decorated->getName();
}
/**
* Forwards the call to the decorated entity.
*/
public function getRoot() {
return $this->decorated->getRoot();
}
/**
* Forwards the call to the decorated entity.
*/
public function getPropertyPath() {
return $this->decorated->getPropertyPath();
}
/**
* Forwards the call to the decorated entity.
*/
public function getParent() {
return $this->decorated->getParent();
}
/**
* Forwards the call to the decorated entity.
*/
public function setContext($name = NULL, ContextAwareInterface $parent = NULL) {
$this->decorated->setContext($name, $parent);
}
/**
* Forwards the call to the decorated entity.
*/
public function getExportProperties() {
$this->decorated->getExportProperties();
}
}
......@@ -23,9 +23,7 @@ public function form(array $form, array &$form_state, EntityInterface $entity) {
// entity properties.
$info = $entity->entityInfo();
if (!empty($info['fieldable'])) {
$entity->setCompatibilityMode(TRUE);
field_attach_form($entity->entityType(), $entity, $form, $form_state, $this->getFormLangcode($form_state));
$entity->setCompatibilityMode(FALSE);
field_attach_form($entity->entityType(), $entity->getBCEntity(), $form, $form_state, $this->getFormLangcode($form_state));
}
return $form;
}
......@@ -40,9 +38,7 @@ public function validate(array $form, array &$form_state) {
$info = $entity->entityInfo();
if (!empty($info['fieldable'])) {
$entity->setCompatibilityMode(TRUE);
field_attach_form_validate($entity->entityType(), $entity, $form, $form_state);
$entity->setCompatibilityMode(FALSE);
field_attach_form_validate($entity->entityType(), $entity->getBCEntity(), $form, $form_state);
}
// @todo Remove this.
......@@ -82,9 +78,7 @@ public function buildEntity(array $form, array &$form_state) {
// Copy field values to the entity.
if ($info['fieldable']) {
$entity->setCompatibilityMode(TRUE);
field_attach_submit($entity_type, $entity, $form, $form_state);
$entity->setCompatibilityMode(FALSE);
field_attach_submit($entity_type, $entity->getBCEntity(), $form, $form_state);
}
return $entity;
}
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Entity;
use Drupal\Core\TypedData\AccessibleInterface;
use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\TranslatableInterface;
......@@ -17,7 +18,7 @@
* When implementing this interface which extends Traversable, make sure to list
* IteratorAggregate or Iterator before this interface in the implements clause.
*/
interface EntityInterface extends ComplexDataInterface, AccessibleInterface, TranslatableInterface {
interface EntityInterface extends ContextAwareInterface, ComplexDataInterface, AccessibleInterface, TranslatableInterface {
/**
* Returns the entity identifier (the entity's machine name or numeric ID).
......@@ -190,4 +191,23 @@ public function isDefaultRevision($new_value = NULL);
*/
public function getExportProperties();
/**
* Gets a backward compatibility decorator entity.
*