Commit 22d8e0dc authored by catch's avatar catch

Issue #2083785 by Berdir, yched, das-peter: Add support for determining which...

Issue #2083785 by Berdir, yched, das-peter: Add support for determining which field properties are cacheable.
parent 0fc52f11
......@@ -153,37 +153,6 @@ public function invokeFieldMethod($method, EntityInterface $entity) {
}
}
/**
* {@inheritdoc}
*/
public function invokeFieldItemPrepareCache(EntityInterface $entity) {
// Only act on content entities.
if (!($entity instanceof ContentEntityInterface)) {
return;
}
foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
$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') && function_exists($type_definition['provider'] . '_field_load'))) {
// Call the prepareCache() method directly on each item
// individually.
foreach ($translation->get($property) as $item) {
$item->prepareCache();
}
}
}
}
}
/**
* Invokes a hook on behalf of the entity.
*
......
......@@ -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);
}
......@@ -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 getCacheData();
}
......@@ -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,28 @@ 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 the field item needs to prepare the cache data, call the
// corresponding method, otherwise use the values as cache
// data.
if ($item instanceof PrepareCacheInterface) {
$data[$langcode][$field_name][$delta] = $item->getCacheData();
}
else {
$data[$langcode][$field_name][$delta] = $item->getValue();
}
}
}
}
}
$cid = "field:{$this->entityType}:$id";
......
<?php
/**
* @file
* Definition of Drupal\datetime\DateTimeComputed.
*/
namespace Drupal\datetime;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedData;
/**
* A computed property for dates of date time field items.
*
* Required settings (below the definition's 'settings' key) are:
* - date source: The date property containing the to be computed date.
*/
class DateTimeComputed extends TypedData {
/**
* Cached computed date.
*
* @var \DateTime|null
*/
protected $date = NULL;
/**
* {@inheritdoc}
*/
public function __construct(array $definition, $name = NULL, TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
if (!isset($definition['settings']['date source'])) {
throw new \InvalidArgumentException("The definition's 'date source' key has to specify the name of the date property to be computed.");
}
}
/**
* {@inheritdoc}
*/
public function getValue($langcode = NULL) {
if ($this->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);
}
}
}
......@@ -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 getCacheData() {
$data = $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($data['value'])) {
$data['date'] = $this->date;
}
return $data;
}
/**
......@@ -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;
}
}
}
......@@ -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';
......
......@@ -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.
......
......@@ -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.
......
......@@ -86,7 +86,7 @@ public function instanceSettingsForm(array $form, array &$form_state) {
*
* @see \Drupal\Core\Entity\DatabaseStorageController::invokeFieldItemPrepareCache()
*/
public function prepareCache() {
public function getCacheData() {
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();
}
/**
......
......@@ -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(
......
......@@ -70,7 +70,8 @@ public function isEmpty() {
/**
* {@inheritdoc}
*/
public function prepareCache() {
public function getCacheData() {
$data = $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();
$data[$property] = $this->get($property)->getValue();
}
}
}
return $data;
}
/**
......
......@@ -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');
......
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment