diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index b38a94b3c2..ae96d2d6e1 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -3,12 +3,15 @@ namespace Drupal\Core\Config\Schema; use Drupal\Core\TypedData\ComplexDataInterface; +use Drupal\Core\TypedData\ComplexDataWithExposedPropertiesTrait; /** * Defines a generic configuration element that contains multiple properties. */ abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface, ComplexDataInterface { + use ComplexDataWithExposedPropertiesTrait; + /** * Parsed elements. */ diff --git a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php index eafb6d88bf..311f4080e2 100644 --- a/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php +++ b/core/lib/Drupal/Core/Entity/Plugin/DataType/EntityAdapter.php @@ -6,6 +6,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinition; use Drupal\Core\TypedData\ComplexDataInterface; +use Drupal\Core\TypedData\ComplexDataWithExposedPropertiesTrait; use Drupal\Core\TypedData\Exception\MissingDataException; use Drupal\Core\TypedData\TypedData; @@ -28,6 +29,7 @@ */ class EntityAdapter extends TypedData implements \IteratorAggregate, ComplexDataInterface { + use ComplexDataWithExposedPropertiesTrait; /** * The wrapped entity object. * diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php index 76366bfe21..f3e254b7f8 100644 --- a/core/lib/Drupal/Core/Field/FieldConfigBase.php +++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php @@ -6,12 +6,14 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\TypedData\FieldItemDataDefinition; +use Drupal\Core\TypedData\ExposableDataDefinitionTrait; /** * Base class for configurable field definitions. */ abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface { + use ExposableDataDefinitionTrait; /** * The field ID. * diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php index 6396cb8f51..321c924dbc 100644 --- a/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php +++ b/core/lib/Drupal/Core/TypedData/ComplexDataInterface.php @@ -102,4 +102,12 @@ public function toArray(); */ public function isEmpty(); + /** + * Gets exposed properties for the field item. + * + * @return \Drupal\Core\TypedData\TypedDataInterface[] + * The exposed properties. + */ + public function getExposedProperties(); + } diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php b/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php new file mode 100644 index 0000000000..69e3d61a50 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ComplexDataWithExposedPropertiesTrait.php @@ -0,0 +1,27 @@ +getProperties(TRUE); + /* @var \Drupal\Core\TypedData\TypedDataInterface[] $properties */ + foreach (array_keys($properties) as $property_name) { + if (!$properties[$property_name]->getDataDefinition()->isExposed()) { + unset($properties[$property_name]); + } + } + return $properties; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 52a4394cd7..6e0f936a8f 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -9,6 +9,8 @@ class DataDefinition implements DataDefinitionInterface, \ArrayAccess { use TypedDataTrait; + use ExposableDataDefinitionTrait; + /** * The array holding values for all definition keys. * diff --git a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php index 64d779c317..afaf4f441f 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php @@ -218,4 +218,23 @@ public function getConstraint($constraint_name); */ public function addConstraint($constraint_name, $options = NULL); + /** + * Sets the whether the data value should be exposed. + * + * @param bool $exposed + * Whether the data value is exposed. + * + * @return static + * The object itself for chaining. + */ + public function setExposed($exposed); + + /** + * Determines whether the data value is exposed. + * + * @return bool + * Whether the data value is exposed. + */ + public function isExposed(); + } diff --git a/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php new file mode 100644 index 0000000000..708e40f3a9 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php @@ -0,0 +1,34 @@ +isComputed() && !isset($this->definition['exposed'])) { + return TRUE; + } + return !empty($this->definition['exposed']); + } + + /** + * {@inheritdoc} + * + * Implements setExposed() for \Drupal\Core\TypedData\DataDefinitionInterface. + */ + public function setExposed($exposed) { + $this->definition['exposed'] = $exposed; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php index a95c14b30e..a3b2657c9f 100644 --- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php @@ -2,6 +2,7 @@ namespace Drupal\Core\TypedData\Plugin\DataType; +use Drupal\Core\TypedData\ComplexDataWithExposedPropertiesTrait; use Drupal\Core\TypedData\TypedData; use Drupal\Core\TypedData\ComplexDataInterface; @@ -26,6 +27,8 @@ */ class Map extends TypedData implements \IteratorAggregate, ComplexDataInterface { + use ComplexDataWithExposedPropertiesTrait; + /** * The data definition. * diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 6e2b3ec43e..b81e0c5629 100644 --- a/core/modules/filter/src/Element/ProcessedText.php +++ b/core/modules/filter/src/Element/ProcessedText.php @@ -3,6 +3,7 @@ namespace Drupal\filter\Element; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element\RenderElement; use Drupal\filter\Entity\FilterFormat; @@ -69,7 +70,11 @@ public static function preRenderText($element) { $langcode = $element['#langcode']; if (!isset($format_id)) { - $format_id = static::configFactory()->get('filter.settings')->get('fallback_format'); + $filter_settings = static::configFactory()->get('filter.settings'); + $format_id = $filter_settings->get('fallback_format'); + CacheableMetadata::createFromRenderArray($element) + ->addCacheableDependency($filter_settings) + ->applyTo($element); } /** @var \Drupal\filter\Entity\FilterFormat $format **/ $format = FilterFormat::load($format_id); diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index 3fa592b627..2a8a71bd1f 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -78,7 +78,7 @@ class FilterProcessResult extends BubbleableMetadata { * @param string $processed_text * The text as processed by a text filter. */ - public function __construct($processed_text) { + public function __construct($processed_text = '') { $this->processedText = $processed_text; } diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php index dc5aec9899..02910c9b41 100644 --- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php @@ -3,6 +3,8 @@ namespace Drupal\hal\Normalizer; use Drupal\Core\Field\FieldItemInterface; +use Drupal\serialization\Normalizer\ComputedPropertiesNormalizerTrait; +use Drupal\serialization\Normalizer\FieldItemPropertiesNormalizerTrait; use Symfony\Component\Serializer\Exception\InvalidArgumentException; /** @@ -10,6 +12,8 @@ */ class FieldItemNormalizer extends NormalizerBase { + use FieldItemPropertiesNormalizerTrait; + /** * The interface or class that this Normalizer supports. * @@ -21,13 +25,8 @@ class FieldItemNormalizer extends NormalizerBase { * {@inheritdoc} */ public function normalize($field_item, $format = NULL, array $context = []) { - $values = []; - // We normalize each individual property, so each can do their own casting, - // if needed. - /** @var \Drupal\Core\TypedData\TypedDataInterface $property */ - foreach ($field_item as $property_name => $property) { - $values[$property_name] = $this->serializer->normalize($property, $format, $context); - } + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ + $values = $this->normalizeFieldProperties($field_item, $format, $context); if (isset($context['langcode'])) { $values['lang'] = $context['langcode']; diff --git a/core/modules/hal/tests/src/Kernel/NormalizeTest.php b/core/modules/hal/tests/src/Kernel/NormalizeTest.php index d837b79a02..e77d4b464a 100644 --- a/core/modules/hal/tests/src/Kernel/NormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Url; use Drupal\entity_test\Entity\EntityTest; +use Drupal\filter\Entity\FilterFormat; /** * Tests HAL normalization edge cases for EntityResource. @@ -18,6 +19,28 @@ class NormalizeTest extends NormalizerTestBase { */ protected function setUp() { parent::setUp(); + // Create a text format because it is needed for TextItemBase normalization. + // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize(). + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_html' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [ + 'allowed_html' => '

', + ], + ], + 'filter_autop' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [], + ], + ], + ])->save(); \Drupal::service('router.builder')->rebuild(); } @@ -37,7 +60,7 @@ public function testNormalize() { 'name' => $this->randomMachineName(), 'field_test_text' => [ 'value' => $this->randomMachineName(), - 'format' => 'full_html', + 'format' => 'my_text_format', ], 'field_test_entity_reference' => [ 'target_id' => $target_entity_de->id(), @@ -152,6 +175,7 @@ public function testNormalize() { [ 'value' => $values['field_test_text']['value'], 'format' => $values['field_test_text']['format'], + 'process_result' => "

{$values['field_test_text']['value']}

", ], ], ]; diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index df77281f37..7dc6585283 100644 --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\rest\EventSubscriber; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Render\RenderContext; @@ -126,11 +127,18 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req /** * Renders a resource response body. * - * Serialization can invoke rendering (e.g., generating URLs), but the - * serialization API does not provide a mechanism to collect the - * bubbleable metadata associated with that (e.g., language and other - * contexts), so instead, allow those to "leak" and collect them here in - * a render context. + * During serialization, encoders and normalizers are able to explicitly + * bubble cacheability metadata via the 'cacheability' key-value pair in the + * received context. This bubbled cacheability metadata will be applied to the + * the response. + * + * In prior versions of Drupal 8, we allowed implicit bubbling of cacheability + * metadata because there was no explicit cacheability metadata bubbling API. + * To maintain backwards compatibility, we continue to support this, but + * support for this will be dropped in Drupal 9.0.0. This is especially useful + * when interacting with APIs that implicitly invoke rendering (for example: + * generating URLs): this allows those to "leak", and we collect their bubbled + * cacheability metadata automatically in a render context. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. @@ -150,14 +158,25 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac // If there is data to send, serialize and set it as the response body. if ($data !== NULL) { + $serialization_context = [ + 'request' => $request, + 'cacheability' => new CacheableMetadata(), + ]; + + // @deprecated In Drupal 8.4.0, will be removed before Drupal 9.0.0. Use + // explicit cacheability metadata bubbling instead. (The wrapping call to + // executeInRenderContext() will be removed before Drupal 9.0.0.) $context = new RenderContext(); $output = $this->renderer - ->executeInRenderContext($context, function () use ($serializer, $data, $format) { - return $serializer->serialize($data, $format); + ->executeInRenderContext($context, function() use ($serializer, $data, $format, $serialization_context) { + return $serializer->serialize($data, $format, $serialization_context); }); - - if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) { - $response->addCacheableDependency($context->pop()); + if ($response instanceof CacheableResponseInterface) { + if (!$context->isEmpty()) { + @trigger_error('Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.4.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling.', E_USER_DEPRECATED); + $response->addCacheableDependency($context->pop()); + } + $response->addCacheableDependency($serialization_context['cacheability']); } $response->setContent($output); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 961441213a..5dd78ffed0 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -5,6 +5,7 @@ use Drupal\comment\Entity\Comment; use Drupal\comment\Entity\CommentType; use Drupal\comment\Tests\CommentTestTrait; +use Drupal\Core\Cache\Cache; use Drupal\entity_test\Entity\EntityTest; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; use Drupal\user\Entity\User; @@ -200,6 +201,7 @@ protected function getExpectedNormalizedEntity() { [ 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', 'format' => 'plain_text', + 'process_result' => '

The name "llama" was adopted by European settlers from native Peruvians.

' . "\n", ], ], ]; @@ -252,6 +254,20 @@ protected function getNormalizedPatchEntity() { } /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts()); + } + + /** * Tests POSTing a comment without critical base fields. * * testPost() is testing with the most minimal normalization possible: the one diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 6e34de2bf5..62c8c6f2b2 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -465,9 +465,9 @@ public function testGet() { // for the keys with the array order the same (it needs to match with // identical comparison). $expected = $this->getExpectedNormalizedEntity(); - ksort($expected); + $this->nestedKsort($expected); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); - ksort($actual); + $this->nestedKsort($actual); $this->assertSame($expected, $actual); // Not only assert the normalization, also assert deserialization of the @@ -528,9 +528,9 @@ public function testGet() { // Config entities are not affected. // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize() $expected = static::castToString($expected); - ksort($expected); + $this->nestedKsort($expected); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); - ksort($actual); + $this->nestedKsort($actual); $this->assertSame($expected, $actual); } @@ -1282,4 +1282,19 @@ protected function assertResourceNotAvailable(Url $url, array $request_options) } } + /** + * Sorts a nested array with ksort(). + * + * @param $array + * The nested array to sort. + */ + public static function nestedKsort(&$array) { + ksort($array); + foreach ($array as &$item) { + if (is_array($item)) { + static::nestedKsort($item); + } + } + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php new file mode 100644 index 0000000000..c563b5c61a --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php @@ -0,0 +1,81 @@ + 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', + 'format' => 'plain_text', + 'process_result' => '

Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.

' . "\n", + ], + ]; + return $expected; + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity = parent::createEntity(); + $entity->field_test_text = [ + 'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', + 'format' => 'plain_text', + ]; + $entity->save(); + return $entity; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + $post_entity = parent::getNormalizedPostEntity(); + $post_entity['field_test_text'] = [ + [ + 'value' => 'Llamas are awesome.', + 'format' => 'plain_text', + ], + ]; + return $post_entity; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(['config:filter.format.plain_text'], parent::getExpectedCacheTags()); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 698451b58c..4d6b3008a5 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\rest\Functional\EntityResource\Term; +use Drupal\Core\Cache\Cache; use Drupal\taxonomy\Entity\Term; use Drupal\taxonomy\Entity\Vocabulary; use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase; @@ -64,6 +65,7 @@ protected function createEntity() { // Create a "Llama" taxonomy term. $term = Term::create(['vid' => $vocabulary->id()]) ->setName('Llama') + ->setDescription("It is a little known fact that llamas cannot count higher the seven.") ->setChangedTime(123456789); $term->save(); @@ -93,8 +95,9 @@ protected function getExpectedNormalizedEntity() { ], 'description' => [ [ - 'value' => NULL, + 'value' => 'It is a little known fact that llamas cannot count higher the seven.', 'format' => NULL, + 'process_result' => "

It is a little known fact that llamas cannot count higher the seven.

\n", ], ], 'parent' => [], @@ -159,4 +162,18 @@ protected function getExpectedUnauthorizedAccessMessage($method) { } } + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']); + } + } diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php index decca43227..77479a0cb8 100644 --- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php @@ -2,6 +2,7 @@ namespace Drupal\serialization\Normalizer; +use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Field\FieldItemInterface; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -11,6 +12,8 @@ */ class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface { + use FieldItemPropertiesNormalizerTrait; + /** * {@inheritdoc} */ @@ -54,4 +57,12 @@ protected function constructValue($data, $context) { return $data; } + /** + * {@inheritdoc} + */ + public function normalize($object, $format = NULL, array $context = []) { + $attributes = $this->normalizeFieldProperties($object, $format, $context); + return $attributes; + } + } diff --git a/core/modules/serialization/src/Normalizer/FieldItemPropertiesNormalizerTrait.php b/core/modules/serialization/src/Normalizer/FieldItemPropertiesNormalizerTrait.php new file mode 100644 index 0000000000..1760188310 --- /dev/null +++ b/core/modules/serialization/src/Normalizer/FieldItemPropertiesNormalizerTrait.php @@ -0,0 +1,42 @@ +getExposedProperties() as $name => $property) { + $attribute = $this->serializer->normalize($property, $format, $context); + if ($attribute instanceof CacheableDependencyInterface && isset($context['cacheability'])) { + $context['cacheability']->addCacheableDependency($attribute); + } + if (is_object($attribute) && method_exists($attribute, '__toString')) { + $attribute = (string) $attribute; + } + $attributes[$name] = $attribute; + } + return $attributes; + } + +} diff --git a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml index 36243e7954..51fdd4a5fa 100644 --- a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml +++ b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml @@ -2,5 +2,5 @@ services: serializer.normalizer.silly_fielditem: class: Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer tags: - # The priority must be higher than serialization.normalizer.field_item. - - { name: normalizer , priority: 9 } + # The priority must be higher than serializer.normalizer.text_item_base. + - { name: normalizer , priority: 30 } diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php index 71fa6b3ec5..094a8771d6 100644 --- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php @@ -2,8 +2,10 @@ namespace Drupal\Tests\serialization\Kernel; +use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; use Drupal\entity_test\Entity\EntityTestMulRev; +use Drupal\filter\Entity\FilterFormat; /** * Tests that entities can be serialized to supported core formats. @@ -60,6 +62,29 @@ protected function setUp() { // User create needs sequence table. $this->installSchema('system', ['sequences']); + // Create a text format because it is needed for TextItemBase normalization. + // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize(). + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_html' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [ + 'allowed_html' => '

', + ], + ], + 'filter_autop' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [], + ], + ], + ])->save(); + // Create a test user to use as the entity owner. $this->user = \Drupal::entityManager()->getStorage('user')->create([ 'name' => 'serialization_test_user', @@ -69,12 +94,13 @@ protected function setUp() { $this->user->save(); // Create a test entity to serialize. + $test_text_value = $this->randomMachineName(); $this->values = [ 'name' => $this->randomMachineName(), 'user_id' => $this->user->id(), 'field_test_text' => [ - 'value' => $this->randomMachineName(), - 'format' => 'full_html', + 'value' => $test_text_value, + 'format' => 'my_text_format', ], ]; $this->entity = EntityTestMulRev::create($this->values); @@ -128,6 +154,7 @@ public function testNormalize() { [ 'value' => $this->values['field_test_text']['value'], 'format' => $this->values['field_test_text']['format'], + 'process_result' => "

{$this->values['field_test_text']['value']}

", ], ], ]; @@ -168,7 +195,7 @@ public function testSerialize() { // JsonEncoder. The output of ComplexDataNormalizer::normalize() is tested // elsewhere, so we can just assume that it works properly here. $normalized = $this->serializer->normalize($this->entity, 'json'); - $expected = json_encode($normalized); + $expected = Json::encode($normalized); // Test 'json'. $actual = $this->serializer->serialize($this->entity, 'json'); $this->assertIdentical($actual, $expected, 'Entity serializes to JSON when "json" is requested.'); @@ -193,7 +220,7 @@ public function testSerialize() { 'revision_id' => '' . $this->entity->getRevisionId() . '', 'default_langcode' => '1', 'non_rev_field' => '', - 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '', + 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '' . $this->values['field_test_text']['value'] . '

]]>
', ]; // Sort it in the same order as normalised. $expected = array_merge($normalized, $expected); diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php index dc14d6f428..f59e15f279 100644 --- a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php +++ b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\serialization\Unit\Normalizer; use Drupal\Core\TypedData\ComplexDataInterface; +use Drupal\Core\TypedData\ComplexDataWithExposedPropertiesTrait; use Drupal\Core\TypedData\TraversableTypedDataInterface; use Drupal\serialization\Normalizer\ComplexDataNormalizer; use Drupal\Tests\UnitTestCase; @@ -76,6 +77,8 @@ public function testNormalize() { */ class TestComplexData implements \IteratorAggregate, ComplexDataInterface { + use ComplexDataWithExposedPropertiesTrait; + private $values; public function __construct(array $values = []) { diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php index e0561a1003..a107250865 100644 --- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php +++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php @@ -4,6 +4,8 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\TypedData\Plugin\DataType\StringData; +use Drupal\Core\TypedData\Type\StringInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\FieldableEntityInterface; @@ -122,6 +124,10 @@ public function testNormalize() { ->willReturn($entity_reference) ->shouldBeCalled(); + $this->fieldItem->getExposedProperties() + ->willReturn(['target_id' => NULL]) + ->shouldBeCalled(); + $normalized = $this->normalizer->normalize($this->fieldItem->reveal()); $expected = [ @@ -146,6 +152,10 @@ public function testNormalizeWithNoEntity() { ->willReturn($entity_reference->reveal()) ->shouldBeCalled(); + $this->fieldItem->getExposedProperties() + ->willReturn(['target_id' => NULL]) + ->shouldBeCalled(); + $normalized = $this->normalizer->normalize($this->fieldItem->reveal()); $expected = [ diff --git a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php index 6dd4339299..06e3be4f21 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php @@ -7,6 +7,8 @@ use Drupal\Core\Field\FieldItemBase; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\TypedData\DataDefinition; +use Drupal\text\TextProcessed; +use Drupal\text\TextProcessedResult; /** * Base class for 'text' configurable field types. @@ -31,6 +33,14 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setClass('\Drupal\text\TextProcessed') ->setSetting('text source', 'value'); + $properties['process_result'] = DataDefinition::create('string') + ->setLabel(t('Processed text (object)')) + ->setDescription(t('The text with the text format applied.')) + ->setComputed(TRUE) + ->setClass(TextProcessedResult::class) + ->setSetting('text source', 'value') + ->setExposed(TRUE); + return $properties; } @@ -57,7 +67,7 @@ public function isEmpty() { public function onChange($property_name, $notify = TRUE) { // Unset processed properties that are affected by the change. foreach ($this->definition->getPropertyDefinitions() as $property => $definition) { - if ($definition->getClass() == '\Drupal\text\TextProcessed') { + if (in_array($definition->getClass(), [TextProcessed::class, TextProcessedResult::class], TRUE)) { if ($property_name == 'format' || ($definition->getSetting('text source') == $property_name)) { $this->writePropertyValue($property, NULL); } diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php index 61bef37dd5..dd9c185952 100644 --- a/core/modules/text/src/TextProcessed.php +++ b/core/modules/text/src/TextProcessed.php @@ -2,9 +2,8 @@ namespace Drupal\text; -use Drupal\Core\TypedData\DataDefinitionInterface; -use Drupal\Core\TypedData\TypedDataInterface; -use Drupal\Core\TypedData\TypedData; +use Drupal\Core\Render\Markup; +use Drupal\filter\FilterProcessResult; /** * A computed property for processing text with a format. @@ -12,56 +11,18 @@ * Required settings (below the definition's 'settings' key) are: * - text source: The text property containing the to be processed text. */ -class TextProcessed extends TypedData { - - /** - * Cached processed text. - * - * @var string|null - */ - protected $processed = NULL; - - /** - * {@inheritdoc} - */ - public function __construct(DataDefinitionInterface $definition, $name = NULL, TypedDataInterface $parent = NULL) { - parent::__construct($definition, $name, $parent); - - if ($definition->getSetting('text source') === NULL) { - throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed."); - } - } +class TextProcessed extends TextProcessedResult { /** * {@inheritdoc} */ public function getValue() { - if ($this->processed !== NULL) { - return $this->processed; - } - - $item = $this->getParent(); - $text = $item->{($this->definition->getSetting('text source'))}; + $value = parent::getValue(); - // Avoid running check_markup() on empty strings. - if (!isset($text) || $text === '') { - $this->processed = ''; - } - else { - $this->processed = check_markup($text, $item->format, $item->getLangcode()); - } - return $this->processed; - } - - /** - * {@inheritdoc} - */ - public function setValue($value, $notify = TRUE) { - $this->processed = $value; - // Notify the parent of any changes. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); + if ($value !== '' || ($value instanceof FilterProcessResult && $value->getProcessedText() !== '')) { + $value = Markup::create((string) $value); } + return $value; } } diff --git a/core/modules/text/src/TextProcessedResult.php b/core/modules/text/src/TextProcessedResult.php new file mode 100644 index 0000000000..a20d670bff --- /dev/null +++ b/core/modules/text/src/TextProcessedResult.php @@ -0,0 +1,92 @@ +getSetting('text source') === NULL) { + throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed."); + } + } + + /** + * {@inheritdoc} + */ + public function getValue() { + if ($this->processed !== NULL) { + return $this->processed; + } + + /** @var \Drupal\Core\Field\FieldItemInterface $item */ + $item = $this->getParent(); + $text = $item->{($this->definition->getSetting('text source'))}; + + // Avoid running \Drupal\Core\Render\RendererInterface::renderPlain + // on empty strings. + if (!isset($text) || $text === '') { + $this->processed = ''; + } + else { + $build = [ + '#type' => 'processed_text', + '#text' => $text, + '#format' => $item->format, + '#filter_types_to_skip' => [], + '#langcode' => $item->getLangcode(), + ]; + // It's necessary to capture the cacheability metadata associated with the + // processed text. See https://www.drupal.org/node/2278483. + $processed_text = $this->getRenderer()->renderPlain($build); + $this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text); + } + return $this->processed; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->processed = $value; + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + + /** + * Returns the renderer service. + * + * @return \Drupal\Core\Render\RendererInterface + */ + protected function getRenderer() { + return \Drupal::service('renderer'); + } + +} diff --git a/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php b/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php index a48c191b5e..fa9db4018d 100644 --- a/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php +++ b/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php @@ -81,6 +81,8 @@ public function testCrudAndUpdate() { // Change the format, this should update the processed properties. $entity->summary_field->format = 'no_filters'; + $entity->save(); + $entity = $storage->load($entity->id()); $this->assertEqual($entity->summary_field->processed, $value); $this->assertEqual($entity->summary_field->summary_processed, $summary);