diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 52a4394cd7..222354c4ae 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -5,7 +5,7 @@ /** * A typed data definition class for defining data based on defined data types. */ -class DataDefinition implements DataDefinitionInterface, \ArrayAccess { +class DataDefinition implements DataDefinitionInterface, ExposableDataDefinitionInterface, \ArrayAccess { use TypedDataTrait; @@ -150,6 +150,25 @@ public function isComputed() { } /** + * {@inheritdoc} + */ + public function setExposed($exposed) { + $this->definition['exposed'] = $exposed; + return $this; + } + + /** + * {@inheritdoc} + */ + public function isExposed() { + // For non-computed fields 'exposed' defaults to TRUE. + if (!$this->isComputed() && !isset($this->definition['exposed'])) { + return TRUE; + } + return !empty($this->definition['exposed']); + } + + /** * Sets whether the data is computed. * * @param bool $computed diff --git a/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php new file mode 100644 index 0000000000..5d3093d883 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php @@ -0,0 +1,31 @@ +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 84a9e4a510..b937552a87 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -456,9 +456,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 @@ -519,9 +519,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); } @@ -1260,4 +1260,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..6d6005440d --- /dev/null +++ b/core/modules/serialization/src/Normalizer/FieldItemPropertiesNormalizerTrait.php @@ -0,0 +1,68 @@ +getExposedProperties($field_item) 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; + } + + /** + * Gets exposed properties for the field item. + * + * @param \Drupal\Core\Field\FieldItemInterface $field_item + * The field item being normalized. + * + * @return \Drupal\Core\TypedData\TypedDataInterface[] + * The exposed properties. + */ + public function getExposedProperties(FieldItemInterface $field_item) { + $properties = $field_item->getProperties(TRUE); + /* @var \Drupal\Core\TypedData\TypedDataInterface[] $properties */ + foreach (array_keys($properties) as $property_name) { + $data_definition = $properties[$property_name]->getDataDefinition(); + if ($data_definition instanceof ExposableDataDefinitionInterface) { + if (!$data_definition->isExposed()) { + unset($properties[$property_name]); + } + } + elseif ($data_definition->isComputed()) { + unset($properties[$property_name]); + } + } + return $properties; + } + +} 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/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php index e0561a1003..0a8fe938d7 100644 --- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php +++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php @@ -122,6 +122,10 @@ public function testNormalize() { ->willReturn($entity_reference) ->shouldBeCalled(); + $this->fieldItem->getProperties(TRUE) + ->willReturn([]) + ->shouldBeCalled(); + $normalized = $this->normalizer->normalize($this->fieldItem->reveal()); $expected = [ @@ -146,6 +150,10 @@ public function testNormalizeWithNoEntity() { ->willReturn($entity_reference->reveal()) ->shouldBeCalled(); + $this->fieldItem->getProperties(TRUE) + ->willReturn([]) + ->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);