diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 007d6d3..60bc73b 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -80,7 +80,7 @@ public function create(array $values) { $entity = new $this->entityClass(array(), $this->entityType); // Make sure to set the bundle first. - if ($this->bundleKey) { + if ($this->bundleKey && !empty($values[$this->bundleKey])) { $entity->{$this->bundleKey} = $values[$this->bundleKey]; unset($values[$this->bundleKey]); } @@ -92,7 +92,7 @@ public function create(array $values) { // Assign a new UUID if there is none yet. if ($this->uuidKey && !isset($entity->{$this->uuidKey})) { $uuid = new Uuid(); - $entity->{$this->uuidKey}->value = $uuid->generate(); + $entity->set($this->uuidKey, $uuid->generate()); } return $entity; } @@ -149,7 +149,12 @@ protected function mapFromStorageRecords(array $records) { $entity->setCompatibilityMode(TRUE); foreach ($record as $name => $value) { - $entity->{$name}[LANGUAGE_DEFAULT][0]['value'] = $value; + if (is_array($entity->{$name})) { + $entity->{$name}[LANGUAGE_DEFAULT][0]['value'] = $value; + } + else { + $entity->{$name} = $value; + } } $records[$id] = $entity; } @@ -182,16 +187,24 @@ public function save(EntityInterface $entity) { if (!$entity->isNew()) { $return = drupal_write_record($this->entityInfo['base table'], $record, $this->idKey); + if ($this->revisionKey) { + $this->saveRevision($entity); + $entity->set($this->revisionKey, $record->{$this->revisionKey}); + } $this->resetCache(array($entity->id())); $this->postSave($entity, TRUE); $this->invokeHook('update', $entity); } else { $return = drupal_write_record($this->entityInfo['base table'], $record); + $entity->set($this->idKey, $record->{$this->idKey}); + if ($this->revisionKey) { + $this->saveRevision($entity); + $entity->set($this->revisionKey, $record->{$this->revisionKey}); + } // Reset general caches, but keep caches specific to certain entities. $this->resetCache(array()); - $entity->{$this->idKey}->value = $record->{$this->idKey}; $entity->enforceIsNew(FALSE); $this->postSave($entity, FALSE); $this->invokeHook('insert', $entity); @@ -211,6 +224,41 @@ public function save(EntityInterface $entity) { } /** + * Saves an entity revision. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity object. + */ + protected function saveRevision(EntityInterface $entity) { + $record = (array) $this->maptoStorageRecord($entity); + + // When saving a new revision, set any existing revision ID to NULL so as to + // ensure that a new revision will actually be created, then store the old + // revision ID in a separate property for use by hook implementations. + if ($entity->isNewRevision() && $record[$this->revisionKey]) { + $record[$this->revisionKey] = NULL; + } + + $this->preSaveRevision($record, $entity); + + if ($entity->isNewRevision()) { + drupal_write_record($this->revisionTable, $record); + if ($entity->isDefaultRevision()) { + db_update($this->entityInfo['base table']) + ->fields(array($this->revisionKey => $record[$this->revisionKey])) + ->condition($this->idKey, $entity->id()) + ->execute(); + } + $entity->setNewRevision(FALSE); + } + else { + drupal_write_record($this->revisionTable, $record, $this->revisionKey); + } + // Make sure to update the new revision key for the entity. + $entity->{$this->revisionKey} = $record[$this->revisionKey]; + } + + /** * Overrides DatabaseStorageController::invokeHook(). * * Invokes field API attachers in compatibility mode and disables it @@ -235,7 +283,21 @@ protected function invokeHook($hook, EntityInterface $entity) { protected function mapToStorageRecord(EntityInterface $entity) { $record = new \stdClass(); foreach ($this->entityInfo['schema_fields_sql']['base table'] as $name) { - $record->$name = $entity->$name->value; + $value = $entity->$name; + if (is_object($value) && is_a($value,'Drupal\Core\TypedData\TypedDataInterface')) { + switch ($value->get('value')->getType()) { + // Store dates using timestamps. + case 'date': + $record->$name = $entity->$name->value->getTimestamp(); + break; + default: + $record->$name = $entity->$name->value; + break; + } + } + else { + $record->$name = $value; + } } return $record; } diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 7520466..c0080aa 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -60,6 +60,28 @@ class EntityNG extends Entity { */ protected $compatibilityMode = FALSE; + /** + * Overrides Entity::__construct(). + */ + public function __construct(array $values, $entity_type) { + parent::__construct($values, $entity_type); + $this->init(); + } + + /** + * Initialize the object. Invoked upon construction and wake up. + */ + protected function init() { + // We unset all defined properties, so magic getters apply. + unset($this->langcode); + } + + /** + * Magic __wakeup() implemenation. + */ + public function __wakeup() { + $this->init(); + } /** * Overrides Entity::id(). @@ -292,11 +314,26 @@ public function translations() { * @see EntityNG::compatibilityMode */ public function setCompatibilityMode($enabled) { - $this->compatibilityMode = (bool) $enabled; - if ($enabled) { + if (!$this->compatibilityMode && $enabled) { $this->updateOriginalValues(); $this->fields = array(); } + elseif ($this->compatibilityMode && !$enabled) { + // Field API might have written values with a language key, which should + // haved used LANGUAGE_DEFAULT. Fix that by moving values around. + $langcode = $this->get('langcode')->value; + + if ($langcode != LANGUAGE_DEFAULT) { + foreach ($this->getPropertyDefinitions() as $name => $definition) { + if (isset($this->values[$name][$langcode]) && is_array($this->values[$name][$langcode])) { + $this->values[$name][LANGUAGE_DEFAULT] = $this->values[$name][$langcode]; + unset($this->values[$name][$langcode]); + } + } + } + } + + $this->compatibilityMode = (bool) $enabled; } /** @@ -313,8 +350,8 @@ public function getCompatibilityMode() { * operation. */ public function updateOriginalValues() { - foreach ($this->fields as $name => $properties) { - foreach ($properties as $langcode => $property) { + foreach ($this->getProperties() as $name => $property) { + foreach ($this->fields[$name] as $langcode => $property) { $this->values[$name][$langcode] = $property->getValue(); } } @@ -339,10 +376,12 @@ public function &__get($name) { $return = $this->get($name); return $return; } - if (!isset($this->$name)) { - $this->$name = NULL; + // Else directly read/write plain values. That way, fields not yet converted + // to the entity field API can always be accessed as in compatibility mode. + if (!isset($this->values[$name])) { + $this->values[$name] = NULL; } - return $this->$name; + return $this->values[$name]; } /** @@ -363,8 +402,10 @@ public function __set($name, $value) { elseif ($this->getPropertyDefinition($name)) { $this->get($name)->setValue($value); } + // Else directly read/write plain values. That way, fields not yet converted + // to the entity field API can always be accessed as in compatibility mode. else { - $this->$name = $value; + $this->values[$name] = $value; } } @@ -372,24 +413,24 @@ public function __set($name, $value) { * Magic method. */ public function __isset($name) { - if ($this->compatibilityMode) { - return isset($this->values[$name]); - } - elseif ($this->getPropertyDefinition($name)) { + if (!$this->compatibilityMode && $this->getPropertyDefinition($name)) { return (bool) count($this->get($name)); } + else { + return isset($this->values[$name]); + } } /** * Magic method. */ public function __unset($name) { - if ($this->compatibilityMode) { - unset($this->values[$name]); - } - elseif ($this->getPropertyDefinition($name)) { + if (!$this->compatibilityMode && $this->getPropertyDefinition($name)) { $this->get($name)->setValue(array()); } + else { + unset($this->values[$name]); + } } /** diff --git a/core/lib/Drupal/Core/Entity/EntityNGHelper.php b/core/lib/Drupal/Core/Entity/EntityNGHelper.php new file mode 100644 index 0000000..cc388af --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityNGHelper.php @@ -0,0 +1,111 @@ +langcode); + + // Ensure backward compatible langcode. + if (is_array($this->langcode)) { + $this->langcode = key($this->langcode); + } + + $properties = get_class_vars(get_class($this)); + foreach ($properties as $property => $value) { + // Remove property if it's a registered one. + if ($this->getPropertyDefinition($property)) { + unset($this->{$property}); + } + } + } + + /** + * Enforces the compatibility mode for the getter. + */ + public function &__get($name) { + // Enforce compatibility mode. + $this->compatibilityMode = TRUE; + return parent::__get($name); + } + + /** + * Enforces the compatibility mode for the getter. + */ + public function __set($name, $value) { + // Enforce compatibility mode. + $this->compatibilityMode = TRUE; + parent::__set($name, $value); + } + + /** + * Implements ComplexDataInterface::set(). + */ + public function set($property_name, $value) { + parent::set($property_name, $value); + // Ensure the property is available in the compatibility mode. + $this->values[$property_name] = $value; + } + + /** + * Implements TranslatableInterface::language(). + */ + public function language() { + return !empty($this->langcode) ? language_load($this->langcode) : new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED)); + } + + /** + * Implements TranslatableInterface::getTranslationLanguages(). + */ + public function getTranslationLanguages($include_default = TRUE) { + // Because we force backward compatibility we have only the node language. + return array($this->language()); + } + + /** + * Enables or disable the compatibility mode. + * + * Ignore because compatibility mode is enforced anyway. + */ + public function setCompatibilityMode($enabled) { + } + + /** + * Updates the original values with the interim changes. + * + * Ignore because compatibility mode is enforced. + */ + public function updateOriginalValues() { + } + + /** + * Returns the entity properties as array. + * + * As entityNG removes the properties casting the object to array won't work. + * All places that use (array) $entity or get_object_vars($entity) need to + * use this method now. + * This is not an official magic method! + * + * @link https://bugs.php.net/bug.php?id=38508 + */ + public function __toArray() { + $return = get_object_vars($this); + foreach ($this->getProperties() as $name => $property) { + $return[$name] = $this->$name; + } + return $return; + } +} diff --git a/core/lib/Drupal/Core/Entity/EntityRenderController.php b/core/lib/Drupal/Core/Entity/EntityRenderController.php index 2cd0578..6ccb5b9 100644 --- a/core/lib/Drupal/Core/Entity/EntityRenderController.php +++ b/core/lib/Drupal/Core/Entity/EntityRenderController.php @@ -37,29 +37,15 @@ public function buildContent(array $entities = array(), $view_mode = 'full', $la drupal_alter('entity_view_mode', $view_mode, $entity, $context); $entity->content['#view_mode'] = $view_mode; - $prepare[$view_mode][$key] = $entity; + $prepare[$view_mode][$entity->id()] = $entity; } // Prepare and build field content, grouped by view mode. foreach ($prepare as $view_mode => $prepare_entities) { - $call = array(); - // To ensure hooks are only run once per entity, check for an - // entity_view_prepared flag and only process items without it. - foreach ($prepare_entities as $entity) { - if (empty($entity->entity_view_prepared)) { - // Add this entity to the items to be prepared. - $call[$entity->id()] = $entity; - - // Mark this item as prepared. - $entity->entity_view_prepared = TRUE; - } - } + field_attach_prepare_view($this->entityType, $prepare_entities, $view_mode, $langcode); + module_invoke_all('entity_prepare_view', $prepare_entities, $this->entityType); - if (!empty($call)) { - field_attach_prepare_view($this->entityType, $call, $view_mode, $langcode); - module_invoke_all('entity_prepare_view', $call, $this->entityType); - } - foreach ($entities as $entity) { + foreach ($prepare_entities as $entity) { $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode); } } diff --git a/core/modules/node/lib/Drupal/node/Node.php b/core/modules/node/lib/Drupal/node/Node.php index 45d93f0..4da47df 100644 --- a/core/modules/node/lib/Drupal/node/Node.php +++ b/core/modules/node/lib/Drupal/node/Node.php @@ -8,12 +8,12 @@ namespace Drupal\node; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\Entity; +use Drupal\Core\Entity\EntityNGHelper; /** * Defines the node entity class. */ -class Node extends Entity implements ContentEntityInterface { +class Node extends EntityNGHelper implements ContentEntityInterface { /** * The node ID. diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 5add3df..86b73e8 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -8,12 +8,12 @@ namespace Drupal\node; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityFormController; +use Drupal\Core\Entity\EntityFormControllerNG; /** * Form controller for the node edit forms. */ -class NodeFormController extends EntityFormController { +class NodeFormController extends EntityFormControllerNG { /** * Prepares the node object. diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index 3993d24..bd57ab7 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -7,7 +7,7 @@ namespace Drupal\node; -use Drupal\Core\Entity\DatabaseStorageController; +use Drupal\Core\Entity\DatabaseStorageControllerNG; use Drupal\Core\Entity\EntityInterface; /** @@ -16,7 +16,7 @@ * This extends the Drupal\Core\Entity\DatabaseStorageController class, adding * required special handling for node entities. */ -class NodeStorageController extends DatabaseStorageController { +class NodeStorageController extends DatabaseStorageControllerNG { /** * Overrides Drupal\Core\Entity\DatabaseStorageController::create(). @@ -166,4 +166,112 @@ protected function postDelete($nodes) { ->condition('nid', $ids, 'IN') ->execute(); } + + /** + * Implements Drupal\Core\Entity\DatabaseStorageController::baseFieldDefinitions(). + */ + public function baseFieldDefinitions() { + $properties['nid'] = array( + 'label' => t('ID'), + 'description' => t('The node ID.'), + 'type' => 'integer_field', + 'read-only' => TRUE, + ); + $properties['uuid'] = array( + 'label' => t('UUID'), + 'description' => t('The node UUID.'), + 'type' => 'string_field', + 'read-only' => TRUE, + ); + $properties['vid'] = array( + 'label' => t('Revision ID'), + 'description' => t('The ID of the node revision.'), + 'type' => 'integer_field', + 'read-only' => TRUE, + ); +// $properties['isDefaultRevision'] = array( +// 'label' => t('Is default revision'), +// 'description' => t('A boolean indicating whether this is the nodes default revision.'), +// 'type' => 'boolean_field', +// ); + $properties['langcode'] = array( + 'label' => t('Language code'), + 'description' => t('The node language code.'), + 'type' => 'language_field', + ); + $properties['title'] = array( + 'label' => t('Title'), + 'description' => t('The title.'), + 'type' => 'string_field', + ); + $properties['uid'] = array( + 'label' => t('User ID'), + 'description' => t('The user ID of the node author.'), + 'type' => 'entityreference_field', + 'settings' => array('entity type' => 'user'), + ); + $properties['created'] = array( + 'label' => t('Created'), + 'description' => t('The time that the node was created.'), + 'type' => 'date_field', + ); + $properties['changed'] = array( + 'label' => t('Changed'), + 'description' => t('The time that the node was last edited.'), + 'type' => 'date_field', + ); + $properties['revision_timestamp'] = array( + 'label' => t('Revision Timestamp'), + 'description' => t('The time that this revision was last edited.'), + 'type' => 'date_field', + ); + $properties['revision_uid'] = array( + 'label' => t('Revision User ID'), + 'description' => t('The user ID of the node revision author.'), + 'type' => 'entityreference_field', + 'settings' => array('entity type' => 'user'), + ); + $properties['status'] = array( + 'label' => t('Publishing status'), + 'description' => t('A boolean indicating whether the node is published.'), + 'type' => 'boolean_field', + ); + $properties['sticky'] = array( + 'label' => t('Sticky'), + 'description' => t('Indicates whether the node should be displayed at the top of lists in which it appears.'), + 'type' => 'boolean_field', + ); + $properties['promote'] = array( + 'label' => t('Promote to frontpage'), + 'description' => t('Indicates whether the node should be displayed on the front page.'), + 'type' => 'boolean_field', + ); + $properties['comment'] = array( + 'label' => t('Comment status'), + 'description' => t('Whether comments are allowed on this node.'), + 'type' => 'boolean_field', + ); + $properties['type'] = array( + 'label' => t('Node type'), + 'description' => t("The node type."), + 'type' => 'string_field', + ); + $properties['tnid'] = array( + 'label' => t('Translation set ID'), + 'description' => t("The translation set id for this node, which equals the node id of the source post in each set."), + 'type' => 'integer_field', + 'read-only' => TRUE, + ); + $properties['translate'] = array( + 'label' => t('Translation status'), + 'description' => t("Indicates whether this translation page needs to be updated."), + 'type' => 'boolean_field', + ); + $properties['log'] = array( + 'label' => t('Revision log'), + 'description' => t("The log entry explaining the changes in this version."), + 'type' => 'string_field', + ); + return $properties; + } } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index a09aee1..b75a3fa 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1215,7 +1215,7 @@ function template_preprocess_node(&$variables) { $variables['page'] = $variables['view_mode'] == 'full' && node_is_page($node); // Flatten the node entity's member fields. - $variables = array_merge((array) $node, $variables); + $variables = array_merge($node->__toArray(), $variables); // Helpful $content variable for templates. $variables += array('content' => array());