diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index eac2f97..1a00261 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -2509,7 +2509,15 @@ function state() { * @return Drupal\Core\TypedData\TypedDataManager */ function typed_data() { - return drupal_container()->get('typed_data'); + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['manager'] = &drupal_static(__FUNCTION__); + } + if (!isset($drupal_static_fast['manager'])) { + $drupal_static_fast['manager'] = drupal_container()->get('typed_data'); + } + return $drupal_static_fast['manager']; } /** diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php index 0145d1c..6b6d5ff 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageControllerNG.php @@ -109,19 +109,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'; @@ -145,12 +146,10 @@ protected function attachLoad(&$queried_entities, $load_revision = FALSE) { protected function mapFromStorageRecords(array $records) { foreach ($records as $id => $record) { - $entity = new $this->entityClass(array(), $this->entityType); - $entity->setCompatibilityMode(TRUE); - foreach ($record as $name => $value) { - $entity->{$name}[LANGUAGE_DEFAULT][0]['value'] = $value; + $values[$name][LANGUAGE_DEFAULT][0]['value'] = $value; } + $entity = new $this->entityClass($values, $this->entityType); $records[$id] = $entity; } return $records; @@ -174,11 +173,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()) { $return = drupal_write_record($this->entityInfo['base_table'], $record, $this->idKey); @@ -196,6 +190,7 @@ public function save(EntityInterface $entity) { $this->postSave($entity, FALSE); $this->invokeHook('insert', $entity); } + $entity->updateOriginalValues(); // Ignore slave server temporarily. db_ignore_slave(); @@ -218,9 +213,7 @@ public function save(EntityInterface $entity) { */ protected function invokeHook($hook, EntityInterface $entity) { if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) { - $entity->setCompatibilityMode(TRUE); - $function($this->entityType, $entity); - $entity->setCompatibilityMode(FALSE); + $function($this->entityType, $entity->getBCEntity()); } // Invoke the hook. diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 710792f..55402e2 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -364,4 +364,18 @@ public function isDefaultRevision($new_value = NULL) { } return $return; } + + /** + * Implements Drupal\Core\Entity\EntityInterface::getBCEntity(). + */ + public function getBCEntity() { + return $this; + } + + /** + * Implements Drupal\Core\Entity\EntityInterface::getOriginalEntity(). + */ + public function getOriginalEntity() { + return $this; + } } diff --git a/core/lib/Drupal/Core/Entity/EntityBCDecorator.php b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php new file mode 100644 index 0000000..1179ff8 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityBCDecorator.php @@ -0,0 +1,325 @@ +decorated = $decorated; + $this->values = &$values; + $this->fields = &$fields; + } + + /** + * Overrides Entity::getOriginalEntity(). + */ + public function getOriginalEntity() { + return $this->decorated; + } + + /** + * Overrides Entity::getBCEntity(). + */ + public function getBCEntity() { + return $this; + } + + /** + * Allows direct access to the plain values, as done in Drupal 7. + */ + public function &__get($name) { + if (!empty($this->fields[$name])) { + foreach ($this->fields[$name] as $langcode => $field) { + $this->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->fields[$name]); + } + if (!isset($this->values[$name])) { + $this->values[$name] = NULL; + } + return $this->values[$name]; + } + + /** + * Allows directly writing to the plain values, as done in 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->values[$name] = $value; + unset($this->fields[$name]); + } + + /** + * Magic method. + */ + public function __isset($name) { + return isset($this->values[$name]); + } + + /** + * Magic method. + */ + public function __unset($name) { + unset($this->values[$name]); + } + + /** + * Forwards the call to the decorated entity. + */ + public function access(\Drupal\user\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(); + } + + /** + * 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(); + } + + /** + * 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); + } +} diff --git a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php index d06614d..6d1cd51 100644 --- a/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php +++ b/core/lib/Drupal/Core/Entity/EntityFormControllerNG.php @@ -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. @@ -77,9 +73,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; } diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 9b751d4..e7e46fc 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -181,4 +181,22 @@ public function getRevisionId(); * $new_value was passed, the previous value is returned. */ public function isDefaultRevision($new_value = NULL); + + /** + * Gets a backward compatibility decorator entity. + * + * @see \Drupal\Core\Entity\EntityInterface::getOriginalEntity() + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getBCEntity(); + + /** + * Removes any possibly (backward compatibility) decorator in use. + * + * @see \Drupal\Core\Entity\EntityInterface::getBCEntity() + * + * @return \Drupal\Core\Entity\EntityInterface + */ + public function getOriginalEntity(); } diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php index 6541d44..e3aff9b 100644 --- a/core/lib/Drupal/Core/Entity/EntityNG.php +++ b/core/lib/Drupal/Core/Entity/EntityNG.php @@ -50,21 +50,25 @@ class EntityNG extends Entity { protected $fields = array(); /** - * Whether the entity is in pre-Entity Field API compatibility mode. + * An instance of the backward compatibility decorator. * - * If set to TRUE, field values are written directly to $this->values, thus - * must be plain property values keyed by language code. This must be enabled - * when calling legacy field API attachers. + * @var EntityBCDecorator + */ + protected $bcEntity; + + /** + * Static cache for property definitions. * - * @var bool + * @var array */ - protected $compatibilityMode = FALSE; + protected $fieldDefinitions; /** * Overrides Entity::__construct(). */ public function __construct(array $values, $entity_type) { - parent::__construct($values, $entity_type); + parent::__construct(array(), $entity_type); + $this->values = $values + $this->values; $this->init(); } @@ -166,6 +170,11 @@ public function getIterator() { * Implements ComplexDataInterface::getPropertyDefinition(). */ public function getPropertyDefinition($name) { + // Try to read from static cache. + if (isset($this->fieldDefinitions)) { + return isset($this->fieldDefinitions[$name]) ? $this->fieldDefinitions[$name] : FALSE; + } + // First try getting property definitions which apply to all entities of // this type. Then if this fails add in definitions of optional properties // as well. That way we can use property definitions of base properties @@ -185,10 +194,13 @@ public function getPropertyDefinition($name) { * Implements ComplexDataInterface::getPropertyDefinitions(). */ public function getPropertyDefinitions() { - return entity_get_controller($this->entityType)->getFieldDefinitions(array( - 'entity type' => $this->entityType, - 'bundle' => $this->bundle(), - )); + if (!isset($this->fieldDefinitions)) { + $this->fieldDefinitions = entity_get_controller($this->entityType)->getFieldDefinitions(array( + 'entity type' => $this->entityType, + 'bundle' => $this->bundle(), + )); + } + return $this->fieldDefinitions; } /** @@ -306,48 +318,17 @@ public function translations() { } /** - * Enables or disable the compatibility mode. - * - * @param bool $enabled - * Whether to enable the mode. - * - * @see EntityNG::compatibilityMode + * Overrides Entity::getBCEntity(). */ - public function setCompatibilityMode($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]); - } - } - } + public function getBCEntity() { + if (!isset($this->bcEntity)) { + $this->bcEntity = new EntityBCDecorator($this, $this->values, $this->fields); } - - $this->compatibilityMode = (bool) $enabled; - } - - /** - * Returns whether the compatibility mode is active. - */ - public function getCompatibilityMode() { - return $this->compatibilityMode; + return $this->bcEntity; } /** * Updates the original values with the interim changes. - * - * Note: This should be called to make sure compatibility mode uses latest - * values. */ public function updateOriginalValues() { if (!$this->fields) { @@ -368,12 +349,6 @@ public function updateOriginalValues() { * For compatibility mode to work this must return a reference. */ public function &__get($name) { - if ($this->compatibilityMode) { - if (!isset($this->values[$name])) { - $this->values[$name] = NULL; - } - return $this->values[$name]; - } if (isset($this->fields[$name][LANGUAGE_DEFAULT])) { return $this->fields[$name][LANGUAGE_DEFAULT]; } @@ -398,10 +373,7 @@ public function __set($name, $value) { $value = $value->getValue(); } - if ($this->compatibilityMode) { - $this->values[$name] = $value; - } - elseif (isset($this->fields[$name][LANGUAGE_DEFAULT])) { + if (isset($this->fields[$name][LANGUAGE_DEFAULT])) { $this->fields[$name][LANGUAGE_DEFAULT]->setValue($value); } elseif ($this->getPropertyDefinition($name)) { @@ -418,7 +390,7 @@ public function __set($name, $value) { * Magic method. */ public function __isset($name) { - if (!$this->compatibilityMode && $this->getPropertyDefinition($name)) { + if ($this->getPropertyDefinition($name)) { return (bool) count($this->get($name)); } else { @@ -430,7 +402,7 @@ public function __isset($name) { * Magic method. */ public function __unset($name) { - if (!$this->compatibilityMode && $this->getPropertyDefinition($name)) { + if ($this->getPropertyDefinition($name)) { $this->get($name)->setValue(array()); } else { @@ -458,13 +430,28 @@ public function createDuplicate() { * Implements a deep clone. */ public function __clone() { + unset($this->bcEntity); + + // Make sure the fields and values variables are copies, not references. + // This is necessary as they are turned into references when they are passed + // by reference to the EntityBCDecorator. + $values = array(); + foreach ($this->values as $name => $value) { + $values[$name] = $value; + } + unset($this->values); + $this->values = $values; + + $fields = array(); foreach ($this->fields as $name => $properties) { foreach ($properties as $langcode => $property) { - $this->fields[$name][$langcode] = clone $property; + $fields[$name][$langcode] = clone $property; if ($property instanceof ContextAwareInterface) { - $this->fields[$name][$langcode]->setParent($this); + $fields[$name][$langcode]->setParent($this); } } } + unset($this->fields); + $this->fields = $fields; } } diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 910ef84..5f929f3 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1670,9 +1670,7 @@ function template_preprocess_comment(&$variables) { } // Preprocess fields. - $comment->setCompatibilityMode(TRUE); - field_attach_preprocess('comment', $comment, $variables['elements'], $variables); - $comment->setCompatibilityMode(FALSE); + field_attach_preprocess('comment', $comment->getBCEntity(), $variables['elements'], $variables); // Helpful $content variable for templates. foreach (element_children($variables['elements']) as $key) { diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 14ea9ad..beaf7a5 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -8,6 +8,7 @@ use Drupal\field\FieldValidationException; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityNG; +use Drupal\Core\Entity\EntityBCDecorator; /** * @defgroup field_storage Field Storage API @@ -1385,12 +1386,8 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcod // Add this entity to the items to be prepared. $prepare[$id] = $entity; - // Enable compatibility mode if necessary. - // @todo: Remove this once all entity types have been converted to the - // new entity field API. - if ($entity instanceof EntityNG) { - $entity->setCompatibilityMode(TRUE); - } + // Enable BC if necessary. + $entity = $entity->getBCEntity(); // Determine the actual language code to display for each field, given the // language codes available in the field data. @@ -1457,6 +1454,8 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcod * A renderable array for the field values. */ function field_attach_view($entity_type, EntityInterface $entity, $view_mode, $langcode = NULL, array $options = array()) { + // Enable BC if necessary. + $entity = $entity->getBCEntity(); // Determine the actual language code to display for each field, given the // language codes available in the field data. $options['langcode'] = field_language($entity_type, $entity, NULL, $langcode); @@ -1465,12 +1464,8 @@ function field_attach_view($entity_type, EntityInterface $entity, $view_mode, $l $null = NULL; $output = field_invoke_method('view', _field_invoke_formatter_target($view_mode), $entity, $view_mode, $null, $options); - // Disable compatibility mode if necessary. - // @todo: Remove this once all entity types have been converted to the - // new entity field API. - if ($entity instanceof EntityNG) { - $entity->setCompatibilityMode(FALSE); - } + // Remove the BC layer now. + $entity = $entity->getOriginalEntity(); // Add custom weight handling. $output['#pre_render'][] = '_field_extra_fields_pre_render'; diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php index fbb735d..65ef649 100644 --- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php +++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php @@ -48,32 +48,41 @@ protected function buildPropertyQuery(QueryInterface $entity_query, array $value * An array of entity objects implementing the EntityInterface. */ protected function mapFromStorageRecords(array $records) { - $records = parent::mapFromStorageRecords($records); - - // Load data of translatable properties. - $this->attachPropertyData($records); + $property_values = $this->getPropertyValues($records); + + foreach ($records as $id => $record) { + $values = $property_values[$id]; + foreach ($record as $name => $value) { + $values[$name][LANGUAGE_DEFAULT][0]['value'] = $value; + } + $entity = new $this->entityClass($values, $this->entityType); + $records[$id] = $entity; + } return $records; } /** * Attaches property data in all languages for translatable properties. */ - protected function attachPropertyData(&$queried_entities) { + protected function getPropertyValues($records) { $data = db_select('entity_test_property_data', 'data', array('fetch' => PDO::FETCH_ASSOC)) ->fields('data') - ->condition('id', array_keys($queried_entities)) + ->condition('id', array_keys($records)) ->orderBy('data.id') ->execute(); + $property_values = array(); + foreach ($data as $values) { $id = $values['id']; // Field values in default language are stored with // LANGUAGE_DEFAULT as key. $langcode = empty($values['default_langcode']) ? $values['langcode'] : LANGUAGE_DEFAULT; - $queried_entities[$id]->name[$langcode][0]['value'] = $values['name']; - $queried_entities[$id]->user_id[$langcode][0]['value'] = $values['user_id']; + $property_values[$id]['name'][$langcode][0]['value'] = $values['name']; + $property_values[$id]['user_id'][$langcode][0]['value'] = $values['user_id']; } + return $property_values; } /**