Commit 37afef00 authored by webchick's avatar webchick

Issue #1818556 by das-peter, plach, fago, Berdir, vijaycs85: Change notice:...

Issue #1818556 by das-peter, plach, fago, Berdir, vijaycs85: Change notice: Convert nodes to the new Entity Field API.
parent 6bcbd0ae
......@@ -96,6 +96,8 @@ public function __construct($entityType) {
*/
public function create(array $values) {
// We have to determine the bundle first.
// @todo Throw an exception if no bundle is passed and we have a bundle key
// defined.
$bundle = $this->bundleKey ? $values[$this->bundleKey] : FALSE;
$entity = new $this->entityClass(array(), $this->entityType, $bundle);
......@@ -154,7 +156,7 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
// Map the loaded stdclass records into entity objects and according fields.
$queried_entities = $this->mapFromStorageRecords($queried_entities, $load_revision);
// Attach fields.
// Activate backward-compatibility mode to attach fields.
if ($this->entityInfo['fieldable']) {
// Prepare BC compatible entities before passing them to the field API.
$bc_entities = array();
......@@ -251,7 +253,10 @@ protected function attachPropertyData(array &$entities, $load_revision = FALSE)
foreach ($field_definition as $name => $definition) {
// Set translatable properties only.
if (isset($data_fields[$name]) && !empty($definition['translatable'])) {
$translation->{$name}->value = $values[$name];
// @todo Figure out how to determine which property has to be set.
// Currently it's guessing, and guessing is evil!
$property_definition = $translation->{$name}->getPropertyDefinitions();
$translation->{$name}->{key($property_definition)} = $values[$name];
}
// Avoid initializing configurable fields before loading them.
elseif (!empty($definition['configurable'])) {
......@@ -270,6 +275,12 @@ protected function attachPropertyData(array &$entities, $load_revision = FALSE)
public function save(EntityInterface $entity) {
$transaction = db_transaction();
try {
// Ensure we are dealing with the actual entity.
$entity = $entity->getOriginalEntity();
// Sync the changes made in the fields array to the internal values array.
$entity->updateOriginalValues();
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
$entity->original = entity_load_unchanged($this->entityType, $entity->id());
......@@ -279,7 +290,7 @@ public function save(EntityInterface $entity) {
$this->invokeHook('presave', $entity);
// Create the storage record to be saved.
$record = $this->maptoStorageRecord($entity);
$record = $this->mapToStorageRecord($entity);
if (!$entity->isNew()) {
if ($entity->isDefaultRevision()) {
......@@ -446,7 +457,9 @@ protected function mapToStorageRecord(EntityInterface $entity) {
protected function mapToRevisionStorageRecord(EntityInterface $entity) {
$record = new \stdClass();
foreach ($this->entityInfo['schema_fields_sql']['revision_table'] as $name) {
$record->$name = $entity->$name->value;
if (isset($entity->$name->value)) {
$record->$name = $entity->$name->value;
}
}
return $record;
}
......@@ -489,6 +502,11 @@ public function delete(array $entities) {
$transaction = db_transaction();
try {
// Ensure we are dealing with the actual entities.
foreach ($entities as $id => $entity) {
$entities[$id] = $entity->getOriginalEntity();
}
$this->preDelete($entities);
foreach ($entities as $id => $entity) {
$this->invokeHook('predelete', $entity);
......
......@@ -45,14 +45,24 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface {
*/
protected $decorated;
/**
* Local cache for field definitions.
*
* @var array
*/
protected $definitions;
/**
* Constructs a Drupal\Core\Entity\EntityCompatibilityDecorator object.
*
* @param \Drupal\Core\Entity\EntityInterface $decorated
* The decorated entity.
* @param array &$definitions
* An array of field definitions.
*/
function __construct(EntityNG $decorated) {
function __construct(EntityNG $decorated, array &$definitions) {
$this->decorated = $decorated;
$this->definitions = &$definitions;
}
/**
......@@ -75,6 +85,11 @@ public function getBCEntity() {
* Directly accesses the plain field values, as done in Drupal 7.
*/
public function &__get($name) {
// Directly return the original property.
if ($name == 'original') {
return $this->decorated->values[$name];
}
// We access the protected 'values' and 'fields' properties of the decorated
// entity via the magic getter - which returns them by reference for us. We
// do so, as providing references to these arrays would make $entity->values
......@@ -88,7 +103,10 @@ public function &__get($name) {
// the field objects managed by the entity, thus we need to ensure
// $this->decorated->values reflects the latest values first.
foreach ($this->decorated->fields[$name] as $langcode => $field) {
$this->decorated->values[$name][$langcode] = $field->getValue();
// Only set if it's not empty, otherwise there can be ghost values.
if (!$field->isEmpty()) {
$this->decorated->values[$name][$langcode] = $field->getValue();
}
}
// The returned values might be changed by reference, so we need to remove
// the field object to avoid the field object and the value getting out of
......@@ -96,21 +114,39 @@ public function &__get($name) {
// receive the possibly updated value.
unset($this->decorated->fields[$name]);
}
// Allow accessing field values in entity default language other than
// LANGUAGE_DEFAULT by mapping the values to LANGUAGE_DEFAULT. This is
// necessary as EntityNG does key values in default language always with
// LANGUAGE_DEFAULT while field API expects them to be keyed by langcode.
$langcode = $this->decorated->language()->langcode;
if ($langcode != LANGUAGE_DEFAULT && isset($this->decorated->values[$name]) && is_array($this->decorated->values[$name])) {
if (isset($this->decorated->values[$name][LANGUAGE_DEFAULT]) && !isset($this->decorated->values[$name][$langcode])) {
$this->decorated->values[$name][$langcode] = &$this->decorated->values[$name][LANGUAGE_DEFAULT];
// When accessing values for entity properties that have been converted to
// an entity field, provide direct access to the plain value. This makes it
// possible to use the BC-decorator with properties; e.g., $node->title.
if (isset($this->definitions[$name]) && empty($this->definitions[$name]['configurable'])) {
if (!isset($this->decorated->values[$name][LANGUAGE_DEFAULT])) {
$this->decorated->values[$name][LANGUAGE_DEFAULT][0]['value'] = NULL;
}
if (is_array($this->decorated->values[$name][LANGUAGE_DEFAULT])) {
// This will work with all defined properties that have a single value.
// We need to ensure the key doesn't matter. Mostly it's 'value' but
// e.g. EntityReferenceItem uses target_id.
if (isset($this->decorated->values[$name][LANGUAGE_DEFAULT][0]) && count($this->decorated->values[$name][LANGUAGE_DEFAULT][0]) == 1) {
return $this->decorated->values[$name][LANGUAGE_DEFAULT][0][key($this->decorated->values[$name][LANGUAGE_DEFAULT][0])];
}
}
return $this->decorated->values[$name][LANGUAGE_DEFAULT];
}
if (!isset($this->decorated->values[$name])) {
$this->decorated->values[$name] = NULL;
else {
// Allow accessing field values in an entity default language other than
// LANGUAGE_DEFAULT by mapping the values to LANGUAGE_DEFAULT. This is
// necessary as EntityNG always keys default language values with
// LANGUAGE_DEFAULT while field API expects them to be keyed by langcode.
$langcode = $this->decorated->language()->langcode;
if ($langcode != LANGUAGE_DEFAULT && isset($this->decorated->values[$name]) && is_array($this->decorated->values[$name])) {
if (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];
}
return $this->decorated->values[$name];
}
/**
......@@ -119,20 +155,29 @@ public function &__get($name) {
* 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.
// This is necessary as EntityNG does key values in default language always
// with LANGUAGE_DEFAULT while field API expects them to be keyed by
// langcode.
foreach ($value as $langcode => $data) {
if ($langcode != LANGUAGE_DEFAULT && $langcode == $this->decorated->language()->langcode) {
$value[LANGUAGE_DEFAULT] = $data;
unset($value[$langcode]);
$defined = isset($this->definitions[$name]);
// When updating values for entity properties that have been converted to
// an entity field, directly write to the plain value. This makes it
// possible to use the BC-decorator with properties; e.g., $node->title.
if ($defined && empty($this->definitions[$name]['configurable'])) {
$this->decorated->values[$name][LANGUAGE_DEFAULT] = $value;
}
else {
if ($defined && is_array($value)) {
// If field API sets a value with a langcode in entity language, move it
// to LANGUAGE_DEFAULT.
// This is necessary as EntityNG always keys default language values
// with LANGUAGE_DEFAULT while field API expects them to be keyed by
// langcode.
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;
}
$this->decorated->values[$name] = $value;
// Remove the field object to avoid the field object and the value getting
// out of sync. That way, the next field object instantiated by EntityNG
// will hold the updated value.
......@@ -151,8 +196,9 @@ public function __isset($name) {
* Implements the magic method for unset().
*/
public function __unset($name) {
// Set the value to NULL.
$value = &$this->__get($name);
$value = array();
$value = NULL;
}
/**
......@@ -173,6 +219,10 @@ public function access($operation = 'view', \Drupal\user\Plugin\Core\Entity\User
* Forwards the call to the decorated entity.
*/
public function get($property_name) {
// Ensure this works with not yet defined fields.
if (!isset($this->definitions[$property_name])) {
return $this->__get($property_name);
}
return $this->decorated->get($property_name);
}
......@@ -180,6 +230,10 @@ public function get($property_name) {
* Forwards the call to the decorated entity.
*/
public function set($property_name, $value) {
// Ensure this works with not yet defined fields.
if (!isset($this->definitions[$property_name])) {
return $this->__set($property_name, $value);
}
return $this->decorated->set($property_name, $value);
}
......
......@@ -81,6 +81,11 @@ public function __construct(array $values, $entity_type, $bundle = FALSE) {
$this->entityType = $entity_type;
$this->bundle = $bundle ? $bundle : $this->entityType;
foreach ($values as $key => $value) {
// If the key matches an existing property set the value to the property
// to ensure non converted properties have the correct value.
if (property_exists($this, $key) && isset($value[LANGUAGE_DEFAULT])) {
$this->$key = $value[LANGUAGE_DEFAULT];
}
$this->values[$key] = $value;
}
$this->init();
......@@ -269,7 +274,7 @@ public function language() {
if ($this->getPropertyDefinition('langcode')) {
$language = $this->get('langcode')->language;
}
if (!isset($language)) {
if (empty($language)) {
// Make sure we return a proper language object.
$language = new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED));
}
......@@ -367,7 +372,9 @@ public function translations() {
*/
public function getBCEntity() {
if (!isset($this->bcEntity)) {
$this->bcEntity = new EntityBCDecorator($this);
// Initialize field definitions so that we can pass them by reference.
$this->getPropertyDefinitions();
$this->bcEntity = new EntityBCDecorator($this, $this->fieldDefinitions);
}
return $this->bcEntity;
}
......@@ -483,6 +490,12 @@ public function createDuplicate() {
$uuid = new Uuid();
$duplicate->{$entity_info['entity_keys']['uuid']}->value = $uuid->generate();
}
// Check whether the entity type supports revisions and initialize it if so.
if (!empty($entity_info['entity_keys']['revision'])) {
$duplicate->{$entity_info['entity_keys']['revision']}->value = NULL;
}
return $duplicate;
}
......
......@@ -36,6 +36,15 @@ abstract class FieldItemBase extends ContextAwareTypedData implements IteratorAg
*/
protected $properties = array();
/**
* Holds any non-property values that get set on the object.
*
* @todo: Remove or refactor once EntityNG conversion is complete.
*
* @var array
*/
protected $extraValues = array();
/**
* Overrides ContextAwareTypedData::__construct().
*/
......@@ -68,7 +77,7 @@ public function getValue() {
foreach ($this->getProperties() as $name => $property) {
$values[$name] = $property->getValue();
}
return $values;
return $values + $this->extraValues;
}
/**
......@@ -92,9 +101,11 @@ public function setValue($values) {
else {
$property->setValue(NULL);
}
unset($values[$name]);
}
// @todo: Throw an exception for invalid values once conversion is
// totally completed.
$this->extraValues = $values;
}
/**
......@@ -129,7 +140,13 @@ public function set($property_name, $value) {
* Implements \Drupal\Core\Entity\Field\FieldItemInterface::__get().
*/
public function __get($name) {
return $this->get($name)->getValue();
if (isset($this->properties[$name])) {
return $this->properties[$name]->getValue();
}
// The property is unknown, so try to get it from the extra values.
elseif (isset($this->extraValues[$name])) {
return $this->extraValues[$name];
}
}
/**
......@@ -140,7 +157,13 @@ public function __set($name, $value) {
if ($value instanceof TypedDataInterface) {
$value = $value->getValue();
}
$this->get($name)->setValue($value);
if (isset($this->properties[$name])) {
$this->properties[$name]->setValue($value);
}
else {
// The property is unknown, so set it to the extra values.
$this->extraValues[$name] = $value;
}
}
/**
......
......@@ -85,6 +85,21 @@ public function setValue($values) {
}
}
/**
* Overrides \Drupal\Core\Entity\Field\FieldItemBase::__set().
*/
public function __set($name, $value) {
parent::__set($name, $value);
}
/**
* Overrides \Drupal\Core\Entity\Field\FieldItemBase::__get().
*/
public function __get($name) {
$name = ($name == 'value') ? 'target_id' : $name;
return parent::__get($name);
}
/**
* Overrides \Drupal\Core\Entity\Field\FieldItemBase::get().
*/
......@@ -92,4 +107,13 @@ public function get($property_name) {
$property_name = ($property_name == 'value') ? 'target_id' : $property_name;
return parent::get($property_name);
}
/**
* Implements \Drupal\Core\Entity\Field\FieldItemInterface::__isset().
*/
public function __isset($property_name) {
$property_name = ($property_name == 'value') ? 'target_id' : $property_name;
return parent::__isset($property_name);
}
}
......@@ -53,6 +53,13 @@ class EntityWrapper extends ContextAwareTypedData implements IteratorAggregate,
*/
protected $id;
/**
* If set, a new entity to create and reference.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $newEntity;
/**
* Overrides ContextAwareTypedData::__construct().
*/
......@@ -65,6 +72,9 @@ public function __construct(array $definition, $name = NULL, ContextAwareInterfa
* Overrides \Drupal\Core\TypedData\TypedData::getValue().
*/
public function getValue() {
if (isset($this->newEntity)) {
return $this->newEntity;
}
$source = $this->getIdSource();
$id = $source ? $source->getValue() : $this->id;
return $id ? entity_load($this->entityType, $id) : NULL;
......@@ -85,10 +95,17 @@ protected function getIdSource() {
* Both the entity ID and the entity object may be passed as value.
*/
public function setValue($value) {
// Support passing in the entity object.
if ($value instanceof EntityInterface) {
// Support passing in the entity object. If it's not yet saved we have
// to store the whole entity such that it could be saved later on.
if ($value instanceof EntityInterface && $value->isNew()) {
$this->newEntity = $value;
$this->entityType = $value->entityType();
$value = FALSE;
}
elseif ($value instanceof EntityInterface) {
$this->entityType = $value->entityType();
$value = $value->id();
unset($this->newEntity);
}
elseif (isset($value) && !(is_scalar($value) && !empty($this->definition['constraints']['EntityType']))) {
throw new InvalidArgumentException('Value is not a valid entity.');
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\TypedData\Type;
use InvalidArgumentException;
use Drupal\Core\Language\Language as LanguageObject;
use Drupal\Core\TypedData\ContextAwareTypedData;
/**
......@@ -40,7 +41,8 @@ public function getValue() {
$source = $this->getLanguageCodeSource();
$langcode = $source ? $source->getValue() : $this->langcode;
if ($langcode) {
return language_load($langcode);
$language = language_load($langcode);
return $language ?: new LanguageObject(array('langcode' => $this->langcode));
}
}
......
......@@ -5,7 +5,7 @@
* Administration page callbacks for the Book module.
*/
use Drupal\node\Plugin\Core\Entity\Node;
use Drupal\Core\Entity\EntityInterface;
/**
* Page callback: Returns an administrative overview of all books.
......@@ -45,7 +45,7 @@ function book_admin_overview() {
/**
* Form constructor for administering a single book's hierarchy.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node of the top-level page in the book.
*
* @see book_menu()
......@@ -53,7 +53,7 @@ function book_admin_overview() {
* @see book_admin_edit_submit()
* @ingroup forms
*/
function book_admin_edit($form, $form_state, Node $node) {
function book_admin_edit($form, $form_state, EntityInterface $node) {
drupal_set_title($node->label());
$form['#node'] = $node;
_book_admin_table($node, $form);
......@@ -136,14 +136,14 @@ function book_admin_edit_submit($form, &$form_state) {
/**
* Builds the table portion of the form for the book administration page.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node of the top-level page in the book.
* @param $form
* The form that is being modified, passed by reference.
*
* @see book_admin_edit()
*/
function _book_admin_table(Node $node, &$form) {
function _book_admin_table(EntityInterface $node, &$form) {
$form['table'] = array(
'#theme' => 'book_admin_table',
'#tree' => TRUE,
......
......@@ -5,7 +5,7 @@
* Allows users to create and organize related content in an outline.
*/
use Drupal\node\Plugin\Core\Entity\Node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
use Drupal\Core\Template\Attribute;
use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
......@@ -89,12 +89,12 @@ function book_permission() {
/**
* Adds relevant book links to the node's links.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The book page node to add links to.
* @param $view_mode
* The view mode of the node.
*/
function book_node_view_link(Node $node, $view_mode) {
function book_node_view_link(EntityInterface $node, $view_mode) {
$links = array();
if (isset($node->book['depth'])) {
......@@ -199,10 +199,10 @@ function book_menu() {
/**
* Access callback: Determines if the book export page is accessible.
*
* @param \Drupal\node\Plugin\Core\Entity\Node $node
* @param \Drupal\node\Plugin\Core\Entity\EntityInterface $node
* The node whose export page is to be viewed.
*/
function book_export_access(Node $node) {
function book_export_access(EntityInterface $node) {
return user_access('access printer-friendly version') && node_access('view', $node);
}
......@@ -213,24 +213,24 @@ function book_export_access(Node $node) {
* - admin/content/book/%node
* - node/%node/outline
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node whose outline tab is to be viewed.
*
* @see book_menu()
*/
function _book_outline_access(Node $node) {
function _book_outline_access(EntityInterface $node) {
return user_access('administer book outlines') && node_access('view', $node);
}
/**
* Access callback: Determines if the user can remove nodes from the outline.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node to remove from the outline.
*
* @see book_menu()
*/
function _book_outline_remove_access(Node $node) {
function _book_outline_remove_access(EntityInterface $node) {
return _book_node_is_removable($node) && _book_outline_access($node);
}
......@@ -240,10 +240,10 @@ function _book_outline_remove_access(Node $node) {
* A node can be removed from a book if it is actually in a book and it either
* is not a top-level page or is a top-level page with no children.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node to remove from the outline.
*/
function _book_node_is_removable($node) {
function _book_node_is_removable(EntityInterface $node) {
return (!empty($node->book['bid']) && (($node->book['bid'] != $node->nid) || !$node->book['has_children']));
}
......@@ -420,10 +420,10 @@ function _book_parent_select($book_link) {
/**
* Builds the common elements of the book form for the node and outline forms.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node whose form is being viewed.
*/
function _book_add_form_elements(&$form, &$form_state, Node $node) {
function _book_add_form_elements(&$form, &$form_state, EntityInterface $node) {
// If the form is being processed during the Ajax callback of our book bid
// dropdown, then $form_state will hold the value that was selected.
if (isset($form_state['values']['book'])) {
......@@ -523,13 +523,13 @@ function book_form_update($form, $form_state) {
* outline through node addition, node editing, node deletion, or the outline
* tab.
*
* @param Drupal\node\Node $node
* @param \Drupal\Core\Entity\EntityInterface $node
* The node that is being saved, added, deleted, or moved.
*
* @return
* TRUE if the menu link was saved; FALSE otherwise.
*/
function _book_update_outline(Node $node) {