Problem/Motivation
When I try to save an entity after denormalization I get this error: Call to a member function getProcessedText() on string in core/modules/text/src/TextProcessed.php line 43. This happens because denormalized entity has the ''processed" property as a string eg. <p>Lorem Ipsum.</p>, not as FilterProcessResult object.
The issue seems to be a followup for #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata.
I've found this issue while working on making Relaxed module compatible with Drupal 8.5.x and this is the last blocker.
Steps to reproduce
save an entity after denormalization
Proposed resolution
In core/lib/Drupal/Core/TypedData/Plugin/DataType/Map.php set a condition for both readonly and commutted
Throw a new exception in core/lib/Drupal/Core/TypedData/TypedData.php when both are
Set text plugins to readonly
Remaining tasks
Update summary
User interface changes
NA
Introduced terminology
NA
API changes
NA
Data model changes
NA
Release notes snippet
NA
| Comment | File | Size | Author |
|---|
Issue fork drupal-2972988
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
timmillwoodI would RTBC but I feel we need someone from the parent issue to take a look at this so we understand the previous change a bit better, I've pinged @WimLeers on IRC.
Comment #4
wim leersThanks for reporting this! And thank you even more for providing a failing test!!! 🎉❤️👍
Digging in.
Comment #5
wim leersRather than the expected/hoped 30-90 minutes, this took more like 6 hours to unravel. 😱
I initially thought this was related to #2957385: FieldItemNormalizer never calls @DataType-level normalizer service' ::denormalize() method. But given the patch (with failing test + fix), I started doubting that.
So I dug in.
The interesting thing is that … it does not make sense to send
processed: it's a read-only computed field! However, we totally support the case of users doing this:[title][value]to e.g.foobarBecause
bodyis unchanged, thanks to #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior, we'll just ignorebody, and not even bother denormalizing it, because it's unchanged! And we do have automated test coverage that iterates over all fields in a given entity type's test entity and verifies it can be included in aPATCHrequest.Of course, the question is what happens when you modify the
valueproperty of a text field. At that point, the field is being changed. And yes,body'sprocessedis only being exposed since #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata, which meant that only since then aPATCHrequest would potentially include aprocessedproperty.But again:
And that's where #2626924: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata made a mistake: it started normalizing this computed field … but did not ensure it would be ignored during denormalization! We don't have test coverage for this. All existing tests that are
PATCHing a text field are doing so explicitly with only thevalueandformatproperties. Because those already existed.Ironically, I even noticed this gap in the test coverage myself 1.5 years ago in #2626924-51: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata:
Also see #2626924-184: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata, in response to a review, about this very subject, and for which I opened #2921890: Computed properties' classes' ::setValue() method should throw ReadOnlyException.
@amateescu closed #2921890 at #2921890-10: Computed properties' classes' ::setValue() method should throw ReadOnlyException. But I think that's because I had gotten the scope/intent wrong: I wanted to make all computed fields read-only. But I should've made it about ensuring read-only computed
What's tricky about this is that there's no way to reproduce this problem except for doing
normalize(denormalize(decode($serialized_entity)). i.e. only if you normalize a denormalized entity, rather than a loaded entity, you can reproduce this. Which is exactly what your test does.The easy solution would be to add a
TextItemNormalizerwhosedenormalize()method would ignore theprocessedproperty that it receives. But that's not the correct solution. That's putting the logic in core's Serialization system, but which means it'll only work for core'srest.moduleand the https://www.drupal.org/project/relaxed module you maintain. It won't work for https://www.drupal.org/project/jsonapi or https://www.drupal.org/project/graphql. It'll be bad for the API-First ecosystem of modules — as mentioned in https://wimleers.com/blog/api-first-drupal-two-big-maintainability-miles... for example.The correct solution is to fix this in the Field/Typed Data API. Then it'll work everywhere. That's why I wanted to do something like this:
…but unfortunately that is not possible, because
setValue()is also used byEntity::create(), even if you just specify thevalueandformatproperties for a text field. So somewhere along the way, aprocessedproperty value is being generated to be set. So we need to dig even deeper in Entity/Field API, to avoid doing this for read-only computed properties!And that's how I arrived in
\Drupal\Core\TypedData\Plugin\DataType\Map::setValue(), where this happens:Despite
$values === ['value' => 'something', 'format' => 'something'], this code insists on updating existing properties, even if it doesn't make sense to set a value on a read-only computed property! (The point being that it is … computed.) AND despite those last 3 lines wrt computed properties, a fix that landed in December 2014, in #2137309: Typed data does not handle set() and onChange() consistently. Obviously we haven't yet had the opportunity to pay attention to read-only computed fields (that's what this very issue is revealing), so I think it's fair to say that that code needs a small update: it should not update read-only computed properties.By fixing it there, we don't even need to modify
FieldItemNormalizer::denormalize(), which #2921890-2: Computed properties' classes' ::setValue() method should throw ReadOnlyException did need to change!Whew, that was a super long comment, but hopefully others (and especially the Entity/Field/Typed Data API maintainers who'll need to review this) find it a helpful guide at hopefully arriving at the same conclusions.
Next up: patches!
Comment #6
wim leersFirst, ensure that we throw an explicit failure when trying to set a value for a read-only computed property.
processedproperty read-only (it's already marked as computed). This should already have been the case; we're just updating the metadata to match reality.TypedData::setValue()throw an exception when setting a value for a read-only computed property.TextProcessed::setValue(), because it's an override ofTypedData::setValue(), and is no longer necessary (we need to either remove it, or add the same lines to it)Then, to add test coverage:
CommentREST test send aprocessedproperty inPATCHandPOSTrequests, so that we're actually testing this.This patch should FAIL.
(We should bring back the failing test that @jeqq wrote later. Right now I want this to be as simple and focused as possible, to make it easier for Entity/Field/Typed Data API maintainers to review.)
Comment #7
wim leers#6 should fail with errors like this:
The solution is to implement the change to
Map::setValue()that I described near the end of #5.Comment #8
timmillwoodIsn't this a BC break? What if there's a contrib or custom module which is using "processed" as non-read-only field?
Comment #9
wim leers#7 should fail with errors like this:
because the test coverage that #2882717: EntityResourceTestBase: assert that PATCHed and POSTed entities contain the intended values introduced made assumptions that no longer hold true; since now request bodies may include read-only computed properties.
processedisn't stored, so we shouldn't be asserting that it is stored to verify that the data in our request bodies is actually stored: we should omit read-only computed properties from that expectation!So, we update those assertions to take this into account and … now the patch should be green! ✅
Comment #10
wim leers#8:
That's impossible. You can't write to it today either. You could theoretically set a value and have it exist for the remainder of the life of that entity object, but that's it. It wouldn't be saved. It couldn't be saved. And it would be SUPER INSECURE to do so anyway — because that's what the
filter.module's filter plugins are for!I'm glad you made that necessary scrutinary remark — somebody had to make it. It's something we need to discuss and question. But for this particular property, this is not a BC break IMO. It may be for some others.
If this issue gets blocked on that BC concern, I'll eat my hat. 🎩 🤠
Comment #11
timmillwoodRe: #10: ok, sounds reasonable, just wanted to check! 😉
Comment #15
wim leers#11: 👍
The failures are due to
\Drupal\text\Plugin\Field\FieldType\TextItemBase::onChange():i.e. that write.
That write's intent is to "reset" the computed value of the
processedproperty, because the data it's based upon has been modified.But why do we need to write that; it's computed, so why do we need to write anything? Because those computed values are currently cached (#2083785: Add support for determining which field properties are cacheable added that): they're stored in-memory on the entity object, so essentially they're statically cached. Hence any writes to their dependencies need to invalidate the computed value.
IOW: it's the static cache that gets in the way.
So we need to figure out a way to support this from a Field/Typed Data API POV.
For now, just disabling
TextItemBase::onChange(). Let's see what happens. What is relying on this?Comment #16
wim leersComment #18
tedbow@Wim Leers great investigating as usual!
Super Nit, @param type needed
The fix looks good.
Just saw the
\Drupal\text\Plugin\Field\FieldType\TextItemBase::onChangewrite error you found 😉Comment #19
Patrick Bauer commented@Wim Leers
Is it save to use your patch while theres still some failing tests?
Comment #20
lee.cocklin commented@Wim Leers
I would also like to know if patch #15 would be safe to use on a production site. The error is popping up when using the REST API to create content. It still seems to create the content, but the error reported in this issue occurs on the target site. If I apply patch #15 the issue goes away and I get a 201 response code from the target site.
Comment #21
Patrick Bauer commented@Lee You can use this patch https://www.drupal.org/files/issues/2018-05-15/text-processed-fix.patch from the creator of the issue. It worked well.
Comment #22
lee.cocklin commented@Patrick Thanks I had missed that one. It works perfectly for me.
Comment #23
wim leersNeither patch has been thoroughly reviewed and committed, so I wouldn't recommend either at this time.
Let's get this patch to green!
Comment #24
wim leersComment #26
wim leers14 to 2 failures, much better :)
The remaining failures are caused by this change. See #15 for detailed explanation. Static caching is getting in the way.
That's the last problem to solve; I'm leaving that for an Entity/Field/Typed Data maintainer.
Comment #27
wim leersNow also reported by a JSON API user: #2984466: [PP-1] Sending a text field's "processed" property breaks POST or PATCH requests.
Comment #28
dawehnerI'm wondering whether it would be worth documenting these lines with a rational.
It feels odd to test this just on comments. Maybe we could add a text field to entity_test entities?
Nitpick: Any reason to not use a filter + map here?
Couldn't we check for readOnly here too?
Comment #29
wim leersthere's one
@todoyet to be figured out.array_map()can't access the keys :(Comment #30
wim leersIn addition to #29.4 pointing to #15:
Uncomment this, then run
\Drupal\Tests\text\Kernel\TextWithSummaryItemTest::testCrudAndUpdate(). That makes it easy to reproduce the problem.Comment #33
wim leersAdded to #3002188: REST: top priorities for Drupal 8.7.x.
Comment #34
anvmn commentedHey,
I see that this issue was added to be fixed at 8.7.x, but I don't think it was, at recent release of 8.7.
Nor I see an issue for this on 8.8.x.
Anyone can update where this stands?
Thanks,
Comment #35
frobIt looks like the bot that automatically changes the target version didn't fire off.
Comment #36
matthieuscarset commentedI confirm this issue still exists in Drupal 8.7.3
Comment #37
john_a commentedI just came across this same issue with Drupal 8.7.6, while trying to use the default_content module.
Comment #38
frobComment #40
hfm commented- deleted -
Comment #42
aiphesHello,
Facing this issue on a fresh D8.9.14 migrated from D6.
Error: Call to a member function getProcessedText() on string in Drupal\text\TextProcessed->getValue() (line 43 of core/modules/text/src/TextProcessed.php).If someone found a way to fix it.
Thanks
Comment #43
parisekComment #46
mxh commentedError still persists in D9.4. As this is rather old and still not fixed, gues I'm doing something wrong when working with entity serialization in general. Is the serialization API not meant to be built for saving denormalized entities?
Comment #53
smustgrave commentedTriaging my component queue by tagging #29 and trying to continue on.
Comment #54
smustgrave commentedComment #55
smustgrave commentedComment #57
smustgrave commentedTest only shows the same error as the summary https://git.drupalcode.org/issue/drupal-2972988/-/jobs/9414259
99% of the fix was already implemented in the previous patches but I ended up reverting some of it and doing an unset in Map vs fixing the spots where tests failed.
Disclosure Claude was use to help with the test.
Comment #58
grimreaperMR looks good to me, but I have not manually reproduced the bug to ensure the fix.
Another pair of eyes before RTBC would be nice.
Comment #59
dcam commentedWe're modifying a core library, but aren't adding explicit test coverage for this change to the library's tests. Coverage has only been added to a downstream module. @dawehner effectively had this same concern in #28 and @wim leers agreed to it in #29 pending approval of the approach. But that coverage never got added. The suggestion was made to add a text field to entity_test, which from what I can tell is exactly the test in the MR does. So maybe all that needs to happen is moving the test.