Problem/Motivation

When accessing entities that use a text field type (subclasses of \Drupal\text\Plugin\Field\FieldType\TextItemBase), only the raw/original value (user input) is normalized, not the processed text (with filters applied). But it's impossible (and nonsensical and insecure) to replicate the logic of filters on the client side. We should just expose TextItemBase's processed property.

Proposed resolution

Original approach: Add TextItemNormalizer that adds the processed value to the TextItem upon normalization
This was the original approach. But adding normalizers for every field type is problematic in multiple ways
  1. lots of boiler plate
  2. this only fixes the problem for core's normalizations, not for contrib (e.g. JSON API — see #2860350: Document why JSON API only supports @DataType-level normalizers)
  3. it also doesn't fix it for alternative API architectures, such as https://www.drupal.org/project/graphql
  4. it makes it impossible to automatically generate documentation, such as https://www.drupal.org/project/openapi
Current approach: Expose the existing computed property + fix the class that represents that computed property to contain its cacheability metadata.
  1. BLOCKER: #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts
  2. Make the existing processed property non-internal (see #2871591]).
  3. BLOCKER: #2910211: Allow computed exposed properties in ComplexData to support cacheability.
  4. Update \Drupal\text\TextProcessed from class TextProcessed extends TypedData {} to class TextProcessed extends TypedData implements CacheableDependencyInterface {}.
  5. The previous point requires updating its logic to collect cacheability metadata correctly when performing filtering, the logic in HEAD uses check_markup() (which is a deprecated D5/D6/D7 function whose result doesn't contain cacheability metadata). However, TextProcessed::getValue() must retain BC (and hence return a value that doesn't carry cacheability).
  6. Add missing cacheability metadata when using the fallback text format.
  7. Updated test coverage expectations: BlockContentResourceTestBase, CommentResourceTestBase, TermResourceTestBase and EntitySerializationTest are all testing a text field already in HEAD. They must be updated to now expect a new processed property in the normalizations.
  8. Explicit test coverage: EntityTestTextItemNormalizerTest is testing all permutations: 1) a format specified (but not the fallback format), 2) format specified (happens to be the fallback format), 3) no format specified (fallback format used automatically).

End result: we can fix the bug (something missing from the normalization) without a BC break at all: everything continues to behave as it does in HEAD, and no new computed property is added.

(If you want a historical summary of why we ended up with the current approach, see #169.)

This then fixes it for core REST, contrib JSON API + GraphQL, and OpenAPI!

Remaining tasks

None.

CommentFileSizeAuthor
#235 2626924-235.patch23.96 KBwim leers
#235 interdiff.txt966 byteswim leers
#233 2626924-233.patch23.93 KBwim leers
#233 interdiff.txt3.02 KBwim leers
#231 2626924-231.patch23.58 KBwim leers
#231 interdiff.txt5.9 KBwim leers
#213 2626924-213.patch22.1 KBwim leers
#213 interdiff.txt2.71 KBwim leers
#204 2626924-204.patch20.35 KBwim leers
#204 interdiff.txt887 byteswim leers
#199 2626924-199.patch20.55 KBwim leers
#199 interdiff-198.txt4.28 KBwim leers
#199 interdiff-191.txt4.18 KBwim leers
#198 2626924-198.patch21.72 KBwim leers
#198 interdiff.txt1.01 KBwim leers
#193 2626924-193.patch20.96 KBtedbow
#193 interdiff-191-193.txt4.34 KBtedbow
#191 2626924-191.patch22.03 KBwim leers
#191 interdiff.txt6.01 KBwim leers
#188 2626924-188.patch23.08 KBwim leers
#188 interdiff.txt2.99 KBwim leers
#185 2626924-185.patch22.9 KBwim leers
#185 interdiff.txt946 byteswim leers
#179 2626924-179.patch22.04 KBwim leers
#179 interdiff.txt888 byteswim leers
#178 2626924-177.patch21.84 KBwim leers
#178 interdiff.txt3.14 KBwim leers
#176 2626924-173.patch22.22 KBwim leers
#176 interdiff.txt875 byteswim leers
#175 2626924-172.patch22.15 KBwim leers
#175 interdiff.txt1.72 KBwim leers
#171 2626924-171.patch22.16 KBwim leers
#171 interdiff.txt3.73 KBwim leers
#170 2626924-170.patch24.4 KBwim leers
#170 interdiff.txt2.94 KBwim leers
#169 2626924-169.patch24.42 KBwim leers
#169 interdiff.txt11.23 KBwim leers
#167 2626924-167.patch26.02 KBwim leers
#167 interdiff.txt1.8 KBwim leers
#166 2626924-166.patch24.28 KBwim leers
#166 interdiff.txt2.38 KBwim leers
#161 2626924-161_plus_2910211-6.patch65.59 KBtedbow
#161 2626924-161-do-not-test.patch24.47 KBtedbow
#157 interdiff-2626924.txt11.67 KBdawehner
#157 2626924-157.patch11.41 KBdawehner
#155 2626924-155.patch23.08 KBdawehner
#151 2626924-152-including-2871591.patch47.13 KBtedbow
#151 2626924-151-do-not-test.patch23.87 KBtedbow
#145 2626924-145.patch38.94 KBtedbow
#145 interdiff-143-145.txt612 bytestedbow
#143 2626924-143.patch38.94 KBtedbow
#143 interdiff-141--143.txt2.29 KBtedbow
#141 2626924-141.patch38.67 KBtedbow
#141 interdiff-139-141.txt4.16 KBtedbow
#139 2626924-139.patch35.95 KBtedbow
#139 interdiff-136_reroll-139.txt13.41 KBtedbow
#136 interdiff--134-136.txt4.14 KBtedbow
#136 2626924-136-text_processed.patch31.16 KBtedbow
#134 2626924-134.patch27.25 KBalmaudoh
#124 interdiff-2626924-124.txt786 bytesdamiankloip
#124 2626924-124.patch28.3 KBdamiankloip
#122 interdiff.txt6.45 KBdawehner
#122 2626924-122.patch28.57 KBdawehner
#120 2626924-120.patch26.38 KBdawehner
#120 interdiff.txt2.73 KBdawehner
#118 interdiff.txt8.6 KBdawehner
#118 2626924-118.patch24.95 KBdawehner
#115 interdiff-111-115.txt1.84 KBtedbow
#115 2626924-115.patch20.56 KBtedbow
#9 core-expose-processed-text-to-rest-2626924-9-8.0.x.patch2.15 KBfrob
#10 core-expose-processed-text-to-rest-2626924-10-8.0.x.patch2.13 KBfrob
#12 core-expose-processed-text-to-rest-2626924-12-8.2.x.patch2.08 KBmartin107
#12 interdiff-10-12.txt1.12 KBmartin107
#14 core-expose-processed-text-to-rest-2626924-14-8.2.x.patch2.06 KBfrob
#16 expose-TextItem-processed-normalizing-2626924-16.patch1.98 KBfrob
#16 inter.diff591 bytesfrob
#24 interdiff.txt5.46 KBdawehner
#24 2626924-24.patch4.02 KBdawehner
#28 2626924-26.patch3.95 KBdawehner
#28 interdiff.txt2.26 KBdawehner
#30 2626924-29.patch3.99 KBdawehner
#30 interdiff.txt1.3 KBdawehner
#34 2626924-34.patch4.26 KBdawehner
#34 interdiff.txt1.08 KBdawehner
#36 2626924-36.patch3.99 KBdagmar
#36 interdiff-2626924-34-36.txt746 bytesdagmar
#49 2626924-49.patch6.07 KBtedbow
#49 interdiff-2626924-36-49.txt3.09 KBtedbow
#50 2626924-50.patch6 KBtedbow
#50 interdiff-2626924-49-50.txt562 bytestedbow
#52 2626924-52.patch6.48 KBtedbow
#52 interdiff-2626924-50-52.txt3.81 KBtedbow
#54 2626924-54.patch6.47 KBtedbow
#54 interdiff-2626924-52-54.txt426 bytestedbow
#56 interdiff-2626924-54-56.txt4.87 KBtedbow
#56 2626924-56.patch6.83 KBtedbow
#61 interdiff-2626924-56-61.txt8.07 KBtedbow
#61 2626924-61.patch9.81 KBtedbow
#64 2626924-61-8.2.x.patch10.1 KBwim leers
#66 2626924-61-8.2.x.patch10.12 KBwim leers
#68 2626924-68.patch10.47 KBwim leers
#68 interdiff.txt2.58 KBwim leers
#73 2626924-73.patch11.08 KBtedbow
#73 interdiff-68-73.txt7.39 KBtedbow
#76 2626924-76.patch11.09 KBmartin107
#76 interdiff-2626924-73-76.txt1.57 KBmartin107
#79 2626924-79.patch12.97 KBtedbow
#79 interdiff-76-79.txt3.78 KBtedbow
#83 2626924-83.patch13.67 KBtedbow
#83 interdiff-79-83.txt6.69 KBtedbow
#84 interdiff-79-83.txt5.68 KBtedbow
#86 2626924-86.patch15.35 KBtedbow
#86 interdiff-83-86.txt9.8 KBtedbow
#91 2626924-91.patch15.64 KBtedbow
#91 interdiff-86-91.txt9.42 KBtedbow
#96 2626924-96.patch16.07 KBtedbow
#96 interdiff-91-96.txt2.59 KBtedbow
#98 2626924-98.patch16.23 KBtedbow
#98 interdiff-96-98.txt1.71 KBtedbow
#98 so_close.gif1.81 MBtedbow
#100 interdiff.txt1.98 KBdawehner
#109 2626924-109.patch17.56 KBwim leers
#109 interdiff.txt2.33 KBwim leers
#111 2626924-111.patch19.28 KBwim leers
#111 interdiff.txt1.77 KBwim leers

Comments

frob created an issue. See original summary.

wim leers’s picture

Title: Support the use of Text Formats in REST requests » Expose TextItems' "processed" item value when normalizing
Category: Support request » Task
Issue tags: +API-First Initiative

Without this, it can can indeed be painful/difficult to consume text data entered in Drupal.

\Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions() says:

  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties['value'] = DataDefinition::create('string')
      ->setLabel(t('Text'))
      ->setRequired(TRUE);

    $properties['format'] = DataDefinition::create('filter_format')
      ->setLabel(t('Text format'));

    $properties['processed'] = DataDefinition::create('string')
      ->setLabel(t('Processed text'))
      ->setDescription(t('The text with the text format applied.'))
      ->setComputed(TRUE)
      ->setClass('\Drupal\text\TextProcessed')
      ->setSetting('text source', 'value');

    return $properties;
  }

So, just exposing that computed processed property would fix this.

wim leers’s picture

Title: Expose TextItems' "processed" item value when normalizing » Expose TextItems' "processed" property when normalizing
frob’s picture

Assigned: Unassigned » frob
wim leers’s picture

Yay, thanks for taking this on @frob! I will happily provide reviews! Let's do this :)

frob’s picture

Whoops, I thought I assigned a different issue to myself. But, whatever, lets do this. :)

frob’s picture

I am not 100% sure where I would do this to keep it at the field level.

My thought is that I would need to create a new TypedDataNormalizer that exposes this. However, TypedDataNormalizer is just exposing $object->getValues(); so it might be better to expose the processed data there. Thoughts?

Also, should this be 8.0.x or 8.1.x

frob’s picture

Writing it down here so I do not lose it.

After chatting with jhodgdon and larowlan on IRC it seems like this is what needs to be done.

In the text module:
I need to create a TextServiceProvider service that provides an alter method that conditionally adds a Normalizing service TextItemNormalizer.
I need to create the TextItemNormalizer class that adds the processed value to the TextItem upon normalization.

This is what the file module does https://github.com/md-systems/file_entity/blob/8.x-2.x/src/FileEntitySer...

Unfortunately, in order to get this into the getValue definition would mean editing the getValue callback globally for a special case that would check the field type and ignore the computed value of TextItems.

[edit] fixed spelling

frob’s picture

Status: Active » Needs review
StatusFileSize
new2.15 KB

Here is my first pass at the patch. It likely does need some work. I cannot test it at this point, figure I will see what testbot tells me first.

Should this have a test?

frob’s picture

StatusFileSize
new2.13 KB

That one has some obvious mistakes. Here is another roll.

frob’s picture

Version: 8.0.x-dev » 8.2.x-dev
Assigned: frob » Unassigned
martin107’s picture

I think the REST module will be improved by this change.

In terms of review as something that extends ServiceProviderBase and NormalizerBase

It follows a well established pattern -and everything looks good.

As for my changes I have resolved only trivial things.

Hmm to answer the testing question from #9

For general information \Drupal\hal\Tests\FileNormalizeTest provides a good patterns for such a test harness, but the question is should we

I guess if some cunning hackers can find a dangerous input which the system accidentally manipulates into bad output.... then potentially we should test.
I am afraid I don't have enough system oversight to have a opinion.

wim leers’s picture

+++ b/core/modules/text/src/TextItemServiceProvider.php
@@ -0,0 +1,32 @@
+    // Check for installed REST & HAL modules, as HAL requires REST.
+    if (isset($modules['serialization'])) {

The comment and the if-test don't match.

frob’s picture

You caught me. Copy and paste error.

wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

So the only big thing missing here, is test coverage! Great work :)

  1. +++ b/core/modules/text/src/Normalizer/TextItemNormalizer.php
    @@ -0,0 +1,32 @@
    + * Converts typed data objects to arrays.
    

    This comment needs to be adjusted to match what this class actually does.

  2. +++ b/core/modules/text/src/Normalizer/TextItemNormalizer.php
    @@ -0,0 +1,32 @@
    +  public function normalize($object, $format = NULL, array $context = array()) {
    

    Nit: s/array()/[]/

  3. +++ b/core/modules/text/src/Normalizer/TextItemNormalizer.php
    @@ -0,0 +1,32 @@
    +    $attributes->processed = check_markup($object->value, $object->format);
    

    What's missing here is the third parameter. The entity has a langcode, we should pass that there.

  4. +++ b/core/modules/text/src/TextItemServiceProvider.php
    @@ -0,0 +1,32 @@
    +    // Check for installed serialization module.
    ...
    +      // Add a normalizer service for text fields.
    

    I think these comments can just be omitted, I think both speak for themselves.

frob’s picture

Still needs tests, if anyone wants to help with that. Cleaned up everything else.

frob’s picture

Status: Needs work » Needs review

The last submitted patch, 16: expose-TextItem-processed-normalizing-2626924-16.patch, failed testing.

frob’s picture

I queued a retest, the failed test had nothing to do with this patch. I expect it is unrelated and fixed now.

Do you have an example or doc page to go off of for the test coverage of this? Do we need to do anything special for testing a REST endpoint or will the usual get url and assert text test work for this?

frob’s picture

Status: Needs review » Needs work

It still needs tests

wim leers’s picture

Do you have an example or doc page to go off of for the test coverage of this?

Yes!

Look at:

  • integration test: \Drupal\serialization\Tests\NormalizerTestBase (and the tests that extend this base class)
  • unit test: \Drupal\Tests\serialization\Unit\Normalizer\ListNormalizerTest tests \Drupal\serialization\Normalizer\ListNormalizer
wim leers’s picture

moshe weitzman’s picture

In most recent patch i noticed a minor typo: parkup

dawehner’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new5.46 KB
new4.02 KB

I decided to review the patch but ended up with all kind of points, so this patch makes it actually working as well as adding a test.

damiankloip’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,31 @@
    +    $attributes = $object->getValue();
    

    I don't think this is right, I think this should extend the ComplexDataNormalizer and get the values that way (from the parent::normalize() method) then add this computed property.

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,31 @@
    +    $attributes['processed'] = (string) $object->processed;
    

    This could be passed to the serializer too, functionally there is little point. Thoughts?

  3. +++ b/core/modules/text/src/TextServiceProvider.php
    @@ -0,0 +1,27 @@
    + * Service Provider for Text Items.
    

    "Service provider for text module" ?

  4. +++ b/core/modules/text/src/TextServiceProvider.php
    @@ -0,0 +1,27 @@
    +      $container->setDefinition('serializer.normalizer.text', $service_definition);
    

    text_item I think is better.

  5. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,65 @@
    +class TextItemBaseNormalizerTest extends KernelTestBase {
    

    It's sad this needs to be a kernel test. I am not even going to ask, I know you would have made it a unit test if that was not a problem...

damiankloip’s picture

In an ideal word this would be handled in a generic way, so a field item normalizer could just expose computed properties. I don;t think we are able to do that in the current world order though :/

The last submitted patch, 24: 2626924-24.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new3.95 KB
new2.26 KB

Here is a new one. Thank you @damiankloip for a, as always, proper review.

damiankloip’s picture

Any time, Daniel!

+++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
@@ -21,10 +21,8 @@ class TextItemBaseNormalizer extends NormalizerBase {
+    $attributes['processed'] = $this->serializer->normalize($object, $format, $context);

This needs to normalize $object->processed?

dawehner’s picture

StatusFileSize
new3.99 KB
new1.3 KB

Well my IDE never failed, but it also didn't stopped in its infinite loop.

Status: Needs review » Needs work

The last submitted patch, 30: 2626924-29.patch, failed testing.

The last submitted patch, 28: 2626924-26.patch, failed testing.

The last submitted patch, 30: 2626924-29.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new4.26 KB
new1.08 KB

Let's see whether this fixes the problem

Status: Needs review » Needs work

The last submitted patch, 34: 2626924-34.patch, failed testing.

dagmar’s picture

Status: Needs work » Needs review
StatusFileSize
new3.99 KB
new746 bytes

I've been playing with this patch. It seems the issue with the failing tests is due the line:

$service_definition->addTag('normalizer', ['priority' => 20]);

If I remove that line, the failing tests work again, but the new one fails.

To be honest I don't know why changing this from 20 to 1 makes the failing tests and the new one added by @dawehner work. Let see what the testbot says.

dawehner’s picture

Interesting!

I'm wondering whether we could provide some list of all normalizers and their priorities to understand the order of them.

Status: Needs review » Needs work

The last submitted patch, 36: 2626924-36.patch, failed testing.

dagmar’s picture

Some other findings. This patch should modify also EntitySerializationTest:

http://cgit.drupalcode.org/drupal/tree/core/modules/serialization/src/Te...

     'non_rev_field' => array(),
      'field_test_text' => array(
        array(
          'value' => $this->values['field_test_text']['value'],
          'format' => $this->values['field_test_text']['format'],
        ),
      ),

And http://cgit.drupalcode.org/drupal/tree/core/modules/serialization/src/Te...

<?php
$expected = array(
      'id' => '<id><value>' . $this->entity->id() . '</value></id>',
      'uuid' => '<uuid><value>' . $this->entity->uuid() . '</value></uuid>',
      'langcode' => '<langcode><value>en</value></langcode>',
      'name' => '<name><value>' . $this->values['name'] . '</value></name>',
      'type' => '<type><value>entity_test_mulrev</value></type>',
      'created' => '<created><value>' . $this->entity->created->value . '</value></created>',
      'user_id' => '<user_id><target_id>' . $this->user->id() . '</target_id><target_type>' . $this->user->getEntityTypeId() . '</target_type><target_uuid>' . $this->user->uuid() . '</target_uuid><url>' . $this->user->url() . '</url></user_id>',
      'revision_id' => '<revision_id><value>' . $this->entity->getRevisionId() . '</value></revision_id>',
      'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
      'non_rev_field' => '<non_rev_field/>',
      'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
    );
?>

Which doesn't include the new processed value.

dawehner’s picture

To be honest I'm not sure whether we should expose the processed value all the time. It could easily double the payload of those requests.
Is there any way how we can opt in things ... ?

frob’s picture

@dawehner I would think that would be better handled in another issue. We should make that issue a dependency of this one (so that this issue isn't committed without that one), but that does sound like separate functionality.

Also, thank you dawehner for picking this up. I got swamped recently.

dawehner’s picture

@frob
Well the additional payload could be an issue we introduce. Ideally one doesn't introduce bugs as part of tasks :) Feel free to simply tell me that this is not really an issue in the first place.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dagmar’s picture

Just wondering, could we use the approach from #16 and the test from #36?

dagmar’s picture

Never mind, we tried #16 using the entity embed module and didnt work.

The approach that fix the issue is #36, we should find the way to fix the tests.

frob’s picture

Sorry @dawehner, I didn't see your reply till today. I am using this patch and it doesn't seem to add any noticeable load. I must also qualify that with, I have not taken any scientific tests on the performance one way or another.

I was under the impression that the processed value is being cached. There isn't any way to configure the API exposed settings in this way yet. Anything is possible, but I would expect this discussion to evolve and turn into one of two things: a feature that is so big that it will delay this even further (possible to the point of never being implemented) or turned into a legacy that needs to be maintained throughout the duration of 8.x. One such solution is to have this be a setting in the field settings or in the settings for the text format. Neither are good solutions, but could somewhat easily be tacked onto this issue without much thought or discussion.

Before we push this back any further we need numbers to see if this does actually create a performance issue and if so, is it enough to postpone this until better API configuration is possible.

wim leers’s picture

#46: "payload" != "server load". Payload == the data you send. So dawehner was saying that this means a lot more data to send (via the network).

You're right though that the processed value is cached, because GET requests are cached by Dynamic Page Cache.

frob’s picture

Ah @Wim Leers, thanks for clearing that up.

RE #40 @dawehner, I don't think this is an issue. A GET REST API request is already a somewhat massive payload with lots of somewhat extra information that would need to be filtered out with custom code if the payload if it is mission critical.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new6.07 KB
new3.09 KB

Fixed EntitySerializationTest as mentioned in #39.

Also fixed namespace in \Drupal\Tests\text\Kernel\Normalizer\TextItemBaseNormalizerTest
and 2 coding standard issues.

tedbow’s picture

StatusFileSize
new6 KB
new562 bytes

Gave it another look over. Just another small coding standard nit.

wim leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,27 @@
    +  public function normalize($object, $format = NULL, array $context = []) {
    

    We should document why there is no denormalize() method, and we should have test coverage ensuring that sending processed as part of a PATCH or POST request results in an error.

  2. +++ b/core/modules/text/src/TextServiceProvider.php
    @@ -0,0 +1,27 @@
    +    if (isset($modules['serialization'])) {
    

    I'm not sure why it even needs to be conditional, I think it can just be defined in text.services.yml: there's no harm at all in having unused services.

  3. +++ b/core/modules/text/src/TextServiceProvider.php
    @@ -0,0 +1,27 @@
    +      $service_definition->addTag('normalizer', ['priority' => 1]);
    

    Why priority 1?

  4. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,66 @@
    +  public function testNormalize() {
    +    /** @var \Symfony\Component\Serializer\Serializer $serializer */
    +    $serializer = \Drupal::service('serializer');
    +
    +    $entity = EntityTest::create([
    +      'field_text' => 'text item',
    +    ]);
    +    $entity->save();
    +
    +    $data = $serializer->normalize($entity);
    +    $this->assertEquals([
    +      'value' => 'text item',
    +      'processed' => "<p>text item</p>\n",
    +      'format' => NULL,
    +    ], $data['field_text'][0]);
    +  }
    

    This is testing the case of format = NULL (I did not even know this was possible?). It should also test a case of a format being defined.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new6.48 KB
new3.81 KB

@Wim Leers thanks for review.

1. Added comment.
Remaining task

and we should have test coverage ensuring that sending processed as part of a PATCH or POST request results in an error.

2. moved to text.services.yml:
3. It will only work for me if there is a priority . Haven't had a chance to see what it needs to be ahead of but 100 works.
4. Created a format and testing for that.

This is testing the case of format = NULL (I did not even know this was possible?).

Debugged and it falls back to plain_text but you can't specify plain_text explicitly.

Changing to "Needs Review" to trigger tests. but still have a remaining test under 1.

Status: Needs review » Needs work

The last submitted patch, 52: 2626924-52.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new6.47 KB
new426 bytes

Changing priority to 10. This should fix a lot (all?) of the fails.

wim leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,31 @@
    + * Adds processed markup from text fields to normalizer data.
    

    Hm, "markup" made me frown here. Do we want s/markup/text/?

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,31 @@
    + * This class does not implement DenormalizerInterface because the 'proccessed'
    + * attribute represents text processed by the filter system therefore it cannot
    + * be provided when creating new entities.
    

    Great! Perhaps mention that 'processed' is computed?

  3. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,98 @@
    +    FilterFormat::create(array(
    ...
    +      'filters' => array(
    

    s/array()/[]/

  4. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,98 @@
    +    /** @var \Symfony\Component\Serializer\Serializer $serializer */
    +    $serializer = \Drupal::service('serializer');
    

    This should be assigned to $this->serializer in setUp().

  5. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,98 @@
    +    $this->assertEquals([
    +      'value' => 'text item',
    ...
    +    $this->assertEquals([
    +      'value' => '<strong>This</strong> is <b>important.</b>',
    

    This can be converted to use a @dataProvider, which will make it easier to add more test cases.

  6. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,98 @@
    +      'field_text' => [
    +          'value' => '<strong>This</strong> is <b>important.</b>',
    +          'format' => 'my_text_format',
    +      ],
    

    This uses 4 spaces of indentation, should be 2.

  7. +++ b/core/modules/text/text.services.yml
    @@ -0,0 +1,6 @@
    +      - { name: normalizer, priority: 10 }
    

    Let's document why this needs to be 10.

    Also, why was 1 okay before, but now it needs to be 10?

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.87 KB
new6.83 KB

@Wim Leers thanks for review
1-6 fixed

7. It didn't see any justification in this issue for why it needed to be 1. So I tried find the lowest it could be and still work. I added a comment for what I found in the services.yml file.
Here is what I found:
In #52 I changed it to 100 which caused REST related tests to fail with this error:

fail: [Fatal error] Line 0 of :
[02-Nov-2016 08:49:59 Australia/Sydney] Uncaught PHP Exception InvalidArgumentException: "Field value is unknown." at /var/www/html/core/lib/Drupal/Core/Entity/ContentEntityBase.php line 479

I found that 10 would make these pass 11 would fail. I found that hal.services.yml sets a number of service to 10. It seemed the new service needed to be the same or higher priority than those.
I tested switch those services in hal.services to 9 to see which one would cause the REST tests to fail.
i found that I could switch any of the services in hal.services from 9 to 10 except serializer.normalizer.field_item.hal
This makes sense because the fail mention above is in \Drupal\Core\Entity\ContentEntityBase::getTranslatedField and \Drupal\hal\Normalizer\FieldItemNormalizer::denormalize calls. \Drupal\hal\Normalizer\FieldItemNormalizer::createTranslatedInstance

Honestly I am little lost about why the new service has to be the same or before serializer.normalizer.field_item.hal but maybe my debugging would help some or is more experienced see what is going on.

wim leers’s picture

Status: Needs review » Needs work

Regarding point 7: yeah that's just the pain/brittleness of weights (Drupal) and priorities (Symfony). I'll bet it's because the YAML files are all read first, and then service providers are called. It used to be in a service provider, now it's in a YAML file.

  1. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,117 @@
    +  public function testNormalize(array $create_values, array $expected) {
    +    $entity = EntityTest::create($create_values);
    ...
    +        'field_text' => 'text item',
    ...
    +        'field_text' => [
    +          'value' => '<strong>This</strong> is <b>important.</b>',
    +          'format' => 'my_text_format',
    +        ],
    

    I think it'd be simpler to hardcode EntityTest::create(['field_text' => $text_item]), which would then simplify the data providers.

  2. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,117 @@
    +    $test_cases[] = [
    ...
    +    $test_cases[] = [
    

    Let's not use $blah[] to auto-append, let's instead use strings as array keys. This results in human-readable error reporting if the test fails :)

wim leers’s picture

Issue tags: +D8 cacheability
  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,31 @@
    +    $attributes['processed'] = $this->serializer->normalize($object->processed, $format, $context);
    

    A bigger thing I just thought of: this is not bubbling the necessary cacheability metadata.

    This is the first normalization case where that will actually be necessary.

    (For example: entity reference fields just contain the reference, they don't include that referenced entity in the response.)

    This is the first field whose data we send with REST EntityResource responses that is invalidated by certain cache tags and varies by certain cache contexts.

    So we need to figure out a way to bubble that up from the normalizer to the overall response.

  2. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,117 @@
    +   * @var \Symfony\Component\Serializer\Serializer
    

    Should typehint to the interface.

  3. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,117 @@
    +    $field_storage_config = FieldStorageConfig::create([
    ...
    +    $field_config = FieldConfig::create([
    

    Let's not assign these to a variable, because we don't use those variables anyway.

  4. +++ b/core/modules/text/text.services.yml
    @@ -0,0 +1,7 @@
    +    # This normalizer needs to be the same or greater priority than serializer.normalizer.field_item.hal
    

    Needs two more leading spaces :)

damiankloip’s picture

Wouldn't the cacheability metadata be a problem with any response if we want to use this data for caching? All entity fields could be cached etc..? It could be a problem for entity references if someone added their own normalizer for an entity reference field I guess.

wim leers’s picture

All entity fields could be cached etc..?

But until now, all fields are normalized to just the values that are stored in the entity. Hence the entity cache tag has been sufficient. To my knowledge this is the first example where the normalization depends on more than just the entity?

It could be a problem for entity references if someone added their own normalizer for an entity reference field I guess.

Yes.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new8.07 KB
new9.81 KB

#57 -1,2 - fixed
#58 - 2,3,4 - fixed
#58 - 1 tried a fix with @Wim Leers' help not working yet figure problem is in \Drupal\text\Normalizer\TextItemBaseNormalizer::bubble but have to a break from this.

Status: Needs review » Needs work

The last submitted patch, 61: 2626924-61.patch, failed testing.

wim leers’s picture

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -2,17 +2,26 @@
    - * This class does not implement DenormalizerInterface because the 'proccessed'
    + * This class does not implement DenormalizerInterface because the 'processed'
    

    Hah, nice catch :)

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -2,17 +2,26 @@
    +  protected $renderer;
    
    @@ -20,12 +29,49 @@ class TextItemBaseNormalizer extends ComplexDataNormalizer {
    +  public function __construct(RendererInterface $renderer) {
    ...
    +  protected function bubble(CacheableDependencyInterface $object) {
    

    Let's put all of this in a trait.

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -20,12 +29,49 @@ class TextItemBaseNormalizer extends ComplexDataNormalizer {
    +   * Constructs a TextItemBaseNormalizer service.
    

    c/p remnant

  4. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -20,12 +29,49 @@ class TextItemBaseNormalizer extends ComplexDataNormalizer {
    +      $build['#cache']['contexts'] = $object->getCacheContexts();
    +      $build['#cache']['tags'] = $object->getCacheTags();
    +      $build['#cache']['max-age'] = $object->getCacheMaxAge();
    

    Can be simplified to

    CacheableMetadata::createFromObject($object)->applyTo($build);
    

Now looking into the test failures.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new10.1 KB

Here's #61 applied to 8.2.x. We'll need separate patches for 8.2.x and 8.3.x because \Drupal\serialization\Tests\EntitySerializationTest was moved in 8.3.x.

This should have the same failures as #61.

Status: Needs review » Needs work

The last submitted patch, 64: 2626924-61-8.2.x.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new10.12 KB

Rebased on latest 8.2.x. Hopefully it'll apply this time.

wim leers’s picture

StatusFileSize
new10.22 KB
new1.35 KB

The problem was that your unit test serialized the same entity object twice. But the first time $entity->FIELD->processed was accessed, the computed value was stored on the entity object (see \Drupal\text\TextProcessed::getValue()). And so any text format changes would not be honored.

The fix is simple: apply normalization to cloned entity objects, so that each normalization operates on a "fresh" entity object.

wim leers’s picture

StatusFileSize
new10.47 KB
new2.58 KB
  1. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,160 @@
    +    $context = new RenderContext();
    +    $data = $this->container->get('renderer')
    +      ->executeInRenderContext($context, function () use ($entity) {
    +        return $this->serializer->normalize($entity);
    +
    +      });
    +    $this->assertEquals($expected, $data['field_text'][0]);
    

    This is not asserting that the expected cacheability metadata was bubbled.

    (This did not cause the test failure, but is an incompleteness in the test coverage.)

  2. $filter_format = FilterFormat::load('plain_text');
    

    We should not hardcode this inline!

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,77 @@
    +class TextItemBaseNormalizer extends ComplexDataNormalizer {
    +  /**
    

    Needs a newline in between.

  4. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,77 @@
    +    if (empty($value['format'])) {
    

    Is this really possible in the real world? I'd think this is only possible in this unit test scenario that you've got going?

  5. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,77 @@
    +      // The default filter format.
    +      $filter_format_id = 'plain_text';
    

    This should most definitely not be hardcoded, it should be retrieved from configuration. Again, if its' even necessary.

  6. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,77 @@
    +      $filter_format_id = $value['format'];
    
    +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,160 @@
    +        $filter_format = FilterFormat::load('plain_text');
    

    We call these variables $text_format, not $filter_format.

  7. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,160 @@
    +      $context = new RenderContext();
    +
    +      $data = $this->container->get('renderer')
    +        ->executeInRenderContext($context, function () use ($entity) {
    +          return $this->serializer->normalize($entity);
    +
    +        });
    

    Spacing issues.

I only addressed points 1 & 2, the rest is up to tedbow.

wim leers’s picture

Having looked at \Drupal\text\TextProcessed::getValue() due to #67, I came to realize something sad.

It does this:

$this->processed = check_markup($text, $item->format, $item->getLangcode());

And check_markup() does this:

function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to_skip = array()) {
  $build = array(
    '#type' => 'processed_text',
    '#text' => $text,
    '#format' => $format_id,
    '#filter_types_to_skip' => $filter_types_to_skip,
    '#langcode' => $langcode,
  );
  return \Drupal::service('renderer')->renderPlain($build);
}

In other words: the cacheability metadata of the filtering process itself is never added. Therefore this is still missing cache tags of e.g. referenced files processed by the \Drupal\editor\Plugin\Filter\EditorFileReference text filter.

We'll need to change TextItemBaseNormalizer to not merely access and normalize ->processed (which is what the patch does right now: $attributes['processed'] = $this->serializer->normalize($object->processed, $format, $context);), we will need to do something like check_markup(), but without rendering it in isolation (which is what renderPlain() does, we need it to render with bubbling! So we need TextItemBaseNormalizer::normalize() to do something like:

  $build = array(
    '#type' => 'processed_text',
    '#text' => $object->value,
    '#format' => $object->format,
    '#filter_types_to_skip' => [],
    '#langcode' => '',
  );
  $attributes['processed'] = $this->serializer->normalize($this->renderer->render($build), $format, $context);

The last submitted patch, 66: 2626924-61-8.2.x.patch, failed testing.

The last submitted patch, , failed testing.

Status: Needs review » Needs work

The last submitted patch, 68: 2626924-68.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new11.08 KB
new7.39 KB

re #68
4.

Is this really possible in the real world? I'd think this is only possible in this unit test scenario that you've got going?

Yes if it is plain text field will be empty here. But will default to the default filter in processing. I have step debugged to check this.

3, 5, 6, 7 fixed

will look into #69

Status: Needs review » Needs work

The last submitted patch, 73: 2626924-73.patch, failed testing.

wim leers’s picture

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -49,7 +63,7 @@ public function normalize($object, $format = NULL, array $context = []) {
           // The default filter format.
    

    s/default/fallback/

  2. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -36,7 +36,7 @@ class TextItemBaseNormalizerTest extends KernelTestBase {
    -  static $fallbackFormatId = 'plain_text';
    +  protected static $fallbackFormatId = 'plain_text';
    

    Better :)

martin107’s picture

Status: Needs work » Needs review
StatusFileSize
new11.09 KB
new1.57 KB

#75.1 fixed.

While following along ... I notice a very small thing

we can specify that a method parameter should be an array.

wim leers’s picture

Status: Needs review » Needs work

#76: nice, thanks!

This still needs work for #69.

The last submitted patch, 76: 2626924-76.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new12.97 KB
new3.78 KB

Fixed the fails in #76. Had to check if the filter actually loads before calling \Drupal\Core\Cache\CacheDependencyBubbleTrait::bubble

Addressed #69
$attributes['processed'] = $this->serializer->normalize($this->renderer->render($build), $format, $context);
Doesn't work because you will get the error.

Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.

Used
$attributes['processed'] = $this->serializer->normalize($this->renderer->renderPlain($build), $format, $context);
Add to fix \Drupal\Tests\serialization\Kernel\EntitySerializationTest
It was checking text processing using the format filter "full_format" but didn't actually create this FilterFormat.
Also it was using this test message

ComplexDataNormalizer produces expected array for $fieldName.

That is not accurate because not all fields are using ComplexDataNormalizer, text and entity reference for sure. So fixed that.
Also it was providing 'processed' when it was creating the entity to normalize. This value should not be provided on create but should be a result of normalization.

dawehner’s picture

  1. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -134,7 +160,7 @@ public function testNormalize() {
    -      $this->assertEqual($expected[$fieldName], $normalized[$fieldName], "ComplexDataNormalizer produces expected array for $fieldName.");
    +      $this->assertEqual($normalized[$fieldName], $expected[$fieldName], "Field normalization produces expected array for $fieldName.");
    

    Instead of swapping the expected and the normalized variable, you could also just append an "s" to assertEqual and get the same effect :)

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,85 @@
    +      '#format' => $object->format,
    ...
    +    $value = $field_item->getValue();
    

    I'm wondering whether its guaranteed that the object has a format. I guess the rendering process applies a default value otherwise? It is also weird that we once fetch it via $object->format and once via $field_item->getValue()['format'], it would be nice to be consistent here.

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,85 @@
    +    /** @var \Drupal\text\Plugin\Field\FieldType\TextItemBase $field_item */
    +    $field_item = $object;
    

    You could name $object directly $field_item in the method itself, see https://3v4l.org/qN9IV

wim leers’s picture

  1. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -60,6 +61,29 @@ protected function setUp() {
    +    // Create filter because it is needed text normalization.
    

    s/filter/text format/

    Also, broken English.

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,7 +57,14 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +    $build = array(
    

    s/array()/[]/

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,7 +57,14 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +    $attributes['processed'] = $this->serializer->normalize($this->renderer->renderPlain($build), $format, $context);
    

    This is wrong. This is still losing the bubbled cacheability metadata.

    You can either:

    1. use renderPlain() and then retrieve the cacheability metadata:
      …->renderPlain($build)…;
      $this->bubble(CacheableMetadata::createFromRenderArray($build));
      
    2. or, just use render() like I proposed, and do that in a render context. And a render context is in fact already provided by \Drupal\rest\RequestHandler::renderResponse(). This is also why that $this->bubble(…) call works.
      So when you said you were getting an error, I'd bet that you were getting that error only in tests… because you forgot to set up a render context there.

    Either way is fine. I think #1 may actually even be better because it's more explicit.

  4. The fact that tests are passing despite the previous point means there's missing test coverage. None of the filters you're using are bubbling cacheability metadata. I suggest using \Drupal\filter_test\Plugin\Filter\FilterTestCacheTags.
  5. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -68,7 +75,10 @@ public function normalize($object, $format = NULL, array $context = []) {
    +    if ($filter = FilterFormat::load($filter_format_id)) {
    

    $filter is a grossly misleading variable name here. It should be $text_format. $filter corresponds to \Drupal\filter\Plugin\FilterInterface, which is a very different thing.

wim leers’s picture

Status: Needs review » Needs work
tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.67 KB
new6.69 KB

@Wim Leers and @dawehner thanks for reviews

re #80
1. fixed.
2. I moved the logic to get the default format if needed above this and use that.
Changed to be more consistent.
3. fixed

re #81
1. fixed
2. fixed
3 & 4
I added your suggest about renderPlain
I also update the test to add the tags from \Drupal\filter_test\Plugin\Filter\FilterTestCacheTags::process and the required cache contexts for the contain paramater.

The test is passing.

5. fixed

tedbow’s picture

StatusFileSize
new5.68 KB

Sorry interdiff was wrong.

wim leers’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Cache/CacheDependencyBubbleTrait.php
    @@ -0,0 +1,34 @@
    + * Provides bubble function to apply cache dependency to render context.
    ...
    +trait CacheDependencyBubbleTrait {
    

    "CacheDependency" makes no sense; the term is "CacheableDependency".

    I think ConditionalCacheabilityMetadataBubblingTrait would be a better name.

    Should also document when it is okay to use this. Something like:
    Should be used with great care. Should only be used by things that may be used both inside of a render context, and outside. For example: generating URLs for CLI vs for HTTP responses, serializing/normalizing data for scripts vs for HTTP responses, et cetera.

  2. +++ b/core/lib/Drupal/Core/Cache/CacheDependencyBubbleTrait.php
    @@ -0,0 +1,34 @@
    +   * Bubbles the bubbleable metadata to the current render context.
    

    This is not bubbling bubbleable metadata (which implies attachments, per \Drupal\Core\Render\BubbleableMetadata), it's only bubbling cacheability metadata.

  3. +++ b/core/lib/Drupal/Core/Cache/CacheDependencyBubbleTrait.php
    @@ -0,0 +1,34 @@
    +  protected function bubble(CacheableDependencyInterface $object) {
    +    if ($this->renderer()->hasRenderContext()) {
    +      $build = [];
    +      CacheableMetadata::createFromObject($object)->applyTo($build);
    +      $this->renderer()->render($build);
    +    }
    +  }
    

    This is equivalent to \Drupal\Core\Render\MetadataBubblingUrlGenerator::bubble() with one exception: this is bubbling only cacheability metadata, whereas \Drupal\Core\Render\MetadataBubblingUrlGenerator::bubble() is also bubbling attachments. That's an important difference.

  4. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -60,6 +61,29 @@ protected function setUp() {
    +    // Create a text format because it is needed for text normalization.
    

    s/text/TextItemBase/

  5. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -60,6 +61,29 @@ protected function setUp() {
    +    // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize().
    

    I think an @see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions() would actually be more appropriate.

  6. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -60,6 +61,29 @@ protected function setUp() {
    +      'format' => 'full_html',
    +      'name' => 'Full HTML',
    +      'filters' => [
    +        'filter_html' => [
    +          'module' => 'filter',
    +          'status' => TRUE,
    +          'weight' => 10,
    +          'settings' => [
    +            'allowed_html' => '<p>',
    +          ],
    +        ],
    +        'filter_autop' => [
    +          'module' => 'filter',
    +          'status' => TRUE,
    +          'weight' => 10,
    +          'settings' => [],
    +        ],
    

    This does not remotely look like the Full HTML text format. So why call it that? Let's give it a random name.

    'format' => $this->randomMachineName(),
    'name' => $this->randomString(),
    
  7. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,86 @@
    +    $build =[
    

    s/=[/= [/

  8. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,86 @@
    +      '#type' => 'processed_text',
    +      '#text' => $value['value'],
    +      '#format' => $filter_format_id,
    +      '#filter_types_to_skip' => [],
    +      '#langcode' => '',
    +    ];
    +    $attributes['processed'] = $this->serializer->normalize($this->renderer->renderPlain($build), $format, $context);
    +    $this->bubble(CacheableMetadata::createFromRenderArray($build));
    

    We're missing a comment that explains why we're doing all this and not just using $field_item->processed.

    I also think that moving the rendering logic to a protected helper method would make the code more understandable.

  9. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,86 @@
    +    if ($text_format = FilterFormat::load($filter_format_id)) {
    +      $this->bubble($text_format);
    +    }
    

    We don't need to do this; \Drupal\filter\Element\ProcessedText::preRenderText() already does it for us. Hence line 79 will already contain those cache tags + contexts.

    (Because the filtered text, i.e. the output to be cached, does itself also depend on the text format config entity: if it changes, then the filtered output may need to change too.)

  10. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,174 @@
    +      'format' => 'my_text_format',
    +      'name' => 'My text format',
    

    This is not random, but it's also not something that's identifiable/established. So this is fine.

  11. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,174 @@
    +          'status' => TRUE,
    ...
    +          'status' => 1,
    ...
    +          'status' => TRUE,
    

    Let's be consistent.

  12. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,174 @@
    +        'filter_test_cache_tags' => [
    

    Let's also add filter_test_cache_contexts.

  13. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,174 @@
    +          'module' => 'filter',
    

    This is wrong, it's provided by the filter_test module.

    Also, you can just remove those keys. We don't have them in FilterAdminTest either, for example.

  14. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,174 @@
    +    if ($text_format->id() == 'my_text_format') {
    +      // Add tags that are hardcoded FilterTestCacheTags::process().
    +      $tags = array_merge($tags, ['foo:bar', 'foo:baz']);
    +    }
    

    It doesn't make sense to conditionally add expectations. It's just a clear sign that these expectations belong in the data provider.

    Let's let the data provider return a CacheableMetadata object with the expectations.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new15.35 KB
new9.8 KB

1. fixed
2. fixed
3. added comment
4. fixed
5. I think TextItemBaseNormalizer::normalize is better because that is where you see that test won't work without the actual filter being created. Before this patch you could have pointed TextItemBase::propertyDefinitions() to but it was being normalized without the need for filter actually being created because it was handled by ComplexDataNormalizer.
6. Changed it to 'my_text_format because that is what other tests are using see 10.
7. fixed
8. created new function normalizeProcessed and added comment in there.
9. removed. thanks for the explination
10. ok
11. fixed
12. done

13. removed
14. Ok added to dataProvider. For others benefit you have to call addContexts not setContexts in dataprovider because setContexts checks for valid contexts which since Drupal is not available when the dataProvider function is called will always throw a not very help error.

dawehner’s picture

Personally it would be great to answer #2626924-40: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata in the issue summary. Its maybe a question others will have as well.

+++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
@@ -0,0 +1,105 @@
+      $filter_format_id = $this->config->get('filter.settings')->get('fallback_format');

Should also add the filter.settings config cache key to it or do we not care about that?

damiankloip’s picture

Also, just putting it out there (haven't reviewed properly yet), it seems like a lot of this logic is happening in the nromalizer. This suggests to me that maybe this should be happening in the field item itself in some way instead? Then it could fix the problem for all other cases? This solution looks like it totally relies on global(ish) state or the renderer anyway.

wim leers’s picture

Status: Needs review » Needs work

Looking great! Very close.

  1. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,42 @@
    + * Provides bubble function to apply cache dependency to render context.
    

    Still says "cache dependency".

  2. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,42 @@
    +   * Bubbles the bubbleable cacheability metadata to the current render context.
    

    s/the bubbleable//

  3. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,42 @@
    +   *   The cacheable dependency object.
    

    s/The/A/

  4. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,42 @@
    +  /**
    +   * Gets the render service.
    +   *
    +   * @return \Drupal\Core\Render\RendererInterface
    +   *   The renderer.
    +   */
    +  protected function renderer() {
    +    return isset($this->renderer) ? $this->renderer : \Drupal::service('renderer');
    +  }
    

    This can simply be removed AFAICT.

  5. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,105 @@
    +  protected $config;
    ...
    +    $this->config = $config_factory;
    

    Let's rename $this->config to $this->configFactory.

    There's 70 cases of that in core, and only one that does it like this code is currently doing it.

  6. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,105 @@
    +    $attributes['processed'] = $this->normalizeProcessed($value, $format, $context);
    

    This is so much clearer!

    But I expected this to contain a $ths->serializer->normalize(…) call.

    And then for the helper method to only do the "generate 'processed' property's value" stuff. And for that helper method to receive $field_item, not $field_item->getValue(). Because then you can even typehint the helper method's parameter to TextItemBase $field_item :)

  7. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,105 @@
    +      $filter_format_id = $this->config->get('filter.settings')->get('fallback_format');
    

    @dawehner is right in #87. We can fix that by changing this to:

    $filter_settings = $this->config->get('filter.settings');
    $this->bubble($filter_settings);
    $filter_format_id = $filter_settings->get('fallback_format');
    
  8. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,184 @@
    +    $cacheability = new BubbleableMetadata();
    

    s/$cacheability/$expected_cacheability/

  9. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,184 @@
    +    $cacheability = new CacheableMetadata();
    ...
    +      $cacheability,
    

    That creation of a new instance can just be moved into the array.

  10. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,184 @@
    +    $test_cases['no_format_specified'] = [
    ...
    +    $test_cases['my_text_format'] = [
    

    You can have spaces in these array keys. It's meant to be a human-readable description.

  11. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,184 @@
    +    $cacheability = new CacheableMetadata();
    +    // Add the tags for the 'filter_test_cache_tags' filter.
    +    $cacheability->setCacheTags(['foo:bar', 'foo:baz']);
    +    // Add the contexts for the 'filter_test_cache_contexts' filter.
    +    $cacheability->setCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]);
    ...
    +      $cacheability,
    

    This one can also be moved into the array itself. That'd make this data provider a lot easier to read.

    (new CacheableMetadata())
      // comment
      ->setCacheTags(…)
      // comment
      ->setCacheContexts(…)
    
wim leers’s picture

This suggests to me that maybe this should be happening in the field item itself in some way instead? Then it could fix the problem for all other cases? This solution looks like it totally relies on global(ish) state or the renderer anyway.

See #69 for a full explanation. A short addition to what I wrote there: the normalization system comes from Symfony, which has no concept of cacheability metadata. Drupal's normalization system is effectively identical to Symfony's — we didn't layer anything on top. Therefore, if we want our GET responses to be cacheable, then we need to bubble cacheability metadata in a way that's supported.

The only alternative is to modify \Drupal\text\TextProcessed::getValue(), so that it doesn't assign a \Drupal\Component\Render\MarkupInterface to $this->processed, but a value object containing a string, plus cacheability metadata. But that'd then be a BC break for anything that is already using the processed property of TextItemBase fields.

IOW: given the constraints (normalization system not designed for cacheability or BC break in TextProcessed), this is the only feasible solution that I see.

If you see an alternative solution, I'd love to hear about it :)

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new15.64 KB
new9.42 KB

@dawehner thanks for review.
Yes adding $this->bubble($filter_settings);
@Wim Leers thanks for review
1-5 fixed
6. Chatted with @Wim Leers about this and helped me clear this up.

Resulted in return FilterProcessResult::createFromRenderArray($build)->setProcessedText($processed_text);
In helper function \Drupal\text\Normalizer\TextItemBaseNormalizer::normalizeProcessed

The problem is this seems to be the first call to FilterProcessResult::createFromRenderArray in core as far as I can tell.
This ultimitely calls \Drupal\Core\Cache\CacheableMetadata::createFromRenderArray
which contains $meta = new static(); so this throws an exception

Warning: Missing argument 1 for Drupal\filter\FilterProcessResult::__construct(), called in /var/www/d8_3_rest/core/lib/Drupal/Core/Cache/CacheableMetadata.php on line 150 and defined in Drupal\filter\FilterProcessResult->__construct() (line 80 of core/modules/filter/src/FilterProcessResult.php).

This fixed by making the argument to \Drupal\filter\FilterProcessResult::__construct optional.
I did this.

7. fixed
8. fixed
9. fixed
10. fixed
11. fixed

dawehner’s picture

The only alternative is to modify \Drupal\text\TextProcessed::getValue(), so that it doesn't assign a \Drupal\Component\Render\MarkupInterface to $this->processed, but a value object containing a string, plus cacheability metadata. But that'd then be a BC break for anything that is already using the processed property of TextItemBase fields.

What I don't understand, why can't this new code at least call out to $item->processed and then extract the cacheable metadata from it?

wim leers’s picture

What I don't understand, why can't this new code at least call out to $item->processed and then extract the cacheable metadata from it?

Again: because $item->processed contains a MarkupInterface object, which does not contain cacheability metadata. We could change that, but that'd break BC.


This looks almost ready to me. Just a few more nits/clarifications:

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,99 @@
    +   * Normalizes the processed attribute.
    ...
    +  protected function normalizeProcessed(TextItemBase $field_item) {
    

    This is not normalizing anything anymore. This is computing the processed attribute while retaining cacheability metadata.

    Also should get an @see \Drupal\text\TextProcessed::getValue() plus an explanation like the one in #90 to answer the question in #88/#92, which is important to have documented for sure.

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,99 @@
    +   * @return FilterProcessResult
    

    Needs FQCN.

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,99 @@
    +    // The rendering process needs to be applied to caching. New cacheability
    +    // metadata can be introduced in the process.
    +    // @see \Drupal\editor\Plugin\Filter\EditorFileReference::process().
    

    The first sentence makes no sense.

    I think all of this can be simplified to It's necessary to capture the cacheability metadata associated with the processed text. See https://www.drupal.org/node/2278483.

dawehner’s picture

Again: because $item->processed contains a MarkupInterface object, which does not contain cacheability metadata. We could change that, but that'd break BC.

Sorry, that was not obvious for me. This though means we can create a render context, process the data and then extract the cacheability metadata out of that? I really try to let us have a normalizer which is minimal as possible. There is no reason to have logic in the normalizer, its not super special what this one tries to solve.

wim leers’s picture

Sorry, that was not obvious for me.

Yep, totally understandable. Hence I repeated the key elements calmly :)

This though means we can create a render context, process the data and then extract the cacheability metadata out of that?

Yes. That's what \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::renderResponseBody() does.

I really try to let us have a normalizer which is minimal as possible. There is no reason to have logic in the normalizer, its not super special what this one tries to solve.

+1! I'd love that.
But the unfortunate reality is that normalizers generate output that ends up in responses. Those responses need cacheability metadata. So the normalizers must bubble cacheability metadata. Hence the $this->bubble() call below. Unfortunately, \Drupal\text\TextProcessed::getValue() currently computes that property in a way that discards cacheability metadata and changing that would break BC. Hence the $processed_text = $this->normalizeProcessed($field_item); call below.

+  public function normalize($field_item, $format = NULL, array $context = []) {
+    $attributes = parent::normalize($field_item, $format, $context);
+    $processed_text = $this->normalizeProcessed($field_item);
+    $this->bubble($processed_text);
+    $attributes['processed'] = $this->serializer->normalize($processed_text->getProcessedText(), $format, $context);
+    return $attributes;
+  }

So given these constraints, do you see a better way to do this, or do you also think this is the only way?

tedbow’s picture

StatusFileSize
new16.07 KB
new2.59 KB

1. fixed

plus an explanation like the one in #90 to answer the question in #88/#92, which is important to have documented for sure.

I did my best for an explanation but let me know what you think
2. fixed
3. fixed

wim leers’s picture

Much better! :) Looks great/ready to me. Just a few final nits.

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,22 +57,31 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +   * Computes the processed attribute.
    

    s/processed/'processed'/

    s/attribute/property of TextItemBase fields/

    Add an

     * @see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions()
    
  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,22 +57,31 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +   * The 'processed' field attribute for fields that extend TextItemBase will
    

    s/field attribute/property/

  3. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,22 +57,31 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +   * not contain cacheability metadata. Therefore this function returns a
    

    will not contain cacheability metadata (see \Drupal\text\TextProcessed::getValue()).

  4. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,22 +57,31 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +   * FilterProcessResult which can be used to get both the processed text and
    

    s/can be used/can carry/

  5. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -57,22 +57,31 @@ public function __construct(RendererInterface $renderer, ConfigFactoryInterface
    +   * cacheable.
    

    s/cacheable/cached (and invalidated) correctly/


I'd RTBC this, but dawehner has indicated on Twitter that he wants to reply to #95, but has not yet found the time. Let's give him the time: https://twitter.com/da_wehner/status/801143016257949696.

tedbow’s picture

StatusFileSize
new16.23 KB
new1.71 KB
new1.81 MB

Much better! :) Looks great/ready to me. Just a few final nits.

so close, I am like...

Ok. Here is a patch that addresses all the nits

wim leers’s picture

:D

As I explained in #97, I'd RTBC, but this also needs the dawehner stamp of approval :)

dawehner’s picture

StatusFileSize
new1.98 KB

So what I was thinking we could do instead is the following. Feel free to ignore me, just hacked that together in a minute.

As I explained in #97, I'd RTBC, but this also needs the dawehner stamp of approval :)

Sorry, I don't want to block anything :(

wim leers’s picture

So we "kinda" discussed this via Twitter/GitHub gist. Copying that verbatim here, from https://gist.github.com/dawehner/283fb5ab0dcd0ceefc88df9afc84e0ec.

dawehner
So what I was thinking was creating a render context, execute $item->processed in there, and then call $this->bubble($render_context->bubble());
Wim Leers
What does
execute $item->processed

mean?
Either it exists, or it doesn't. When it doesn't exist, it's generated by \Drupal\text\TextProcessed::getValue(). When it does exist, it's a MarkupInterface instance, which doesn't allow cacheability metadata to be retrieved, nor does it allow "execution" of any way.

So, can you clarify?

dawehner
$Item->processed is using the fieldapi magic, sorry, I should have made this clear.
If you "call" $item->processed, its called out \Drupal\text\TextProcessed to retrieve its value: \Drupal\text\TextProcessed::getValue ... which calls out to check_markup, which renders it using ::renderPlain.

So in case we would create a render context around this magic here, wouldn't we be able to catch the cacheability metadata from the render context?

Wim Leers
Ah! Yes, that's possible. Except in the case of $item->processed already having been generated. Then the magic wouldn't happen again, and we wouldn't be able to catch the cacheability metadata.
wim leers’s picture

So dawhner's idea could work. With the caveat that I outlined in #101: it's built only once, so if it was already built, then it wouldn't work. (caveat 1)

But there's another problem: ::renderPlain() prevents bubbling. IOW: once you call check_markup(), you have no chance at all to get at the cacheability metadata. That's very much intentional! Quoting the docs for check_markup():

/**
  * Runs all the enabled filters on a piece of text.
  *
  * …
  *
  * Note: this function should only be used when filtering text for use elsewhere
  * than on a rendered HTML page. If this is part of a HTML page, then a
  * renderable array with a #type 'processed_text' element should be used instead
  * of this, because that will allow cacheability metadata to be set and bubbled
  * up and attachments to be associated (assets, placeholders, etc.). In other
  * words: if you are presenting the filtered text in a HTML page, the only way
  * this will be presented correctly, is by using the 'processed_text' element.

i.e. it's specifically designed for use cases like sending e-mails. In fact, that's specifically the reason we kept it. See #2217877: Text filters should be able to add #attached, #post_render_cache, and cache tags, and the change record, which states: You should only use it in scenarios where you're not rendering HTML (e.g. a plain-text e-mail whose contents are processed text).
(caveat 2)

Third, I'm not at all certain it'd be okay to always bubble the cacheability metadata for the filtered/processed text from \Drupal\text\TextProcessed::getValue(). Because there's always a render context when responding to a route (because of \Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber). Doing this automatically could be more trouble than it's worth. That being said, we absolutely could change \Drupal\text\TextProcessed::getValue() to not bubble, but to at least contain a value object that contains cacheability metadata. A \Drupal\filter\FilterProcessResult would make perfect sense… because that's what we already use for this very purpose.


In other words: the only way we can have safe bubbling of cacheability metadata for $item->processed, is if it contains a value object that implements CacheableDependencyInterface. It currently contains a MarkupInterface object. Changing that would break BC.

I see two options:

  1. To do this, we would not be able to use check_markup() anymore in \Drupal\text\TextProcessed::getValue(), instead we would need to move TextItemBaseNormalizer::computeProcessedAttribute() to TextProcessed. End result: $item->processed instanceof \Drupal\filter\FilterProcessResult === TRUE.
  2. Update \Drupal\Core\Render\Markup to also implement CacheableDependencyInterface and the getters of AttachmentsInterface (we'd need to split up AttachmentsInterface to multiple interfaces). Then the Markup class would contain not just the HTML markup string, but also the associated cacheability metadata and attachments.

Either of those options would allow for this code in TextItemBaseNormalizer:

+  public function normalize($field_item, $format = NULL, array $context = []) {
+    $attributes = parent::normalize($field_item, $format, $context);
+    $this->bubble($field_item->processed);
+    $attributes['processed'] = $this->serializer->normalize((string)$field_item->processed, $format, $context);
+    return $attributes;
+  }

instead of the current patch's:

+  public function normalize($field_item, $format = NULL, array $context = []) {
+    $attributes = parent::normalize($field_item, $format, $context);
+    $processed_text = $this->normalizeProcessed($field_item);
+    $this->bubble($processed_text);
+    $attributes['processed'] = $this->serializer->normalize($processed_text->getProcessedText(), $format, $context);
+    return $attributes;
+  }

I like the sound of that last one. But it's a big undertaking, with a high probability of not succeeding.

So, I'm thinking we should indeed not block this on that. The implementation introduced here doesn't lock us down. In fact, as you can see from the above two code snippets, it would be absolutely trivial to start using this in the future :)

dawehner’s picture

An alternative approach could be to introduceprocessed_filtered or so, which has the cacheable metadata attached, instead of bubbled up magically.

wim leers’s picture

@dawehner: But "processed" implies "filtered". "To process" and "to filter" were (and still are) used as synonyms in the filter module. (Yes, that's confusing.) Also, it'd imply adding a new property to \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions().

dawehner’s picture

Also, it'd imply adding a new property to \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions().
Sure this means that, but is that a problem? It is still a computed property.

wim leers’s picture

It means a duplicate computed property. And adding a property is also a BC break I suspect?

dawehner’s picture

It means a duplicate computed property. And adding a property is also a BC break I suspect?

I seriously cannot see how an additional computed property is a BC break.

damiankloip’s picture

This is looking good. It's kind of sad we have to involve so much of the render system here, in a normalizer.. but hey ho. Not the fault of this issue! Here are a couple of points:

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,109 @@
    +      $this->bubble($filter_settings);
    

    Why do we bubble the settings directly here when we'er using the fallback format but not in the case below when we have a format. I think the processed_text element handles that anyway? So this part can be removed - we just need the filter format ID?

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,109 @@
    +    // It's necessary to capture the cacheability metadata associated with the
    +    // processed text. See https://www.drupal.org/node/2278483.
    +    $processed_text = $this->renderer->renderPlain($build);
    +    return FilterProcessResult::createFromRenderArray($build)->setProcessedText($processed_text);
    

    This comment is misleading here I think, as renderPlain doesn't allow us to capture the metadata anyway? Do we need to get processed text here. Can't we get the metadata, then render it? Maybe in the normalize method'?

Initially I had similar thoughts to Daniel, with a new computed property when reading this issue through. @WimLeers what BC issues do you see with this approach? I could see that property just returning what we currently have the in the normalizer method and be done (same code and the TextItem already pretty much).

wim leers’s picture

StatusFileSize
new17.56 KB
new2.33 KB

#108

  1. Because we only consult the filter.settings configuration when the if (empty($value['format'])) { condition is met. We then use that configuration to look up the default text format ID.

    The end result is that we always end up with a text format ID, and use that to process the text using that text format. This results in \Drupal\filter\Element\ProcessedText::preRenderText() being called, which always associates the text format's cache tags. (See the end of that function.)

  2. renderPlain() does not bubble the cacheability metadata, but it also doesn't throw it away: $build contains the bubbled cacheability metadata at $build['#cache'] (and the bubbled attachments at $build['#attached']).

    That means we can capture it from there and then bubble it manually, which is what this does.

#108.1 + #108.2 made me see one small detail that could be improved: the burden lies on the normalize() method to do explicit bubbling. This is great. But computeProcessedAttribute() still bubbles something in one case: #108.1. We should change that code to not do bubbling separately. And, in fact, \Drupal\filter\Element\ProcessedText::preRenderText() should handle this for us, because it already has logic to deal with fallback format. But turns out it's not bubbling the filter.settings config's cacheability metadata. So that's the real thing we should fix here.

Status: Needs review » Needs work

The last submitted patch, 109: 2626924-109.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.77 KB
new19.28 KB

Aha, 6 new failures! This makes sense — #98 was rolled before #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method landed. This is great! Because it's proving that the changes this patch is making to TextItemBase fields' normalizer is also affecting the output for any entity type that has such a field :) Which is exactly what we want of course.

Once you fix that, it complains that there is an unexpected processed property… because we're adding that here. Again, yay!

This reroll fixes the test expectations just for Comment entities. Leaving it to @tedbow to fix the others.

wim leers’s picture

#108

Initially I had similar thoughts to Daniel, with a new computed property when reading this issue through. @WimLeers what BC issues do you see with this approach? I could see that property just returning what we currently have the in the normalizer method and be done (same code and the TextItem already pretty much).

The addition of a new property can break existing PHP code, because it doesn't expect this new property, since that's changing the data model. Whereas the approach in this patch is not breaking the data model, it's just fixing the normalization.

If we're going to add a new computed property, we might as well change \Drupal\text\TextProcessed::getValue() to not use check_markup(), and to change it to the logic used in \Drupal\text\Normalizer\TextItemBaseNormalizer::computeProcessedAttribute() instead.

The last submitted patch, 111: 2626924-111.patch, failed testing.

The last submitted patch, 111: 2626924-111.patch, failed testing.

tedbow’s picture

StatusFileSize
new20.56 KB
new1.84 KB

Added changes to TermResourceTestBase for testing text processing

Status: Needs review » Needs work

The last submitted patch, 115: 2626924-115.patch, failed testing.

dawehner’s picture

The addition of a new property can break existing PHP code, because it doesn't expect this new property, since that's changing the data model. Whereas the approach in this patch is not breaking the data model, it's just fixing the normalization.

If we're going to add a new computed property, we might as well change \Drupal\text\TextProcessed::getValue() to not use check_markup(), and to change it to the logic used in \Drupal\text\Normalizer\TextItemBaseNormalizer::computeProcessedAttribute() instead.

Well, let's be clear. The second alternative you proposed is worse, as it changes the behaviour of existing normal expected codepaths. My suggestion above is not touching those. Sure, you could access any object in PHP and set a random value on there, given that even adding protected variables to classes are BC breaks.

I think it would be though worth discussing concrete examples of broken code and then expand our BC policy to make things clearer.

  1. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,101 @@
    +      '#filter_types_to_skip' => [],
    +      '#langcode' => '',
    

    These are the default values, so we could skip them.

  2. +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php
    @@ -0,0 +1,101 @@
    +    $processed_text = $this->renderer->renderPlain($build);
    +    return FilterProcessResult::createFromRenderArray($build)->setProcessedText($processed_text);
    

    Its such a sad api, mutating our value object after construction time. Well it is not the fault of this patch for sure, but we should try to improve our APIs in those areas still

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new24.95 KB
new8.6 KB

Let's see:

4 files changed, 41 insertions(+), 87 deletions(-)

Status: Needs review » Needs work

The last submitted patch, 118: 2626924-118.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new2.73 KB
new26.38 KB

Some temporary fixes ... I had on my local machine

Status: Needs review » Needs work

The last submitted patch, 120: 2626924-120.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new28.57 KB
new6.45 KB

This could fix a good amount of failures.

Status: Needs review » Needs work

The last submitted patch, 122: 2626924-122.patch, failed testing.

damiankloip’s picture

Status: Needs work » Needs review
StatusFileSize
new28.3 KB
new786 bytes
damiankloip’s picture

Status: Needs review » Needs work

The last submitted patch, 124: 2626924-124.patch, failed testing.

damiankloip’s picture

Funnily/ironically the failures here would be fixed by #2751325: All serialized values are strings, should be integers/booleans when appropriate I think :D

wim leers’s picture

dawehner’s picture

Title: Expose TextItems' "processed" property when normalizing » [pp-1] Expose TextItems' "processed" property when normalizing
Status: Needs work » Postponed

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

Title: [pp-1] Expose TextItems' "processed" property when normalizing » Expose TextItems' "processed" property when normalizing
Status: Postponed » Needs review
tedbow’s picture

Assigned: Unassigned » tedbow

#124 doesn't apply. re-rolling

dawehner’s picture

@tedbow
Did you got distracted :)

almaudoh’s picture

StatusFileSize
new27.25 KB

Drive-by re-roll. There was a conflict with parts of #2751325: All serialized values are strings, should be integers/booleans when appropriate which had same fixes for maybe same things. So I left those out of the re-roll. Hope I didn't miss something.

Status: Needs review » Needs work

The last submitted patch, 134: 2626924-134.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new31.16 KB
new4.14 KB

Ok fixed a couple tests.

\Drupal\Tests\hal\Kernel\NormalizeTest
This was taking into account "proccessed". Had to add a custom filter to use like in \Drupal\Tests\serialization\Kernel\EntitySerializationTest

EntitySerializationTest
Changed to use \Drupal\Component\Serialization\Json::encode instead of json_encode b/c there is difference in handle special characters and this what the serializer service will actually use.
also fixed a missing comma

\Drupal\Tests\text\Kernel\TextWithSummaryItemTest
In this test after the filter was set you actually have to reset the entity cache, save and reload the entity before the process text change will show up.
This not a test that was touched before in this issue so is this a change in behavior.
This seems reasonable but because the test passed before it seems you didn't have to before. Does that sound right?

All the other test failures from #134 seemed to from
$this->assertEquals($this->getExpectedCacheContexts(), empty($cache_contexts_header_value) ? [] : explode(' ', $cache_contexts_header_value));
...rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:408
b/c cache_contexts_header_value is returning the 'url.site' for Comments, Terms. There are also 2 test bases that in this patch updated getExpectedCacheContexts

wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update, +Needs tests

This patch is kind of hard to come back to if you haven't seen it in a while. The IS is not at all helpful. We need the IS to be helpful for core committers.


This seems reasonable but because the test passed before it seems you didn't have to before. Does that sound right?

The fail was already present in #122. So it was introduced between #118 and #122, where @dawehner refactored how TextProcessed works.

I'm fine with continuing this direction.

  1. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,32 @@
    +trait ConditionalCacheabilityMetadataBubblingTrait {
    

    We should refactor this away just like we did at #2825812-66: ImageItem should have an "derivatives" computed property, to expose all image style URLs.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
    @@ -64,6 +65,7 @@ protected function createEntity() {
    +      ->setDescription("It is a little known fact that llamas cannot count higher the seven.")
    
    @@ -93,8 +95,9 @@ protected function getExpectedNormalizedEntity() {
    -          'value' => NULL,
    +          'value' => 'It is a little known fact that llamas cannot count higher the seven.',
               'format' => NULL,
    +          'processed' => "<p>It is a little known fact that llamas cannot count higher the seven.</p>\n",
    

    This is effectively a functional test. Great!

    But shouldn't we add an explicit one? Let the text module subclass \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase, let it add a text field in its setUp(). Then assert that the text field is serialized as expected.

  3. +++ b/core/modules/text/src/TextProcessedResult.php
    @@ -48,7 +49,17 @@ public function getValue() {
    -      $this->processed = check_markup($text, $item->format, $item->getLangcode());
    ...
    +        '#langcode' => '',
    

    We've lost the langcode.

  4. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,32 @@
    +trait ConditionalCacheabilityMetadataBubblingTrait {
    

    We should refactor this away just like we did for #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs.

  5. +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php
    @@ -0,0 +1,32 @@
    +trait ConditionalCacheabilityMetadataBubblingTrait {
    

    We should refactor this away, just like we did for #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs.

frob’s picture

Issue summary: View changes

You are correct, the IS was actually wrong due to a typo.

I have rewritten the IS. Hopefully this makes it easier for maintainers.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new13.41 KB
new35.95 KB

@Wim Leers thanks review. finally getting back this!
1. Yes! refactored. copied \Drupal\rest\EventSubscriber\ResourceResponseSubscriber directly from [#11955703]. Deleted ConditionalCacheabilityMetadataBubblingTrait
2. added an explicit test.
3. Added langcode here
4,5 @see item 1

Status: Needs review » Needs work

The last submitted patch, 139: 2626924-139.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new4.16 KB
new38.67 KB

Ok hopefully this will get the tests passing.

Most of the test fails in #139 where because the expected contexts were not correct. This was fixed by updates to :: getExpectedCacheContexts()

After that was fixed it exposed another tricker problem.
Basically that problem was that:

  1. \Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait::applyHalFieldNormalization() was adding 'lang' key for all field values which was working except for the processed text fields.
  2. The 'lang' key was being added at the end of the array but the $actual field values for these hal fields didn't have the 'lang' at the end of the array
  3. because \Drupal\hal\Normalizer\TextItemBaseNormalizer::normalize() is adding 'processed' after it calls \Drupal\hal\Normalizer\FieldItemNormalizer::normalize().
  4. So 'processed' is at the end of the array.

Without making applyHalFieldNormalization() much more complicated the order of the field keys is not going to match but that doesn't seem like something we should be testing for that I think

So I created \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::nestedKsort() to called that instead of ksort().

This fixes the problem but I am just realizing 1 performance problem is that we would be doing a nested sort not only in HAL but also json format when don't need to be.
I am sure that is worth the complexity it would add to solve it for just performance reasons.

The only other fail was the test normalizer in \Drupal\Tests\serialization\Kernel\FieldItemSerializationTest need to have a higher priority than the new Text item normalizer so it would still be used in the test.

wim leers’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests

So I created \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::nestedKsort() to called that instead of ksort().

One of the other rest or serialization patches is doing the same thing! (Difficult to find which one, d.o doesn't have "patch search" functionality.)

This fixes the problem but I am just realizing 1 performance problem is that we would be doing a nested sort not only in HAL but also json format when don't need to be.
I am sure that is worth the complexity it would add to solve it for just performance reasons.

Eh, I'm not remotely concerned about this. These arrays are not enormous. Who cares if it takes 0.01 seconds or 0.02 seconds.

The only other fail was the test normalizer in \Drupal\Tests\serialization\Kernel\FieldItemSerializationTest need to have a higher priority than the new Text item normalizer so it would still be used in the test.

Nice — it's great to see you updated the docs to reflect this. Although it'd be good to document why you choose that particular priority.


Time for another round of patch review — hopefully the last!

  1. +++ b/core/modules/hal/hal.services.yml
    @@ -8,6 +8,11 @@ services:
    +      - { name: normalizer, priority: 20 }
    

    Let's document why we want priority 20.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1217,4 +1217,19 @@ protected function assertResourceNotAvailable(Url $url, array $request_options)
    +  protected function nestedKsort(&$array) {
    

    Let's make this static, to prove that this doesn't rely on any object (instance) data.

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php
    @@ -0,0 +1,81 @@
    +        'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.',
    

    :)

  4. +++ b/core/modules/text/src/TextProcessedResult.php
    @@ -0,0 +1,88 @@
    +class TextProcessedResult extends TypedData {
    

    Should we add a @todo that says this is deprecated and will be removed before 9.0.0, because this is what we'll make \Drupal\text\TextProcessed behave like?

  5. +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php
    @@ -0,0 +1,176 @@
    +class TextItemBaseNormalizerTest extends KernelTestBase {
    

    Ideally we would make this a unit test, but given the amount of things we'd need to mock, I think a kernel test is justified here.

  6. Test coverage I asked for in #137 is present now.
tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new2.29 KB
new38.94 KB

1. added
2. fixed. But also wondering if this would be better in \Drupal\simpletest\AssertHelperTrait. It seems it could be useful in other situations. If not there then maybe \Drupal\Tests\rest\Functional\ResourceTestBase because at least it could be useful for other custom REST tests.
3. :)
4. Added @deprecated. Would it still need a @todo. But also I look at the class comments for these 2 classes and they are exactly the same.
I also noticed that there was old comment that mentioned "check_markup" so I also updated that since it is not being called anymore.
5. good we are in agreement

Status: Needs review » Needs work

The last submitted patch, 143: 2626924-143.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new612 bytes
new38.94 KB
+++ b/core/modules/hal/hal.services.yml
@@ -12,6 +12,7 @@ services:
+      // The priority needs to higher than serializer.normalizer.field_item.hal.

Wow, how many tests can I break with just 1 line :(

wim leers’s picture

#143.2: maybe, but adding this new helper method to that generic trait is out of scope here. Let's first see if there are other tests that need it. Premature abstraction and all that.
#145: :)

The IS was updated by @frob in #138 (thanks!). But it's still confusing. I updated the problem/motivation section. We still need the proposed resolution explained, and why this solution was chosen over others.

We also need a CR to make people aware that the processed property is now being normalized.

Once the IS is updated and CR is created, this is RTBC.

Finally, this is a contributed project blocker for both JSON API (#2775205: [PP-2] [FEATURE] Discus how to make JSON API use text input formats for long text fields) and GraphQL (#2690347: [PP-1] Expose rendered text fields (text+format)).

frob’s picture

Assigned: tedbow » Unassigned
Issue summary: View changes

Did my best to add the proposed resolution and the reasoning behind it.

For the CR is this enough detail?

"Exposed TextItems' "processed" property when normalizing for web services."

tedbow’s picture

I think this method might be overly complicated
See: #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts

If that seems like good idea we should probably postpone this.

wim leers’s picture

Title: Expose TextItems' "processed" property when normalizing » [PP-1] Expose TextItems' "processed" property when normalizing
Status: Needs work » Postponed
Related issues: +#2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts

#2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts sure looks appealing. In theory we could commit the current patch and then remove the normalizers this patch adds when #2871591 happens. But we risk breaking BC then, so it's probably safer to postpone this until we reach consensus in #2871591.

frob’s picture

What about pushing this patch in and setting the default to true for TextItems (with regard to #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts) when we remove the normalizers.

As it has been stated several times in this thread, it doesn't make sense to not include processed values for TextItems. Therefore, TRUE would be the logical default for TextItems anyway.

tedbow’s picture

Status: Postponed » Needs review
StatusFileSize
new23.87 KB
new47.13 KB

Ok first off @frob sorry right now I am not getting your point and too tired to look back through this issue, long day. Will try to address later.
Or maybe this patch will help clear it.

These patches are to bring inline with #2871591-26: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts
There I have posted a patch that rips out all "TextItems" specific changes

Here 2626924-151-do-not-test.patch contains only "TextItems" specific changes
It would fail but can be used to look at what would need once #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts lands

2626924-152-including-2871591.patch as you might guess includes all changes need for both issues.
Hopefully should pass.

Setting to need review to see if it passes but still should probably postpone on #2871591

wim leers’s picture

Status: Needs review » Postponed

#151: NICE! The new patch is lovely: much easier to understand, because all the infrastructure work is offloaded to #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts. This issue then just needs to take advantage of that new infrastructure.

And as a triple bonus of this approach:

  1. this new patch allows OpenAPI documentation for the normalization to be generated automatically
  2. this new patch allows https://www.drupal.org/project/jsonapi to benefit automatically, without needing to duplicate the normalizer there
  3. this new patch allows https://www.drupal.org/project/graphql to benefit automatically, without needing to duplicate custom logic there

Marking postponed. This is RTBC-worthy as soon as #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts is in!

dawehner’s picture

I have some question about this issue and basically every other issue blocked by #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts

Can we work on the computed properties already, but skip the normalization step work ... and then once #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts is in, which seemed to be blocked by typed data resources, we can iterate on that.

wim leers’s picture

Can we work on the computed properties already, but skip the normalization step work ..

+1, we need that first anyway. If you have the time to reroll this, I'll review! If not, no worries. Great point in any case :)

dawehner’s picture

Status: Postponed » Needs review
StatusFileSize
new23.08 KB

I'm totally happy to work on it for a bit.

Here is a quick reroll.

Status: Needs review » Needs work

The last submitted patch, 155: 2626924-155.patch, failed testing. View results

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new11.41 KB
new11.67 KB

Now this removes the files which should not have to be touched.

Status: Needs review » Needs work

The last submitted patch, 157: 2626924-157.patch, failed testing. View results

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new24.47 KB
new65.59 KB

Status: Needs review » Needs work

The last submitted patch, 161: 2626924-161_plus_2910211-6.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Great — it's working for Comment REST test coverage! But since we last worked on this, #2835845: EntityResource: Provide comprehensive test coverage for BlockContent entity also landed, and that contains a TextItem field too, and hence it needs to be updated. All test failures are coming from the BlockContent REST test coverage.

wim leers’s picture

Title: [PP-2] Expose TextItems' "processed" property when normalizing » [PP-1] Expose TextItems' "processed" property when normalizing

#2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts is in, #2910211: Allow computed exposed properties in ComplexData to support cacheability. is the last remaining blocker (was split off from #2871591 to make its scope narrower and help it land sooner), and that too is now RTBC!

wim leers’s picture

Status: Needs work » Postponed
wim leers’s picture

Title: [PP-1] Expose TextItems' "processed" property when normalizing » Expose TextItems' "processed" property when normalizing
Status: Postponed » Needs review
StatusFileSize
new2.38 KB
new24.28 KB

#2910211: Allow computed exposed properties in ComplexData to support cacheability. landed, so this is finally unblocked!

Rebased #161 on top of HEAD. One hunk thing in there didn't apply at all anymore: the ComplexDataPropertiesNormalizerTrait hunk couldn't apply anymore, since that file didn't make it in the final patch for #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts. That patch hunk was doing some hardcoded string casting. But … turns out that wasn't actually necessary!

The example that #2910211: Allow computed exposed properties in ComplexData to support cacheability. added test coverage for (\Drupal\entity_test\TypedData\ComputedString) is doing class ComputedString extends TypedData implements CacheableDependencyInterface { … }. If we'd do the same for TextProcessedResult, we won't even need that hardcoded string casting. This is what the interdiff shows.

Hopefully this has the same passes/failures as @tedbow's last patch in #161.

wim leers’s picture

StatusFileSize
new1.8 KB
new26.02 KB

This should fix the failures in #161 (and the failures in #166).

The last submitted patch, 166: 2626924-166.patch, failed testing. View results

wim leers’s picture

StatusFileSize
new11.23 KB
new24.42 KB

While doing #166, I realized something… we've been adding a new computed property process_result because the existing computed property processed's behavior cannot be changed. Well, thanks to #2910211: Allow computed exposed properties in ComplexData to support cacheability., we don't need to change its behavior anymore!

Since my comment in #69, we were on this path to add a new computed property. But #69 dates back to the days we were wanting to add a new normalizer. Thanks to @damiankloip's comment in #88 (which pointed out we were doing an awful lot in the normalizer, which meant we probably needed to do something in the field item). In #90, I answered that doing what he suggested would be a BC break. @dawehner questioned my response in #92, to which I replied in #93 and #95. @dawehner then further clarified his idea in #101 and #102. That discussion is how we ended up adding a new computed property. The discussion then continued in #103 through #117, where BC concerns were discussed more and more.

Then the issue became fairly quiet, culminating in #149, where this became blocked on #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts.

Thanks to #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts and #2910211: Allow computed exposed properties in ComplexData to support cacheability., we can now vastly simplify the complex discussion we had before:

  1. we can change the internals of TextProcessed, which means we can remove TextProcessedResult
  2. we can let TextProcessed implement CacheableDependencyInterface
  3. thanks to #2910211, TypedDataNormalizer::normalize() will bubble cacheability metdata
  4. end result: we can fix the bug without break BC at all: everything continues to behave as it does in HEAD, and no new computed property is added
wim leers’s picture

StatusFileSize
new2.94 KB
new24.4 KB
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php
@@ -0,0 +1,81 @@
+        'process_result' => '<p>Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.</p>' . "\n",

+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -134,6 +160,7 @@ public function testNormalize() {
+          'process_result' => "<p>{$this->values['field_test_text']['value']}</p>",

Oops, forgot to update this in #169.

wim leers’s picture

StatusFileSize
new3.73 KB
new22.16 KB
  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1365,4 +1365,19 @@ protected function assertResourceNotAvailable(Url $url, array $request_options)
    +  public static function nestedKsort(&$array) {
    

    This is no longer used, so no longer necessary. (#2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX already added a similar method and already fixed the order-sensitivity.)

  2. +++ b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml
    @@ -2,5 +2,5 @@ services:
    -      # 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 }
    

    These changes are no longer necessary: we're not adding a normalizer anymore.

  3. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -57,7 +60,7 @@ public function isEmpty() {
    -      if ($definition->getClass() == '\Drupal\text\TextProcessed') {
    +      if (in_array($definition->getClass(), [TextProcessed::class, TextProcessedResult::class], TRUE)) {
    

    And this second class doesn't exist anymore as of #169, so we can revert this change too!

  4. +++ b/core/modules/text/src/TextProcessed.php
    @@ -64,4 +84,29 @@ public function setValue($value, $notify = TRUE) {
    +  public function getCacheTags() {
    +    $this->getValue();
    +    return $this->processed->getCacheTags();
    +  }
    +
    +
    +  public function getCacheContexts() {
    +    $this->getValue();
    +    return $this->processed->getCacheContexts();
    +  }
    +
    +  public function getCacheMaxAge() {
    +    $this->getValue();
    +    return $this->processed->getCacheMaxAge();
    +  }
    

    This is sloppy, should be cleaned up.

The last submitted patch, 169: 2626924-169.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 170: 2626924-170.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 171: 2626924-171.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.72 KB
new22.15 KB
+++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php
@@ -152,6 +175,7 @@ public function testNormalize() {
+          'process_result' => "<p>{$values['field_test_text']['value']}</p>",

+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -197,6 +198,7 @@ protected function getExpectedNormalizedEntity() {
+          'process_result' => '<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>' . "\n",

Dammit, I forgot these too! 😡

They're responsible for 7 of the 10 failures.

wim leers’s picture

StatusFileSize
new875 bytes
new22.22 KB

And then the 3 remaining failures:

1) Drupal\Tests\comment\Functional\CommentTokenReplaceTest::testCommentTokenReplacement
Comment token [comment:body] replaced.
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'zAH2Moug
+'&lt;p&gt;zAH2Moug&lt;/p&gt;
 '

/var/www/html/core/tests/Drupal/Tests/BrowserTestBase.php:1240
/var/www/html/core/tests/Drupal/KernelTests/AssertLegacyTrait.php:57
/var/www/html/core/modules/comment/tests/src/Functional/CommentTokenReplaceTest.php:131

+ Drupal\Tests\node\Kernel\NodeTokenReplaceTest + Drupal\Tests\taxonomy\Functional\TokenReplaceTest.

Looks like these are affected by the changes I made in #169, which I thought were going to be transparent… but turns out I only overlooked a small detail. FilterProcessResult::getProcessedText() returns a string, but check_markup() returns a FilteredMarkup.

Thankfully we had that test coverage! Plus, it helps prove that this patch indeed is not breaking BC! 👍

Should be green again now.

dagmar’s picture

Thanks for working on this!

  1. +++ b/core/modules/filter/src/FilterProcessResult.php
    @@ -78,7 +78,7 @@ class FilterProcessResult extends BubbleableMetadata {
    +  public function __construct($processed_text = '') {
    

    Do we need this change?

  2. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -7,6 +7,8 @@
    +use Drupal\text\TextProcessed;
    +use Drupal\text\TextProcessedResult;
    

    This two use statements seems not being used.

  3. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -29,7 +31,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +      ->setInternal(FALSE);
    

    I think this line of code should have at least a comment about why this is important. After all you worked a lot to make the method setInternal available :)

wim leers’s picture

StatusFileSize
new3.14 KB
new21.84 KB
  1. +++ b/core/lib/Drupal/Core/TypedData/ComputedPropertyTrait.php
    @@ -0,0 +1,42 @@
    \ No newline at end of file
    

    Nit.

  2. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -7,6 +7,8 @@
    +use Drupal\text\TextProcessed;
    +use Drupal\text\TextProcessedResult;
    

    No longer necessary.

  3. +++ b/core/modules/text/src/TextProcessed.php
    @@ -36,21 +41,36 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
    +      // It's necessary to capture the cacheability metadata associated with the
    +      // processed text. See https://www.drupal.org/node/2278483.
    

    Can be clarified a bit.

  4. +++ b/core/modules/text/src/TextProcessed.php
    @@ -64,4 +84,37 @@ public function setValue($value, $notify = TRUE) {
    +   * @return string[]
    

    Oops.

  5. +++ b/core/modules/text/src/TextProcessed.php
    @@ -64,4 +84,37 @@ public function setValue($value, $notify = TRUE) {
    +  public function getCacheTags() {
    ...
    +  public function getCacheContexts() {
    ...
    +  public function getCacheMaxAge() {
    

    These can and should live on the trait.

wim leers’s picture

StatusFileSize
new888 bytes
new22.04 KB

#177:

  1. Good question. Yes, this is necessary. @tedbow did this in #91. Without this, you'll get an error like this:
    1) Drupal\Tests\taxonomy\Functional\TokenReplaceTest::testTaxonomyTokenReplacement
    Missing argument 1 for Drupal\filter\FilterProcessResult::__construct(), called in /Users/wim.leers/Work/d8/core/lib/Drupal/Core/Cache/CacheableMetadata.php on line 150 and defined
    
    /Users/wim.leers/Work/d8/core/modules/filter/src/FilterProcessResult.php:81
    /Users/wim.leers/Work/d8/core/lib/Drupal/Core/Cache/CacheableMetadata.php:150
    /Users/wim.leers/Work/d8/core/lib/Drupal/Core/Render/BubbleableMetadata.php:66
    /Users/wim.leers/Work/d8/core/modules/text/src/TextProcessed.php:72
    /Users/wim.leers/Work/d8/core/lib/Drupal/Core/TypedData/ComputedPropertyTrait.php:31
    /Users/wim.leers/Work/d8/core/modules/text/src/TextProcessed.php:45
    /Users/wim.leers/Work/d8/core/lib/Drupal/Core/Field/FieldItemBase.php:140
    /Users/wim.leers/Work/d8/core/lib/Drupal/Core/Field/FieldItemList.php:127
    /Users/wim.leers/Work/d8/core/modules/taxonomy/tests/src/Functional/TokenReplaceTest.php:83
    

    (Because FilterProcessResult::createFromRenderArray() eventually calls \Drupal\Core\Cache\CacheableMetadata::createFromRenderArray() which does new static() to instantiate a new value object of this type. These are side effects-free value objects, hence the need to instantiate a new object.

    class FilterProcessResult extends BubbleableMetadata extends CacheableMetadata means subclasses may not add new required parameters to methods, which FilterProcessResult was violating.)

  2. Heh, I already did that in #178.2 :)
  3. 👍 Done.

The last submitted patch, 175: 2626924-172.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 176: 2626924-173.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 178: 2626924-177.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 179: 2626924-179.patch, failed testing. View results

wim leers’s picture

In theory, \Drupal\text\TextProcessed::setValue() should throw \Drupal\Core\TypedData\Exception\ReadOnlyException. Because the docs of \Drupal\Core\TypedData\TypedDataInterface::setValue() say this:

   * @throws \Drupal\Core\TypedData\Exception\ReadOnlyException
   *   If the data is read-only.

But unfortunately, this requires changes elsewhere. It requires either \Drupal\Core\TypedData\Plugin\DataType\Map::setValue() or \Drupal\serialization\Normalizer\FieldItemNormalizer::denormalize() to be updated so that it doesn't try to set values for computed properties anymore, like it does in HEAD.

Fixing that is out of scope for this issue, and is less important/less impactful than committing the current patch. So we should keep TextProcessed::setValue() unchanged. Another example with the same problem is \Drupal\datetime\DateTimeComputed::setValue().

I opened #2921890: Computed properties' classes' ::setValue() method should throw ReadOnlyException for this.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new946 bytes
new22.9 KB

Hah, the tiny change in #176 now causes failures in all other tests. So clearly, that didn't work.

  1. Turns out that TextProcessed::getValue() must return a MarkupInterface object to not break BC.
  2. But TextProcessed::getValue() should return a string for it to be normalized correctly.

And the solution is … to bring back the one hunk from @tedbow's #161 patch that I dropped in #166!

wim leers’s picture

Title: Expose TextItems' "processed" property when normalizing » TextItem's "processed" computed property should be non-internal and carry cacheability metadata

#185 is ready for review, but I'll still need to work on an IS update. Here's an issue title update already.

tedbow’s picture

@Wim Leers thanks for getting this moving again. Will look again but for now a couple thoughts

  1. +++ b/core/modules/text/src/TextProcessed.php
    @@ -12,12 +16,14 @@
    -   * @var string|null
    +   * @var \Drupal\filter\FilterProcessResult|null
        */
       protected $processed = NULL;
    

    \Drupal\Core\TypedData\ComputedPropertyTrait relies on $process. Will other computed properties always have this? If so should we move the property to the trait. Maybe then just say it implements CacheableDependencyInterface? Or will it always in other fields?

  2. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -29,7 +29,11 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +      // Computed properties are internal by default. This property opts out
    +      // from that default, to prevent the need for external systems to
    +      // reimplement filters to compute this value.
    +      ->setInternal(FALSE);
    

    Just a note since we are doing this in TextItemBase then any field type in custom code or contrib that extends this will automatically have processed text exposed in normalization which is a change since computed properties were not exposed before.

    Maybe this is fine but I think should be noted. The other alternative would be to just in the core subclasses

    $properties = parent::propertyDefinitions($field_definition);
    $properties['processed']->setInternal(FALSE);
    
wim leers’s picture

StatusFileSize
new2.99 KB
new23.08 KB
  1. ComputedPropertyTrait relies on $process

    😳 D'oh, stupidly overlooked that, good catch!

    Fixed this, by settling on the default of $this->value that \Drupal\Core\TypedData\TypedData prescribes.

  2. Good catch. I'd think that they'd want that though: text processing is what distinguishes TextItem(Base) compared to StringItem: the processing/filtering of the stored string value. Otherwise, they might as well use StringItem(Base). So if they specifically chose to use TextItemBase, then they also chose text processing/filtering, and that means they've been wanting this to be supported all this time, and this gives it to them.
wim leers’s picture

After I posted that, wrt #187.1/#188.1: perhaps it's better to simply not yet add a trait, to wait to do that until there's at least one more implementation that needs it?

tedbow’s picture

#188.1 That looks good.
2. Ok makes sense.

#189 Yeah waiting on the trait would make sense to me.

wim leers’s picture

StatusFileSize
new6.01 KB
new22.03 KB

#190: done. Makes the patch smaller, too!

Also another self-review:

  1. +++ b/core/modules/filter/src/Element/ProcessedText.php
    @@ -69,7 +70,11 @@ public static function preRenderText($element) {
    +      CacheableMetadata::createFromRenderArray($element)
    

    Could use a comment.

  2. +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php
    @@ -18,6 +19,28 @@ class NormalizeTest extends NormalizerTestBase {
    +    // Create a text format because it is needed for TextItemBase normalization.
    +    // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize().
    

    Stale comment.

  3. +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
    @@ -63,6 +65,29 @@ protected function setUp() {
    +    // Create a text format because it is needed for TextItemBase normalization.
    +    // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize().
    

    Same.

amateescu’s picture

I'm so glad that you dropped the approach with the new computed property! Here's a short review for now:

  1. +++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
    @@ -19,7 +19,12 @@ class TypedDataNormalizer extends NormalizerBase {
    +    // Support for stringable value objects: avoid numerous custom normalizers.
    +    if (is_object($value) && method_exists($value, '__toString')) {
    +      $value = (string) $value;
    +    }
    

    Note that there is a getString() method on TypedDataInterface ;)

  2. +++ b/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php
    @@ -81,6 +81,8 @@ public function testCrudAndUpdate() {
    +    $entity->save();
    +    $entity = $storage->load($entity->id());
    

    Why is this change needed?

tedbow’s picture

StatusFileSize
new4.34 KB
new20.96 KB

re #192 @amateescu thanks for the review!
1. Thanks for the tip. Now using this to simplify the normalizer.
2. First I commented these just confirm they are needed. The test fails without it.

I think because they way \Drupal\text\TextProcessed is written now anytime you update $text or $format properties of the parent item, $this->getParent() then the processed value will no longer be valid. This is because \Drupal\text\TextProcessed::ensureComputedValue() just checks $this->valueComputed === FALSE. So if the it has been computed once it will not compute it again. This is by design but if you update the format or text of the parent item then the processed value will not be valid anymore.

So you have to save and reload the $entity. If we want to fix this then we should check if these have been changed since the value was computed. To do this I got rid of $this->valueComputed and replaced it with $computedBuild which is what we will use for \Drupal\Core\Render\RendererInterface::renderPlain and \Drupal\Core\Render\BubbleableMetadata::createFromRenderArray in \Drupal\text\TextProcessed::computeValue.

So we can just check if $computedBuild is the same as the last time it was computed. If it is then we don't need to computed again.

amateescu’s picture

That's a bit better but now I wonder why are all these changes to \Drupal\text\TextProcessed actually needed. The new getCacheTags(), getCacheContexts() and getCacheMaxAge() methods from that class can simply call getValue(), no?

wim leers’s picture

#192.2: because computed properties are not designed to automatically recompute if another property they depend on has changed in the mean time, because in the real world, that cannot happen: it can only happen in tests. Which is why the test was handily fixing the problem by resaving the entity. Although I now wonder if an even simpler solution is to simply not do static caching at all, and just go back to “compute on access, always”. That prevents premature optimization. Thoughts?

(Of course, I could be totally wrong, because I’m no Typed Data expert. The above is based on my understanding/interpretation of the intended Typed Data architecture.)

Also: very glad you like the overall approach! 🙂

Status: Needs review » Needs work

The last submitted patch, 193: 2626924-193.patch, failed testing. View results

amateescu’s picture

because computed properties are not designed to automatically recompute if another property they depend on has changed in the mean time

That's the job of the field item, see how the entity reference item handles it in \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::setValue() with the target_id property and the entity computed property. Which means that TextItemBase must be changed to do something similar.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.01 KB
new21.72 KB

#193's first interdiff hunk:

+++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
@@ -18,13 +18,9 @@ class TypedDataNormalizer extends NormalizerBase {
   public function normalize($object, $format = NULL, array $context = []) {
+    /* @var \Drupal\Core\TypedData\TypedDataInterface $object */
     $this->addCacheableDependency($context, $object);
-    $value = $object->getValue();
-    // Support for stringable value objects: avoid numerous custom normalizers.
-    if (is_object($value) && method_exists($value, '__toString')) {
-      $value = (string) $value;
-    }
-    return $value;
+    return $object->getString();
   }

This implements @amateescu's suggestion in #192.1. But it's what's causing these test failures.

The thing is that \Drupal\Core\TypedData\TypedDataInterface::getString() always returns a string representation of some typed data. Even for data that isn't necessarily stringable. This is causing significant changes in the normalized data structures, and is hence a pretty significant BC break.

Reverting that change.

wim leers’s picture

StatusFileSize
new4.18 KB
new4.28 KB
new20.55 KB

#197: thanks, that was the hint I needed! 👍 😀

But AFAICT, the really important thing is not \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::setValue(), it is \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onChange(). Because

$entity->summary_field->format = 'no_filters';

triggers FieldItemList::__set(), which triggers FieldItemBase::__set(), which triggers Map::set(), which calls both FieldItemBase::onChange(), or its override, in this case TextItemBase::onChange(). IOW: setValue() is never called.

(As a Typed Data non-expert, this is absolutely puzzling.)

So, here it's \Drupal\text\Plugin\Field\FieldType\TextItemBase::onChange(). In fact, all this needed to do was add this to TextProcessed:

  public function setValue($value, $notify = TRUE) {
    parent::setValue($value, $notify);
    $this->valueComputed = FALSE;
  }

… which I guess is maybe what you meant, but you said it had to happen at TextItemBase::setValue(), but in reality we had to do it at TextProcessed::setValue().

(The differences between set(), setValue(), writePropertyValue() are all beyond me.)

Looking on it back now, this makes sense: I renamed protected $processed to protected $value so we wouldn't need that silly setValue() override anymore. But now I'm ending up adding it back all the same! And look at HEAD, it didn't need that protected $valueComputed. It relied on semantics of Typed Data API to not need that boolean!

So we can simply remove protected $valueComputed. But then we're still renaming protected $processed to protected $value. That's kind of out of scope. I really made that change because I didn't fully understand how this was supposed to work. (I still don't, but I understand a bit more now.)

Which then brings me to that other remark:

That's a bit better but now I wonder why are all these changes to \Drupal\text\TextProcessed actually needed.

By not renaming protected $processed to protected $value, we can significantly reduce the amount of changes we need.

I strongly suspect (and hope!) that this is where @amateescu wanted us to go :)

berdir’s picture

  1. +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php
    @@ -29,7 +29,11 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
    +      ->setSetting('text source', 'value')
    +      // Computed properties are internal by default. This property opts out
    +      // from that default, to prevent the need for external systems to
    +      // reimplement filters to compute this value.
    +      ->setInternal(FALSE);
    

    Not fully convinced by the description, why do we need to repeat here that it is internal by default, explicitly setting it to FALSE kind of implies that?

  2. +++ b/core/modules/text/src/TextProcessed.php
    @@ -37,20 +40,29 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
    +    return FilteredMarkup::create($this->processed->getProcessedText());
    

    FilteredMarkup is currently documented as @internal and that it should only be used inside filter.module.

    Either this code is wrong or the documentation is :)

amateescu’s picture

By not renaming protected $processed to protected $value, we can significantly reduce the amount of changes we need.

Yes, that's what I was hoping to see.

wim leers’s picture

(Typing on my phone from 🏓 competition.)

#200.1: are you saying you think that comment is not necessary? I’d agree with that.

#200.2: Two reasons: A) check-markup()’s return value was of this type, and that was what was assigned to $this->processed so we must continue to return that for BC, B) this lives in the text module, but it’s tightly coupled to the filter module. Arguably this “TextProcessed extends TypedData” should live in the filter module and be called “Filtered” instead. But we again can’t do that, due to BC.

#201: hurray! Does that mean you like the current patch? :)

berdir’s picture

#200.1: are you saying you think that comment is not necessary? I’d agree
with that.

Kind of. The comment as it is is not very useful and not really necessary, question is, can or do we need to come up with a more useful comment that would make sense to keep? If not then I suppose we can also just remove it..

wim leers’s picture

StatusFileSize
new887 bytes
new20.35 KB

Done.

frob’s picture

Arguably this “TextProcessed extends TypedData” should live in the filter module and be called “Filtered” instead. But we again can’t do that, due to BC.

Should we create a new issue to figure out how to make this change with BC in place so we can deprecate the old interface? Or is that simply impossible to do BC.

berdir’s picture

TextItem and TextProcessed are pretty tightly coupled and require each other basically, so not sure how much sense it would make to move it or if it would really solve something.

IMHO, *if* there are valid reasons to call FilteredMarkup::create() outside of filter.module (and apparently there are, if this needs it then possibly other places that filter text might too) and there is no better way to do it, then it shouldn't be internal? :)

wim leers’s picture

not sure how much sense it would make to move it or if it would really solve something.

True, it wouldn't solve anything. But really, TextItem should be provided by filter.module, because "text" is really about "processed text", which uses filters.

if this needs it then possibly other places that filter text might too) and there is no better way to do it, then it shouldn't be internal? :)

We could do that, but as I just explained, I think that FilteredMarkup being used outside the filter module is artificial, because TextItem really shouldn't be in a text module, it should be in filter module.

berdir’s picture

I don't really agree with that, text.module *is* processed_text.module, that's all it does. non-processed text field types are string* and are in Drupal\Core, not text.module. And text.module depends on filter.module.

If we'd move TextItem, we'd have to merge the whole text module into filter.module.

wim leers’s picture

I don't really agree with that, text.module *is* processed_text.module, that's all it does. non-processed text field types are string* and are in Drupal\Core, not text.module. And text.module depends on filter.module.

You're confusing me, because I agree with this :P (Maybe I'm more jetlagged than I think I am, and am communicating poorly?)

If we'd move TextItem, we'd have to merge the whole text module into filter.module.

That is exactly what I think we eventually should do :)

berdir’s picture

Ok, if you meant that then that's different :)

Still, I'm not sure that merging the two modules makes sense. Yes, they overlap a lot in many ways, but in others, e.g. in terms of maintenance, it would IMHO not make much sense. As filter.module maintainer, you need to know a lot about security and how the text is actually processed internally. And while you need to know about the filter.module API to work on text.module, you don't really need to understand its internals and crazy regular expressions :)

Of course that's a rather theoretical point given that *neither* module currently has any officla maintainers, but it would IMHO be even harder possible maintainers for them.

wim leers’s picture

#200.2 got us really got on a theoretical tangent about text + filter module. Berdir questioned whether it's okay to use FilteredMarkup::create(). I explained why I think it is. Berdir, how you feel about it now?

tedbow’s picture

+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -230,4 +233,18 @@ public function testPatchPath() {
+  protected function getExpectedCacheTags() {
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']);
+  }

The "config:filter.settings" cache tag is expected here because the format is set to NULL in this test.

Should also be explicitly be testing this in EntityTestTextItemNormalizerTest?

Otherwise it looks good to me.

wim leers’s picture

StatusFileSize
new2.71 KB
new22.1 KB

The "config:filter.settings" cache tag is expected here because the format is set to NULL in this test.

Correct.

Should also be explicitly be testing this in EntityTestTextItemNormalizerTest?

Great point! Done. Took some inspiration from the testGetWithParent() that #2543726: Make $term->parent behave like any other entity reference field, to fix REST and Migrate support and de-customize its Views integration is adding.

Also fixed a small conflict with #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage.

Status: Needs review » Needs work

The last submitted patch, 213: 2626924-213.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review

#213 had nonsensical fails. DrupalCI problems, probably.

tedbow’s picture

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php
@@ -78,4 +79,54 @@ protected function getExpectedCacheContexts() {
+    FilterFormat::create([
+      'name' => 'Pablo Piccasso',
+      'format' => 'pablo',
+      'langcode' => 'es',
+      'filters' => [],
+    ])->save();

Very interested in what this filter does! 😉

#213 looks good for my test coverage point in #212

wim leers’s picture

Great! Pinged @Berdir for a review, since he had thoughts about this before.

Would be great to finally ship this in Drupal 8.5!

wim leers’s picture

Title: TextItem's "processed" computed property should be non-internal and carry cacheability metadata » Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata

Improving title.

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

IS updated.

wim leers’s picture

Issue summary: View changes
wim leers’s picture

Issue summary: View changes
wim leers’s picture

dagmar’s picture

I did another pass on the patch. some minor comments.

  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php
    @@ -180,4 +182,18 @@ protected function getExpectedUnauthorizedAccessCacheability() {
    +    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
    

    This is merging cache tags not consistently with the other parts of this patch. (parent::getExpectedCacheTags is always the second parameter).

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php
    @@ -0,0 +1,132 @@
    +  public function providerTestGetWithFormat() {
    

    This method could have a comment.

  3. +++ b/core/modules/text/src/TextProcessed.php
    @@ -12,12 +15,12 @@
    -   * @var string|null
    +   * @var \Drupal\filter\FilterProcessResult|null
    

    Should this be documented in the CR?

wim leers’s picture

@dagmar Thanks for the review!

  1. CommentResourceTestBase does the exact same thing. More importantly: the parameter order doesn't matter, because Cache::mergeTags() sorts the cache tags and only keeps the unique ones, precisely to make sure that never matters. In other words: we make sure that a list of cache tags behaves like a set (sadly PHP doesn't have native support for a "set" data structure.)
  2. It doesn't need a comment because it's just a data provider.
  3. Excellent question! 👌

    No, we took great care to return the exact same value. In HEAD, $this->processed = check_markup(…). check-markup() returns a \Drupal\Component\Render\MarkupInterface object. This means the doc saying string in HEAD is simply wrong.

    This patch indeed stores a value that doesn't implement MarkupInterface in $this->processed. But we also no longer simply return $this->processed:

    +++ b/core/modules/text/src/TextProcessed.php
    @@ -37,20 +40,29 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
    +    return FilteredMarkup::create($this->processed->getProcessedText());
    

    … we still return a MarkupInterface object — $this->processed's value and type is irrelevant: it's an implementation detail.

wim leers’s picture

Assigned: Unassigned » berdir

Pinged Berdir again about a review. I'd love to have his "RTBC +1" on this.

berdir’s picture

Assigned: berdir » Unassigned
Status: Needs review » Reviewed & tested by the community

@dawehner made an interesting remark in another issue recently, this can possibly result in a considerably increased response size as we essentially duplicate all formatted text strings, which on real content is usually going to be *a lot*. But I guess there's not much that we can do about that, certainly not without BC changes.

In a way, this is similar to the config API, you can either get the editable config, without overrides/formatting, when you want to edit/change it or synchronize the data between two sites, when you want to use/display it, you get the formatted data. But as I said, don't see how to get to anything like that without BC changes.

In regards to #200.2, I still think that calling an internal method from another module is a bit weird, lets see what the core committers have to say about that. In other cases, we use separate classes for that, for example \Drupal\Core\Field\FieldFilteredMarkup (although that also has special logic), or we could open an issue to not really make it internal but just document very explicitly when it should be used (which we basicalyl already do).

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 213: 2626924-213.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

Failed due to DrupalCI infra problems. Retesting, back to RTBC.

wim leers’s picture

Priority: Normal » Major

IDK why this is marked Normal, this is one of the top 8.5 priorities (#2905563: REST: top priorities for Drupal 8.5.x), so bumping to Major.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
    @@ -79,6 +80,7 @@ protected function createEntity() {
    +      ->setDescription("It is a little known fact that llamas cannot count higher the seven.")
    
    @@ -109,8 +111,9 @@ protected function getExpectedNormalizedEntity() {
    +          'value' => 'It is a little known fact that llamas cannot count higher the seven.',
    ...
    +          'processed' => "<p>It is a little known fact that llamas cannot count higher the seven.</p>\n",
    

    i know this is just test text, but I think it should be higher than seven

  2. +++ b/core/modules/text/src/TextProcessed.php
    @@ -37,20 +40,29 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
    +      $processed_text = $this->getRenderer()->renderPlain($build);
    

    Shouldn't we care about the bubble metadata this generates?

    i.e. shouldn't we be creating our own render context, calling ::executeInRenderContext and then adding the metadata from the context to our result?

    It looks to me like FilterProcessResult::createFromRenderArray() doesn't add any metadata here, because it only looks at the #cache key on $build. And given we generate that above, there are none.?

    It looks like it's the call to \Drupal\filter\Element\ProcessedText::preRenderText that happens during the rendering that generates the cacheability metadata - but I don't see how that is captured here. My understanding is that renderPlain just discards that metadata?

    I'd like to see our tests adding in filter plugins that add new cacheability metadata, e.g. filter_test_cache_contexts or something that embeds a file entity for example, where its cacheability metadata should be added.

    I note this was discussed as a short-coming of the current code in HEAD in #69 but dropped in #79

    Apologies in advance if I'm missing something obvious

Re #200.2, text module relies on filter module and in my opinion this is returning the result of a filtering operation, so is a valid use of the class. However, we could add a new method to FilterResult - something like ::toMarkup that would return this object for us, and avoid the need to call create from outside the filter module. Thoughts? Could that be something we could use elsewhere?

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new5.9 KB
new23.58 KB

Great remarks, @larowlan! :)

  1. Fixed.
  2. Yes, we should care, and we already do. renderPlain() already sets its own render context. The thing I think you're not aware of, is that during rendering, we not just bubble using a RenderContext, we also "materialize" or "reflect" that state into the render array. This has test coverage in \Drupal\Tests\Core\Render\RendererBubblingTest, \Drupal\Tests\Core\Render\RendererRecursionTest.

    However, you're right that the explicit test coverage is missing here. So, adding such explicit test coverage. Inspired by the explicit test coverage at \Drupal\Tests\filter\Kernel\FilterAPITest::testProcessedTextElement().

Re #200.2, text module relies on filter module and in my opinion this is returning the result of a filtering operation, so is a valid use of the class.

This is exactly my opinion too.

However, we could add a new method

This would be okay by me, although I'm not fully convinced. We'd be able to convert the two FilteredMarkup::create() calls added in this patch to use that new method, plus the one in \Drupal\filter\Element\ProcessedText::preRenderText(). We would not be able to convert the ones in \Drupal\filter\Plugin\Filter\FilterCaption::process() though.
I'm not sure it's worth it because we'd be expanding the API surface for no real gain IMHO.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 231: 2626924-231.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new3.02 KB
new23.93 KB

Hah, I should've tested more than only GET, then I would've noticed :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 233: 2626924-233.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new966 bytes
new23.96 KB

Ugh, #231 reported two failures. But actually, there were three:

Testing Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestTextItemNormalizerTest
....FFE

Time: 10.35 minutes, Memory: 4.00MB

There was 1 error:

1) Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestTextItemNormalizerTest::testDelete
…

--

There were 2 failures:

1) Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestTextItemNormalizerTest::testPost
…
2) Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestTextItemNormalizerTest::testPatch
…

d.o parses this incorrectly. This is why I didn't notice that testPatch() was also failing, or I'd have fixed that too in #233.

Anyway, simple enough to fix: it's a permissions problem.

larowlan’s picture

The thing I think you're not aware of, is that during rendering, we not just bubble using a RenderContext, we also "materialize" or "reflect" that state into the render array.

Yes, you're correct, I wasn't aware of that.

Was the materializing/reflecting added recently?

larowlan’s picture

Nope, #2543334: Auto-placeholdering for #lazy_builder with bubbling of contexts and tags added that, some time ago. Thanks, always learning stuff.

wim leers’s picture

Thanks, always learning stuff.

Thanks, likewise!

larowlan’s picture

adding review credit for reviews that changed the shape of the patch

larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed as 9ba7824 and pushed to 8.5.x.

Published changed record.

Unpostponed issues blocked on this.

  • larowlan committed 9ba7824 on 8.5.x
    Issue #2626924 by Wim Leers, tedbow, dawehner, frob, martin107,...
berdir’s picture

wim leers’s picture

Issue tags: +8.5.0 highlights

More than a year of working on building consensus both in this issue and in its blockers to add missing API infrastructure. I think this belongs in the release notes, and even highlights.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

jeqq’s picture

I've got this issue (a follow-up): #2972988: Error when saving a denormalized entity with text fields with "processed" property when testing with Relaxed.

larowlan’s picture

Blast from the past, but in a client project I've hit an issue with metadata not bubbling from the processed element.

In a formatter we're doing this

$output = [
  '#theme' => 'something',
  '#something' => $node->some_field->value,
  '#something_else' => $node->this_is_a_text_field->processed,
];

And seeing different bubbling metadata to

$output = [
  '#theme' => 'something',
  '#something' => $node->some_field->value,
  '#something_else' => [
    '#type' => 'processed_text',
      '#text' =>  $node->this_is_a_text_field->value,
      '#format' =>  $node->this_is_a_text_field->format,
  ],
];

And so I went looking into the git forensics of this line:

// Capture the cacheability metadata associated with the processed text.
$processed_text = $this->getRenderer()->renderPlain($build);

And that brought me here, and then I found my comment at #230 and went 🤔

So is the issue with using processed direct in a render array that inside the calculation of the property we set $this->processed to be the FilterProcessResult, which has the metadata, but we return a FilteredMarkup object which does not.

And there's no way for the external caller to access the processed property to get to that metadata.

But then I re-read the patch and saw that TextProcessed implements CacheableDependencyInterface and indeed all we were missing was an addCacheableDependency call on the property.

Sorry for the noise, but I thought that might help others who also found themselves in that boat

Wim++ for the explanation all those years ago that is still useful today

larowlan’s picture

Actually, if $this->processed has attachments, there's no way for calling code to access those right? Because they're not part of CacheableDependencyInterface

i.e. shouldn't TextProcessed also implement AttachmentsInterface, its feasible that a filter format would attach css/js etc.

frob’s picture

That sounds less like it is feasible and more like that is the purpose of a WYSISYG based text field filter. Sounds like this should be written up as a new issue.