diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php index 43ff5f1..767d3a2 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerBase.php @@ -173,67 +173,6 @@ public function invokeFieldMethod($method, EntityInterface $entity) { } } - /** - * {@inheritdoc} - */ - public function invokeFieldItemPrepareCache(EntityInterface $entity) { - foreach (array_keys($entity->getTranslationLanguages()) as $langcode) { - // @todo getTranslation() only works on NG entities. Remove the condition - // and the second code branch when all core entity types are converted. - if ($translation = $entity->getTranslation($langcode)) { - foreach ($translation->getPropertyDefinitions() as $property => $definition) { - $type_definition = \Drupal::typedData()->getDefinition($definition['type']); - // Only create the item objects if needed. - if (is_subclass_of($type_definition['class'], '\Drupal\Core\Entity\Field\PrepareCacheInterface') - // Prevent legacy field types from skewing performance too much by - // checking the existence of the legacy function directly, instead - // of making LegacyConfigFieldItem implement PrepareCacheInterface. - // @todo Remove once all core field types have been converted (see - // http://drupal.org/node/2014671). - || (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem') - && isset($type_definition['provider']) && function_exists($type_definition['provider'] . '_field_load'))) { - - // Call the prepareCache() method directly on each item - // individually. - foreach ($translation->get($property) as $item) { - $item->prepareCache(); - } - } - } - } - else { - // For BC entities, iterate through the fields and instantiate NG items - // objects manually. - $definitions = \Drupal::entityManager()->getFieldDefinitions($entity->entityType(), $entity->bundle()); - foreach ($definitions as $field_name => $definition) { - if (!empty($definition['configurable'])) { - $type_definition = \Drupal::typedData()->getDefinition($definition['type']); - // Only create the item objects if needed. - if (is_subclass_of($type_definition['class'], '\Drupal\Core\Entity\Field\PrepareCacheInterface') - // @todo Remove once all core field types have been converted - // (see http://drupal.org/node/2014671). - || (is_subclass_of($type_definition['class'], '\Drupal\field\Plugin\field\field_type\LegacyConfigFieldItem') && function_exists($type_definition['provider'] . '_field_load'))) { - - // Create the items object. - $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); - $itemsNG = \Drupal::typedData()->create($definition, $items, $field_name, $entity); - - foreach ($itemsNG as $item) { - $item->prepareCache(); - } - - // Put back the items values in the entity. - $items = $itemsNG->getValue(TRUE); - if ($items !== array() || isset($entity->{$field_name}[$langcode])) { - $entity->{$field_name}[$langcode] = $items; - } - } - } - } - } - } - } - /** * Invokes a hook on behalf of the entity. * diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php index c7f72f7..63a38bf 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php @@ -163,12 +163,4 @@ public function getQueryServicename(); */ public function invokeFieldMethod($method, EntityInterface $entity); - /** - * Invokes the prepareCache() method on all the relevant FieldItem objects. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object. - */ - public function invokeFieldItemPrepareCache(EntityInterface $entity); - } diff --git a/core/lib/Drupal/Core/Entity/Field/PrepareCacheInterface.php b/core/lib/Drupal/Core/Entity/Field/PrepareCacheInterface.php index 5701523..7f57c53 100644 --- a/core/lib/Drupal/Core/Entity/Field/PrepareCacheInterface.php +++ b/core/lib/Drupal/Core/Entity/Field/PrepareCacheInterface.php @@ -10,22 +10,28 @@ /** * Interface for preparing field values before they enter cache. * - * If a field type implements this interface, the prepareCache() method will be - * invoked before field values get cached. + * If a field type implements this interface, this method will be used instead + * of the regular getValue() to collect the data to include in the cache of + * field values. */ interface PrepareCacheInterface { /** - * Massages loaded field values before they enter the field cache. + * Returns the data to store in the field cache. * - * You should never load fieldable entities within this method, since this is - * likely to cause infinite recursions. Use the prepareView() method instead. + * This method is called if the entity type has field caching enabled, when an + * entity is loaded and no existing cache entry was found in the field cache. * - * Also note that the method is not called on field values displayed during - * entity preview. If the method adds elements that might be needed during - * display, you might want to also use prepareView() to add those elements in - * case they are not present. + * This method should never trigger the loading of fieldable entities, since + * this is likely to cause infinite recursions. A common workaround is to + * provide a base formatter class implementing the prepareView() method + * instead. + * + * The recommended way to implement it is to provide a computed field item + * property that can accepts setting a value through setValue(). See + * \Drupal\text\Plugin\field\field_type\TextItemBase and the corresponding + * computed property Drupal\text\TextProcessed for an example. */ - public function prepareCache(); + public function getCacheValue(); } diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php index 87b86c0..d1fadd7 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageControllerBase.php @@ -7,8 +7,10 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Entity\Field\PrepareCacheInterface; use Drupal\field\FieldInterface; use Drupal\field\FieldInstanceInterface; +use Drupal\field\Plugin\Type\FieldType\ConfigFieldItemListInterface; use Symfony\Component\DependencyInjection\Container; abstract class FieldableEntityStorageControllerBase extends EntityStorageControllerBase implements FieldableEntityStorageControllerInterface { @@ -77,20 +79,27 @@ protected function loadFieldItems(array $entities, $age) { // Let the storage controller actually load the values. $this->doLoadFieldItems($queried_entities, $age); - // Invoke the field type's prepareCache() method. - foreach ($queried_entities as $entity) { - $this->invokeFieldItemPrepareCache($entity); - } - // Build cache data. + // @todo: Improve this logic to avoid instantiating field objects once + // the field logic is improved to not do that anyway. if ($use_cache) { foreach ($queried_entities as $id => $entity) { $data = array(); - $instances = field_info_instances($this->entityType, $entity->bundle()); foreach ($entity->getTranslationLanguages() as $langcode => $language) { $translation = $entity->getTranslation($langcode); - foreach ($instances as $instance) { - $data[$langcode][$instance['field_name']] = $translation->{$instance['field_name']}->getValue(); + foreach ($translation as $field_name => $items) { + if ($items instanceof ConfigFieldItemListInterface && !$items->isEmpty()) { + foreach ($items as $delta => $item) { + if ($item instanceof PrepareCacheInterface) { + // Load the cache values. + $data[$langcode][$field_name][$delta] = $item->getCacheValue(); + } + else { + // Get the item values. + $data[$langcode][$field_name][$delta] = $item->getValue(); + } + } + } } } $cid = "field:{$this->entityType}:$id"; diff --git a/core/modules/datetime/lib/Drupal/datetime/DateTimeComputed.php b/core/modules/datetime/lib/Drupal/datetime/DateTimeComputed.php new file mode 100644 index 0000000..c0f6123 --- /dev/null +++ b/core/modules/datetime/lib/Drupal/datetime/DateTimeComputed.php @@ -0,0 +1,74 @@ +date !== NULL) { + return $this->date; + } + + $item = $this->getParent(); + $value = $item->{($this->definition['settings']['date source'])}; + + $storage_format = $item->getFieldDefinition()->getFieldSetting('datetime_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; + try { + $date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE); + if ($date instanceOf DrupalDateTime && !$date->hasErrors()) { + $this->date = $date; + } + } + catch (\Exception $e) { + // @todo Handle this. + } + return $this->date; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->date = $value; + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + +} diff --git a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/field_type/DateTimeItem.php b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/field_type/DateTimeItem.php index 558ca47..c5df7ea 100644 --- a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/field_type/DateTimeItem.php +++ b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/field_type/DateTimeItem.php @@ -49,6 +49,16 @@ public function getPropertyDefinitions() { 'type' => 'datetime_iso8601', 'label' => t('Date value'), ); + static::$propertyDefinitions['date'] = array( + 'type' => 'datetime_computed', + 'label' => t('Computed date'), + 'description' => t('The computed DateTime object.'), + 'computed' => TRUE, + 'class' => '\Drupal\datetime\DateTimeComputed', + 'settings' => array( + 'date source' => 'value', + ), + ); } return static::$propertyDefinitions; @@ -114,24 +124,16 @@ public function instanceSettingsForm(array $form, array &$form_state) { /** * {@inheritdoc} */ - public function prepareCache() { + public function getCacheValue() { + $values = $this->getValue(); // The function generates a Date object for each field early so that it is // cached in the field cache. This avoids the need to generate the object // later. The date will be retrieved in UTC, the local timezone adjustment // must be made in real time, based on the preferences of the site and user. - $value = $this->get('value')->getValue(); - if (!empty($value)) { - $storage_format = $this->getFieldSetting('datetime_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT; - try { - $date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE); - if ($date instanceOf DrupalDateTime && !$date->hasErrors()) { - $this->set('date', $date); - } - } - catch (\Exception $e) { - // @todo Handle this. - } + if (!empty($values['value'])) { + $values['date'] = $this->date; } + return $values; } /** @@ -142,4 +144,16 @@ public function isEmpty() { return $value === NULL || $value === ''; } + /** + * {@inheritdoc} + */ + public function onChange($property_name) { + parent::onChange($property_name); + + // Enforce that the computed date is recalculated. + if ($property_name == 'value') { + $this->date = NULL; + } + } + } diff --git a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/formatter/DatetimeDefaultFormatter.php b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/formatter/DatetimeDefaultFormatter.php index c4e6344..baabb9c 100644 --- a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/formatter/DatetimeDefaultFormatter.php +++ b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/formatter/DatetimeDefaultFormatter.php @@ -103,11 +103,8 @@ public function viewElements(FieldItemListInterface $items) { $formatted_date = ''; $iso_date = ''; - if (!empty($item->date)) { - // The date was created and verified during field_load(), so it is safe - // to use without further inspection. + if ($item->date) { $date = $item->date; - // Create the ISO date in Universal Time. $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z'; diff --git a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDatelistWidget.php b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDatelistWidget.php index 0123088..e835408 100644 --- a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDatelistWidget.php +++ b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDatelistWidget.php @@ -126,7 +126,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // validator will not have access to the field definition. $element['value']['#date_storage_format'] = $storage_format; - if (!empty($items[$delta]->date)) { + if ($items[$delta]->date) { $date = $items[$delta]->date; // The date was created and verified during field_load(), so it is safe to // use without further inspection. diff --git a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDefaultWidget.php b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDefaultWidget.php index e0e1dac..2011135 100644 --- a/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDefaultWidget.php +++ b/core/modules/datetime/lib/Drupal/datetime/Plugin/field/widget/DatetimeDefaultWidget.php @@ -114,8 +114,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // validator will not have access to the field definition. $element['value']['#date_element_format'] = $element_format; $element['value']['#date_storage_format'] = $storage_format; - - if (!empty($items[$delta]->date)) { + if ($items[$delta]->date) { $date = $items[$delta]->date; // The date was created and verified during field_load(), so it is safe to // use without further inspection. diff --git a/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php index e46622f..58a5ef29 100644 --- a/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php +++ b/core/modules/field/lib/Drupal/field/Plugin/field/field_type/LegacyConfigFieldItem.php @@ -86,7 +86,7 @@ public function instanceSettingsForm(array $form, array &$form_state) { * * @see \Drupal\Core\Entity\DatabaseStorageController::invokeFieldItemPrepareCache() */ - public function prepareCache() { + public function getCacheValue() { if ($callback = $this->getLegacyCallback('load')) { $entity = $this->getEntity(); $entity_id = $entity->id(); @@ -105,7 +105,9 @@ public function prepareCache() { ); call_user_func_array($callback, $args); $this->setValue($items[$entity_id][0]); + return $items[$entity_id][0]; } + return $this->getValue(); } /** diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php index 2de2456..7bccfd8 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldAttachStorageTest.php @@ -90,8 +90,6 @@ function testFieldAttachSaveLoad() { for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. $this->assertEqual($entity->{$this->field_name}[$delta]->value, $values[$revision_id][$delta]['value'], format_string('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta))); - // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$this->field_name}[$delta]->additional_key, 'additional_value', format_string('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); } } } @@ -100,7 +98,7 @@ function testFieldAttachSaveLoad() { * Test the 'multiple' load feature. */ function testFieldAttachLoadMultiple() { - $entity_type = 'entity_test'; + $entity_type = 'entity_test_rev'; // Define 2 bundles. $bundles = array( diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php index 54630fd..cdf9a77 100644 --- a/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php +++ b/core/modules/text/lib/Drupal/text/Plugin/field/field_type/TextItemBase.php @@ -70,7 +70,8 @@ public function isEmpty() { /** * {@inheritdoc} */ - public function prepareCache() { + public function getCacheValue() { + $values = $this->getValue(); // Where possible, generate the processed (sanitized) version of each // textual property (e.g., 'value', 'summary') within this field item early // so that it is cached in the field cache. This avoids the need to look up @@ -79,10 +80,11 @@ public function prepareCache() { if (!$text_processing || filter_format_allowcache($this->get('format')->getValue())) { foreach ($this->getPropertyDefinitions() as $property => $definition) { if (isset($definition['class']) && ($definition['class'] == '\Drupal\text\TextProcessed')) { - $this->get($property)->getValue(); + $values[$property] = $this->get($property)->getValue(); } } } + return $values; } /** diff --git a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php index 82480a5..6309c98 100644 --- a/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php +++ b/core/modules/text/lib/Drupal/text/Tests/TextWithSummaryItemTest.php @@ -119,8 +119,30 @@ function testProcessedCache() { $entity->name->value = $this->randomName(); $entity->save(); - // Inject values into the cache to make sure that these are used as-is and - // not re-calculated. + // Check that the processed values are correctly computed. + $this->assertEqual($entity->summary_field->processed, $value); + $this->assertEqual($entity->summary_field->summary_processed, $summary); + + // Load the entity and check that the field cache contains the expected + // data. + $entity = entity_load($entity_type, $entity->id()); + $cache = cache('field')->get("field:$entity_type:" . $entity->id()); + $this->assertEqual($cache->data, array( + Language::LANGCODE_DEFAULT => array( + 'summary_field' => array( + 0 => array( + 'value' => $value, + 'summary' => $summary, + 'format' => 'plain_text', + 'processed' => $value, + 'summary_processed' => $summary, + ), + ), + ), + )); + + // Inject fake processed values into the cache to make sure that these are + // used as-is and not re-calculated when the entity is loaded. $data = array( Language::LANGCODE_DEFAULT => array( 'summary_field' => array( @@ -135,8 +157,7 @@ function testProcessedCache() { ), ); cache('field')->set("field:$entity_type:" . $entity->id(), $data); - - $entity = entity_load($entity_type, $entity->id()); + $entity = entity_load($entity_type, $entity->id(), TRUE); $this->assertEqual($entity->summary_field->processed, 'Cached processed value'); $this->assertEqual($entity->summary_field->summary_processed, 'Cached summary processed value'); diff --git a/core/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/text/lib/Drupal/text/TextProcessed.php index f3c65e2..bbf0fd0 100644 --- a/core/modules/text/lib/Drupal/text/TextProcessed.php +++ b/core/modules/text/lib/Drupal/text/TextProcessed.php @@ -47,7 +47,12 @@ public function getValue($langcode = NULL) { $item = $this->getParent(); $text = $item->{($this->definition['settings']['text source'])}; - if ($item->getFieldDefinition()->getFieldSetting('text_processing')) { + + // Avoid running check_markup() or check_plain() on empty strings. + if (!isset($text) || $text === '') { + $this->processed = ''; + } + elseif ($item->getFieldDefinition()->getFieldSetting('text_processing')) { $this->processed = check_markup($text, $item->format, $item->getLangcode()); } else {