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:
AddTextItemNormalizerthat 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
- lots of boiler plate
- 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)
- it also doesn't fix it for alternative API architectures, such as https://www.drupal.org/project/graphql
- 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.
-
- BLOCKER: #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts
- Make the existing
processedproperty non-internal (see #2871591]). - BLOCKER: #2910211: Allow computed exposed properties in ComplexData to support cacheability.
- Update
\Drupal\text\TextProcessedfromclass TextProcessed extends TypedData {}toclass TextProcessed extends TypedData implements CacheableDependencyInterface {}. - 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). - Add missing cacheability metadata when using the fallback text format.
- Updated test coverage expectations:
BlockContentResourceTestBase,CommentResourceTestBase,TermResourceTestBaseandEntitySerializationTestare all testing atextfield already in HEAD. They must be updated to now expect a newprocessedproperty in the normalizations. - Explicit test coverage:
EntityTestTextItemNormalizerTestis 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.
| Comment | File | Size | Author |
|---|---|---|---|
| #235 | 2626924-235.patch | 23.96 KB | wim leers |
| #213 | 2626924-213.patch | 22.1 KB | wim leers |
| #204 | 2626924-204.patch | 20.35 KB | wim leers |
| #199 | 2626924-199.patch | 20.55 KB | wim leers |
| #198 | 2626924-198.patch | 21.72 KB | wim leers |
Comments
Comment #2
wim leersWithout this, it can can indeed be painful/difficult to consume text data entered in Drupal.
\Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions()says:So, just exposing that computed
processedproperty would fix this.Comment #3
wim leersComment #4
frobComment #5
wim leersYay, thanks for taking this on @frob! I will happily provide reviews! Let's do this :)
Comment #6
frobWhoops, I thought I assigned a different issue to myself. But, whatever, lets do this. :)
Comment #7
frobI 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
Comment #8
frobWriting 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
Comment #9
frobHere 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?
Comment #10
frobThat one has some obvious mistakes. Here is another roll.
Comment #11
frobComment #12
martin107 commentedI 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.
Comment #13
wim leersThe comment and the if-test don't match.
Comment #14
frobYou caught me. Copy and paste error.
Comment #15
wim leersSo the only big thing missing here, is test coverage! Great work :)
This comment needs to be adjusted to match what this class actually does.
Nit: s/array()/[]/
What's missing here is the third parameter. The entity has a langcode, we should pass that there.
I think these comments can just be omitted, I think both speak for themselves.
Comment #16
frobStill needs tests, if anyone wants to help with that. Cleaned up everything else.
Comment #17
frobComment #19
frobI 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?
Comment #20
frobIt still needs tests
Comment #21
wim leersYes!
Look at:
\Drupal\serialization\Tests\NormalizerTestBase(and the tests that extend this base class)\Drupal\Tests\serialization\Unit\Normalizer\ListNormalizerTesttests\Drupal\serialization\Normalizer\ListNormalizerComment #22
wim leersClosed #2756667: Rendered content from REST /GET as a duplicate of this.
Comment #23
moshe weitzman commentedIn most recent patch i noticed a minor typo: parkup
Comment #24
dawehnerI 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.
Comment #25
damiankloip commentedI 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.
This could be passed to the serializer too, functionally there is little point. Thoughts?
"Service provider for text module" ?
text_itemI think is better.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...
Comment #26
damiankloip commentedIn 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 :/
Comment #28
dawehnerHere is a new one. Thank you @damiankloip for a, as always, proper review.
Comment #29
damiankloip commentedAny time, Daniel!
This needs to normalize $object->processed?
Comment #30
dawehnerWell my IDE never failed, but it also didn't stopped in its infinite loop.
Comment #34
dawehnerLet's see whether this fixes the problem
Comment #36
dagmarI've been playing with this patch. It seems the issue with the failing tests is due the line:
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.
Comment #37
dawehnerInteresting!
I'm wondering whether we could provide some list of all normalizers and their priorities to understand the order of them.
Comment #39
dagmarSome other findings. This patch should modify also EntitySerializationTest:
http://cgit.drupalcode.org/drupal/tree/core/modules/serialization/src/Te...
And http://cgit.drupalcode.org/drupal/tree/core/modules/serialization/src/Te...
Which doesn't include the new
processedvalue.Comment #40
dawehnerTo 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 ... ?
Comment #41
frob@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.
Comment #42
dawehner@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.
Comment #44
dagmarJust wondering, could we use the approach from #16 and the test from #36?
Comment #45
dagmarNever 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.
Comment #46
frobSorry @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.
Comment #47
wim leers#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.
Comment #48
frobAh @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.
Comment #49
tedbowFixed EntitySerializationTest as mentioned in #39.
Also fixed namespace in \Drupal\Tests\text\Kernel\Normalizer\TextItemBaseNormalizerTest
and 2 coding standard issues.
Comment #50
tedbowGave it another look over. Just another small coding standard nit.
Comment #51
wim leersWe should document why there is no
denormalize()method, and we should have test coverage ensuring that sendingprocessedas part of aPATCHorPOSTrequest results in an error.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.Why priority 1?
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.Comment #52
tedbow@Wim Leers thanks for review.
1. Added comment.
Remaining task
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.
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.
Comment #54
tedbowChanging priority to 10. This should fix a lot (all?) of the fails.
Comment #55
wim leersHm, "markup" made me frown here. Do we want s/markup/text/?
Great! Perhaps mention that 'processed' is computed?
s/array()/[]/
This should be assigned to
$this->serializerinsetUp().This can be converted to use a
@dataProvider, which will make it easier to add more test cases.This uses 4 spaces of indentation, should be 2.
Let's document why this needs to be 10.
Also, why was 1 okay before, but now it needs to be 10?
Comment #56
tedbow@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:
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.
Comment #57
wim leersRegarding 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.
I think it'd be simpler to hardcode
EntityTest::create(['field_text' => $text_item]), which would then simplify the data providers.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 :)Comment #58
wim leersA 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
EntityResourceresponses 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.
Should typehint to the interface.
Let's not assign these to a variable, because we don't use those variables anyway.
Needs two more leading spaces :)
Comment #59
damiankloip commentedWouldn'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.
Comment #60
wim leersBut 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?
Yes.
Comment #61
tedbow#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.
Comment #63
wim leersHah, nice catch :)
Let's put all of this in a trait.
c/p remnant
Can be simplified to
Now looking into the test failures.
Comment #64
wim leersHere's #61 applied to 8.2.x. We'll need separate patches for 8.2.x and 8.3.x because
\Drupal\serialization\Tests\EntitySerializationTestwas moved in 8.3.x.This should have the same failures as #61.
Comment #66
wim leersRebased on latest 8.2.x. Hopefully it'll apply this time.
Comment #67
wim leersThe problem was that your unit test serialized the same entity object twice. But the first time
$entity->FIELD->processedwas 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.
Comment #68
wim leersThis 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.)
We should not hardcode this inline!
Needs a newline in between.
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?
This should most definitely not be hardcoded, it should be retrieved from configuration. Again, if its' even necessary.
We call these variables
$text_format, not$filter_format.Spacing issues.
I only addressed points 1 & 2, the rest is up to tedbow.
Comment #69
wim leersHaving looked at
\Drupal\text\TextProcessed::getValue()due to #67, I came to realize something sad.It does this:
And
check_markup()does this: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\EditorFileReferencetext filter.We'll need to change
TextItemBaseNormalizerto 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 likecheck_markup(), but without rendering it in isolation (which is whatrenderPlain()does, we need it to render with bubbling! So we needTextItemBaseNormalizer::normalize()to do something like:Comment #73
tedbowre #68
4.
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
Comment #75
wim leerss/default/fallback/
Better :)
Comment #76
martin107 commented#75.1 fixed.
While following along ... I notice a very small thing
we can specify that a method parameter should be an array.
Comment #77
wim leers#76: nice, thanks!
This still needs work for #69.
Comment #79
tedbowFixed 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.
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
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.
Comment #80
dawehnerInstead of swapping the expected and the normalized variable, you could also just append an "s" to
assertEqualand get the same effect :)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.
You could name $object directly $field_item in the method itself, see https://3v4l.org/qN9IV
Comment #81
wim leerss/filter/text format/
Also, broken English.
s/array()/[]/
This is wrong. This is still losing the bubbled cacheability metadata.
You can either:
renderPlain()and then retrieve the cacheability metadata: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.
\Drupal\filter_test\Plugin\Filter\FilterTestCacheTags.$filteris a grossly misleading variable name here. It should be$text_format.$filtercorresponds to\Drupal\filter\Plugin\FilterInterface, which is a very different thing.Comment #82
wim leersComment #83
tedbow@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
renderPlainI 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
Comment #84
tedbowSorry interdiff was wrong.
Comment #85
wim leers"CacheDependency" makes no sense; the term is "CacheableDependency".
I think
ConditionalCacheabilityMetadataBubblingTraitwould 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.This is not bubbling bubbleable metadata (which implies attachments, per
\Drupal\Core\Render\BubbleableMetadata), it's only bubbling cacheability metadata.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.s/text/TextItemBase/
I think an
@see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions()would actually be more appropriate.This does not remotely look like the Full HTML text format. So why call it that? Let's give it a random name.
s/=[/= [/
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.
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.)
This is not random, but it's also not something that's identifiable/established. So this is fine.
Let's be consistent.
Let's also add
filter_test_cache_contexts.This is wrong, it's provided by the
filter_testmodule.Also, you can just remove those keys. We don't have them in
FilterAdminTesteither, for example.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
CacheableMetadataobject with the expectations.Comment #86
tedbow1. 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.
Comment #87
dawehnerPersonally 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.
Should also add the filter.settings config cache key to it or do we not care about that?
Comment #88
damiankloip commentedAlso, 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.
Comment #89
wim leersLooking great! Very close.
Still says "cache dependency".
s/the bubbleable//
s/The/A/
This can simply be removed AFAICT.
Let's rename
$this->configto$this->configFactory.There's 70 cases of that in core, and only one that does it like this code is currently doing it.
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 toTextItemBase $field_item:)@dawehner is right in #87. We can fix that by changing this to:
s/$cacheability/$expected_cacheability/
That creation of a new instance can just be moved into the array.
You can have spaces in these array keys. It's meant to be a human-readable description.
This one can also be moved into the array itself. That'd make this data provider a lot easier to read.
Comment #90
wim leersSee #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
GETresponses 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\MarkupInterfaceto$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 theprocessedproperty ofTextItemBasefields.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 :)
Comment #91
tedbow@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 exceptionThis fixed by making the argument to \Drupal\filter\FilterProcessResult::__construct optional.
I did this.
7. fixed
8. fixed
9. fixed
10. fixed
11. fixed
Comment #92
dawehnerWhat 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?
Comment #93
wim leersAgain: because
$item->processedcontains aMarkupInterfaceobject, 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:
This is not normalizing anything anymore. This is computing the
processedattribute 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.Needs FQCN.
The first sentence makes no sense.
I think all of this can be simplified to
Comment #94
dawehnerSorry, 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.
Comment #95
wim leersYep, totally understandable. Hence I repeated the key elements calmly :)
Yes. That's what
\Drupal\rest\EventSubscriber\ResourceResponseSubscriber::renderResponseBody()does.+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.So given these constraints, do you see a better way to do this, or do you also think this is the only way?
Comment #96
tedbow1. fixed
I did my best for an explanation but let me know what you think
2. fixed
3. fixed
Comment #97
wim leersMuch better! :) Looks great/ready to me. Just a few final nits.
s/processed/'processed'/
s/attribute/property of TextItemBase fields/
Add an
s/field attribute/property/
will not contain cacheability metadata (see \Drupal\text\TextProcessed::getValue()).s/can be used/can carry/
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.
Comment #98
tedbowso close, I am like...

Ok. Here is a patch that addresses all the nits
Comment #99
wim leers:D
As I explained in #97, I'd RTBC, but this also needs the dawehner stamp of approval :)
Comment #100
dawehnerSo what I was thinking we could do instead is the following. Feel free to ignore me, just hacked that together in a minute.
Sorry, I don't want to block anything :(
Comment #101
wim leersSo we "kinda" discussed this via Twitter/GitHub gist. Copying that verbatim here, from https://gist.github.com/dawehner/283fb5ab0dcd0ceefc88df9afc84e0ec.
$item->processedin there, and then call$this->bubble($render_context->bubble());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 aMarkupInterfaceinstance, which doesn't allow cacheability metadata to be retrieved, nor does it allow "execution" of any way.So, can you clarify?
$Item->processedis using the fieldapi magic, sorry, I should have made this clear.If you "call"
$item->processed, its called out\Drupal\text\TextProcessedto retrieve its value:\Drupal\text\TextProcessed::getValue... which calls out tocheck_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?
$item->processedalready having been generated. Then the magic wouldn't happen again, and we wouldn't be able to catch the cacheability metadata.Comment #102
wim leersSo 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 callcheck_markup(), you have no chance at all to get at the cacheability metadata. That's very much intentional! Quoting the docs forcheck_markup():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:
(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\FilterProcessResultwould 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 implementsCacheableDependencyInterface. It currently contains aMarkupInterfaceobject. Changing that would break BC.I see two options:
check_markup()anymore in\Drupal\text\TextProcessed::getValue(), instead we would need to moveTextItemBaseNormalizer::computeProcessedAttribute()toTextProcessed. End result:$item->processed instanceof \Drupal\filter\FilterProcessResult === TRUE.\Drupal\Core\Render\Markupto also implementCacheableDependencyInterfaceand the getters ofAttachmentsInterface(we'd need to split upAttachmentsInterfaceto multiple interfaces). Then theMarkupclass 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:instead of the current patch's:
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 :)
Comment #103
dawehnerAn alternative approach could be to introduce
processed_filteredor so, which has the cacheable metadata attached, instead of bubbled up magically.Comment #104
wim leers@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().Comment #105
dawehnerAlso, 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.
Comment #106
wim leersIt means a duplicate computed property. And adding a property is also a BC break I suspect?
Comment #107
dawehnerI seriously cannot see how an additional computed property is a BC break.
Comment #108
damiankloip commentedThis 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:
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?
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).
Comment #109
wim leers#108
filter.settingsconfiguration when theif (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.)renderPlain()does not bubble the cacheability metadata, but it also doesn't throw it away:$buildcontains 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. ButcomputeProcessedAttribute()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 thefilter.settingsconfig's cacheability metadata. So that's the real thing we should fix here.Comment #111
wim leersAha, 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
TextItemBasefields' 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
processedproperty… because we're adding that here. Again, yay!This reroll fixes the test expectations just for
Commententities. Leaving it to @tedbow to fix the others.Comment #112
wim leers#108
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 usecheck_markup(), and to change it to the logic used in\Drupal\text\Normalizer\TextItemBaseNormalizer::computeProcessedAttribute()instead.Comment #115
tedbowAdded changes to TermResourceTestBase for testing text processing
Comment #117
dawehnerWell, 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.
These are the default values, so we could skip them.
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
Comment #118
dawehnerLet's see:
4 files changed, 41 insertions(+), 87 deletions(-)Comment #120
dawehnerSome temporary fixes ... I had on my local machine
Comment #122
dawehnerThis could fix a good amount of failures.
Comment #124
damiankloip commentedComment #125
damiankloip commentedComment #127
damiankloip commentedFunnily/ironically the failures here would be fixed by #2751325: All serialized values are strings, should be integers/booleans when appropriate I think :D
Comment #128
wim leers#127: heh, tagged it as a blocker at #2751325-143: All serialized values are strings, should be integers/booleans when appropriate.
Comment #129
dawehnerComment #131
wim leers#2751325: All serialized values are strings, should be integers/booleans when appropriate is in! Unblocked.
Comment #132
tedbow#124 doesn't apply. re-rolling
Comment #133
dawehner@tedbow
Did you got distracted :)
Comment #134
almaudoh commentedDrive-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.
Comment #136
tedbowOk 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
Comment #137
wim leersThis 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.
The fail was already present in #122. So it was introduced between #118 and #122, where @dawehner refactored how
TextProcessedworks.I'm fine with continuing this direction.
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.
This is effectively a functional test. Great!
But shouldn't we add an explicit one? Let the
textmodule subclass\Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase, let it add atextfield in itssetUp(). Then assert that the text field is serialized as expected.We've lost the langcode.
We should refactor this away just like we did for #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs.
We should refactor this away, just like we did for #2825812: ImageItem should have an "derivatives" computed property, to expose all image style URLs.
Comment #138
frobYou are correct, the IS was actually wrong due to a typo.
I have rewritten the IS. Hopefully this makes it easier for maintainers.
Comment #139
tedbow@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
Comment #141
tedbowOk 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:
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.
Comment #142
wim leersOne of the other
restorserializationpatches is doing the same thing! (Difficult to find which one, d.o doesn't have "patch search" functionality.)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.
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!
Let's document why we want priority 20.
Let's make this
static, to prove that this doesn't rely on any object (instance) data.:)
Should we add a
@todothat says this is deprecated and will be removed before 9.0.0, because this is what we'll make\Drupal\text\TextProcessedbehave like?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.
Comment #143
tedbow1. 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
Comment #145
tedbowWow, how many tests can I break with just 1 line :(
Comment #146
wim leers#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
processedproperty 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)).
Comment #147
frobDid 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."
Comment #148
tedbowI 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.
Comment #149
wim leers#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.
Comment #150
frobWhat 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.
Comment #151
tedbowOk 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
Comment #152
wim leers#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:
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!
Comment #153
dawehnerI 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.
Comment #154
wim leers+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 :)
Comment #155
dawehnerI'm totally happy to work on it for a bit.
Here is a quick reroll.
Comment #157
dawehnerNow this removes the files which should not have to be touched.
Comment #160
wim leers#2910211: Allow computed exposed properties in ComplexData to support cacheability. was split off from #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts.
Comment #161
tedbowOk starting this back up and updating the patch to work with the latest changes from #2910211-6: Allow computed exposed properties in ComplexData to support cacheability. which in turn is working off #2871591-133: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts
Comment #163
wim leersGreat — it's working for
CommentREST test coverage! But since we last worked on this, #2835845: EntityResource: Provide comprehensive test coverage for BlockContent entity also landed, and that contains aTextItemfield too, and hence it needs to be updated. All test failures are coming from theBlockContentREST test coverage.Comment #164
wim leers#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!
Comment #165
wim leersComment #166
wim leers#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
ComplexDataPropertiesNormalizerTraithunk 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 doingclass ComputedString extends TypedData implements CacheableDependencyInterface { … }. If we'd do the same forTextProcessedResult, 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.
Comment #167
wim leersThis should fix the failures in #161 (and the failures in #166).
Comment #169
wim leersWhile doing #166, I realized something… we've been adding a new computed property
process_resultbecause the existing computed propertyprocessed'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:
TextProcessed, which means we can removeTextProcessedResultTextProcessedimplementCacheableDependencyInterfaceTypedDataNormalizer::normalize()will bubble cacheability metdataComment #170
wim leersOops, forgot to update this in #169.
Comment #171
wim leersThis 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.)
These changes are no longer necessary: we're not adding a normalizer anymore.
And this second class doesn't exist anymore as of #169, so we can revert this change too!
This is sloppy, should be cleaned up.
Comment #175
wim leersDammit, I forgot these too! 😡
They're responsible for 7 of the 10 failures.
Comment #176
wim leersAnd then the 3 remaining failures:
+
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 astring, butcheck_markup()returns aFilteredMarkup.Thankfully we had that test coverage! Plus, it helps prove that this patch indeed is not breaking BC! 👍
Should be green again now.
Comment #177
dagmarThanks for working on this!
Do we need this change?
This two use statements seems not being used.
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
setInternalavailable :)Comment #178
wim leersNit.
No longer necessary.
Can be clarified a bit.
Oops.
These can and should live on the trait.
Comment #179
wim leers#177:
(Because
FilterProcessResult::createFromRenderArray()eventually calls\Drupal\Core\Cache\CacheableMetadata::createFromRenderArray()which doesnew 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 CacheableMetadatameans subclasses may not add new required parameters to methods, whichFilterProcessResultwas violating.)Comment #184
wim leersIn theory,
\Drupal\text\TextProcessed::setValue()should throw\Drupal\Core\TypedData\Exception\ReadOnlyException. Because the docs of\Drupal\Core\TypedData\TypedDataInterface::setValue()say this: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.
Comment #185
wim leersHah, the tiny change in #176 now causes failures in all other tests. So clearly, that didn't work.
TextProcessed::getValue()must return aMarkupInterfaceobject to not break BC.TextProcessed::getValue()should return astringfor 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!
Comment #186
wim leers#185 is ready for review, but I'll still need to work on an IS update. Here's an issue title update already.
Comment #187
tedbow@Wim Leers thanks for getting this moving again. Will look again but for now a couple thoughts
\Drupal\Core\TypedData\ComputedPropertyTraitrelies on$process. Will other computed properties always have this? If so should we move the property to the trait. Maybe then just say it implementsCacheableDependencyInterface? Or will it always in other fields?Just a note since we are doing this in
TextItemBasethen 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
Comment #188
wim leers😳 D'oh, stupidly overlooked that, good catch!
Fixed this, by settling on the default of
$this->valuethat\Drupal\Core\TypedData\TypedDataprescribes.TextItem(Base)compared toStringItem: the processing/filtering of the stored string value. Otherwise, they might as well useStringItem(Base). So if they specifically chose to useTextItemBase, 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.Comment #189
wim leersAfter 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?
Comment #190
tedbow#188.1 That looks good.
2. Ok makes sense.
#189 Yeah waiting on the trait would make sense to me.
Comment #191
wim leers#190: done. Makes the patch smaller, too!
Also another self-review:
Could use a comment.
Stale comment.
Same.
Comment #192
amateescu commentedI'm so glad that you dropped the approach with the new computed property! Here's a short review for now:
Note that there is a
getString()method onTypedDataInterface;)Why is this change needed?
Comment #193
tedbowre #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\TextProcessedis written now anytime you update$textor$formatproperties 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->valueComputedand replaced it with$computedBuildwhich is what we will use for\Drupal\Core\Render\RendererInterface::renderPlainand\Drupal\Core\Render\BubbleableMetadata::createFromRenderArrayin\Drupal\text\TextProcessed::computeValue.So we can just check if
$computedBuildis the same as the last time it was computed. If it is then we don't need to computed again.Comment #194
amateescu commentedThat's a bit better but now I wonder why are all these changes to
\Drupal\text\TextProcessedactually needed. The newgetCacheTags(),getCacheContexts()andgetCacheMaxAge()methods from that class can simply callgetValue(), no?Comment #195
wim leers#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! 🙂
Comment #197
amateescu commentedThat'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 thetarget_idproperty and theentitycomputed property. Which means thatTextItemBasemust be changed to do something similar.Comment #198
wim leers#193's first interdiff hunk:
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.
Comment #199
wim leers#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(). Becausetriggers
FieldItemList::__set(), which triggersFieldItemBase::__set(), which triggersMap::set(), which calls bothFieldItemBase::onChange(), or its override, in this caseTextItemBase::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 toTextProcessed:… 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 atTextProcessed::setValue().(The differences between
set(),setValue(),writePropertyValue()are all beyond me.)Looking on it back now, this makes sense: I renamed
protected $processedtoprotected $valueso we wouldn't need that sillysetValue()override anymore. But now I'm ending up adding it back all the same! And look at HEAD, it didn't need thatprotected $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 renamingprotected $processedtoprotected $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:
By not renaming
protected $processedtoprotected $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 :)
Comment #200
berdirNot 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?
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 :)
Comment #201
amateescu commentedYes, that's what I was hoping to see.
Comment #202
wim leers(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? :)
Comment #203
berdirKind 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..
Comment #204
wim leersDone.
Comment #205
frobShould 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.
Comment #206
berdirTextItem 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? :)
Comment #207
wim leersTrue, it wouldn't solve anything. But really,
TextItemshould be provided byfilter.module, because "text" is really about "processed text", which uses filters.We could do that, but as I just explained, I think that
FilteredMarkupbeing used outside thefiltermodule is artificial, becauseTextItemreally shouldn't be in atextmodule, it should be infiltermodule.Comment #208
berdirI 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.
Comment #209
wim leersYou're confusing me, because I agree with this :P (Maybe I'm more jetlagged than I think I am, and am communicating poorly?)
That is exactly what I think we eventually should do :)
Comment #210
berdirOk, 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.
Comment #211
wim leers#200.2 got us really got on a theoretical tangent about
text+filtermodule. Berdir questioned whether it's okay to useFilteredMarkup::create(). I explained why I think it is. Berdir, how you feel about it now?Comment #212
tedbowThe "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.
Comment #213
wim leersCorrect.
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.
Comment #215
wim leers#213 had nonsensical fails. DrupalCI problems, probably.
Comment #216
tedbowVery interested in what this filter does! 😉
#213 looks good for my test coverage point in #212
Comment #217
wim leersGreat! Pinged @Berdir for a review, since he had thoughts about this before.
Would be great to finally ship this in Drupal 8.5!
Comment #218
wim leersImproving title.
Comment #219
wim leersIS updated.
Comment #220
wim leersComment #221
wim leersComment #222
wim leersCR created: https://www.drupal.org/node/2930012
Comment #223
dagmarI did another pass on the patch. some minor comments.
This is merging cache tags not consistently with the other parts of this patch. (parent::getExpectedCacheTags is always the second parameter).
This method could have a comment.
Should this be documented in the CR?
Comment #224
wim leers@dagmar Thanks for the review!
CommentResourceTestBasedoes the exact same thing. More importantly: the parameter order doesn't matter, becauseCache::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.)No, we took great care to return the exact same value. In HEAD,
$this->processed = check_markup(…).check-markup()returns a\Drupal\Component\Render\MarkupInterfaceobject. This means the doc sayingstringin HEAD is simply wrong.This patch indeed stores a value that doesn't implement
MarkupInterfacein$this->processed. But we also no longer simply return$this->processed:… we still return a
MarkupInterfaceobject —$this->processed's value and type is irrelevant: it's an implementation detail.Comment #225
wim leersPinged Berdir again about a review. I'd love to have his "RTBC +1" on this.
Comment #226
berdir@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).
Comment #228
wim leersFailed due to DrupalCI infra problems. Retesting, back to RTBC.
Comment #229
wim leersIDK why this is marked , this is one of the top 8.5 priorities (#2905563: REST: top priorities for Drupal 8.5.x), so bumping to .
Comment #230
larowlani know this is just test text, but I think it should be higher than seven
Shouldn't we care about the bubble metadata this generates?
i.e. shouldn't we be creating our own render context, calling
::executeInRenderContextand 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#cachekey on$build. And given we generate that above, there are none.?It looks like it's the call to
\Drupal\filter\Element\ProcessedText::preRenderTextthat 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_contextsor 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::toMarkupthat 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?Comment #231
wim leersGreat remarks, @larowlan! :)
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 aRenderContext, 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().This is exactly my opinion too.
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.
Comment #233
wim leersHah, I should've tested more than only
GET, then I would've noticed :)Comment #235
wim leersUgh, #231 reported two failures. But actually, there were three:
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.
Comment #236
larowlanYes, you're correct, I wasn't aware of that.
Was the materializing/reflecting added recently?
Comment #237
larowlanNope, #2543334: Auto-placeholdering for #lazy_builder with bubbling of contexts and tags added that, some time ago. Thanks, always learning stuff.
Comment #238
wim leersThanks, likewise!
Comment #239
larowlanadding review credit for reviews that changed the shape of the patch
Comment #240
larowlanCommitted as 9ba7824 and pushed to 8.5.x.
Published changed record.
Unpostponed issues blocked on this.
Comment #242
berdir@WimLeers/@larowlan: Interested in your thoughts on #2933777: REST/Serializer improvements in core/contrib make it less suitable for default_content use case
Comment #243
wim leersMore 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.
Comment #245
jeqq commentedI've got this issue (a follow-up): #2972988: Error when saving a denormalized entity with text fields with "processed" property when testing with Relaxed.
Comment #246
larowlanBlast 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
And seeing different bubbling metadata to
And so I went looking into the git forensics of this line:
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
Comment #247
larowlanActually, 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.
Comment #248
frobThat 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.