Problem/Motivation
Like commerce's order,It use 'any' type data to store object:Adjustment, But the TypedDataNormalizer just return the Adjustment object, It cause it can't be encode, So it can't be serializad
First(laravel): Let's look at laravel's serialization
laravel's serialization has two steps:
- toArray() ====> corresponds to druapl's normalize
- toJson() ====> corresponds to drupal's encode
laravel's toArray() is recursive,it will recurs to check whether the child value is Arrayable
https://github.com/illuminate/database/blob/5.5/Eloquent/Model.php#L918
https://github.com/illuminate/database/blob/5.5/Eloquent/Concerns/HasAtt...
Second(symfony):
https://github.com/symfony/serializer/blob/3.2/Serializer.php#L138
public function normalize($data, $format = null, array $context = array())
{
// If a normalizer supports the given data, use it
if ($normalizer = $this->getNormalizer($data, $format)) {
return $normalizer->normalize($data, $format, $context);
}
if (null === $data || is_scalar($data)) {
return $data;
}
if (is_array($data) || $data instanceof \Traversable) {
$normalized = array();
foreach ($data as $key => $val) {
$normalized[$key] = $this->normalize($val, $format, $context);
}
return $normalized;
}
if (is_object($data)) {
if (!$this->normalizers) {
throw new LogicException('You must register at least one normalizer to be able to normalize objects.');
}
throw new UnexpectedValueException(sprintf('Could not normalize object of type %s, no supporting normalizer found.', get_class($data)));
}
throw new UnexpectedValueException(sprintf('An unexpected value could not be normalized: %s', var_export($data, true)));
}symfony's Serializer->normalize() has the ability to recursive normalize object and array too
Both laravel and symfony has the ability to recursive normalize object or array
Then(Drupal):
Let's see TypedDataNormalizer used by normalizing 'any' type data
core/modules/serialization/src/Normalizer/TypedDataNormalizer.php#n21
public function normalize($object, $format = NULL, array $context = []) {
return $object->getValue();
}We can see TypedDataNormalizer Just return the value
Drupal use symfony's serializer, But it dosn't follow or take full advantage of the symfony's serializer's design
Proposed resolution
- make TypedDataNormalizer flow and use symfony's serialize->normalize()'s design. (solve here)
- Map type data has similar trouble too: But Map have other more base problem, It can't be normalized when it has no propertydefinition , But if TypedDataNormalizer's problem is solved, Two kind of https://www.drupal.org/node/2895532's the patch(two kind of controversial solution) will work good with no change needed
Remaining tasks
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #90 | 2915705-90.patch | 20.82 KB | yevko |
| #83 | 2915705-82.patch | 20.78 KB | jurgenhaas |
| #66 | 2915705-66.patch | 21.57 KB | sch4lly |
| #59 | interdiff-2915705-59-55.txt | 2.17 KB | dravenk |
| #59 | 2915705-59.patch | 21.18 KB | dravenk |
Issue fork drupal-2915705
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 #2
lawxen commentedThe any use TypedDataNormalizer to normalize data
If the getValue return object
add checking:wherher the object could be further normalized
Comment #3
lawxen commentedsymfony's Serializer's normalzer()'s design dont's will recursive normalize object and array, and don't allow object which can't be normalizable(didn't implement TypedDataInterface or it's child interface) to pass
So the patch Follow and take full advantage of the symfony's serializer's normalzer()'s design
Then when data which is unnormalizable object or has unnormalizable daughter object stores to a typed data,It must be normalizable
So,I then create a commerce issue about commerce order's adjustment as a demo:https://www.drupal.org/node/2916252
Comment #4
lawxen commentedComment #5
lawxen commentedComment #6
wim leersComment #7
wim leersThis looks sensible to me. But this definitely needs (unit) test coverage!
Comment #8
wim leersAlso, this looks extremely similar to the code in #2871591: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts that @tedbow reverted (in #2871591-142: Allow ComplexData in TypedData to specify computed properties that should be exposed in normalization and other contexts) because it was no longer necessary in that particular issue.
Different, but in the exact same class, doing something similar — I suspect @tedbow is the best candidate to review this in detail :)
Comment #9
tedbowThis does reasonable to me but probably not nothing in core is passing that if statement right now so as @Wim Leers says will needs tests.
There should be a "test-only" patch to prove that an array or object won't normalize correctly now.
Comment #10
lawxen commentedto
Comment #11
tedbow@caseylau thanks for providing comprehensive text coverage.
I think this function name is confusing. Too close to "build database".
Maybe
buildDataBasicString()Nit: comment to long. First sentence should less than 80.
To clear in this and all the function comments we should surround "any" with single quotes. So 'any'.
Maybe I missing something but why is
$property3declared dynamically compared to the other 2 properties.If we didn't need to do this we could remove the constructor.
Comment #12
lawxen commented@tedbow Thanks for reviewing the code and so many improvable advice.
Comment #13
skyredwangDon't split one English sentence into two (broken ones). Just make each line only about 80 characters long.
Comment #14
lawxen commentedMake one English sentence into one line
Comment #15
tstoecklerSo in addition to (or instead of) documenting what $value should be, shouldn't we be checking explicitly with instanceof?
Comment #16
lawxen commentedchange comment
instanceof TraversabletotraversableThe serializer.php->normalize() has the ability to check whether the object is traversable,
If the object is not traversable or typed data, It will give an error reminder to make module make corresponding modification.
If we add check "instanceof \Traversable" in TypedDataNormalizer.php, it will avoid the error reminder,
But it can't check one situation: when the object which isn't traversable appear in an array.
So check "instanceof \Traversable" in TypedDataNormalizer.php is inapposite.
Comment #17
skyredwangComment #18
wim leersThanks again for pushing this forward! 👍 This is important, and tricky, so thank you for your patience! 🙏
We're modifying the generic
TypedDataNormalizer, which has:We should instead be adding a
AnyNormalizer, which hasThen we don't need to add those very generic checks to
TypedDataNormalizer, and we hugely minimize risk (of applying this logic to other typed data types inappropriately).Comment #19
lawxen commented@Wim Leers Thanks for the great advice. I make some change correspond #18
Comment #20
wim leersNit: Let's use
Any::classinstead of this string.(Sorry, I should've said that in my previous review.)
Tip 1 says it must be either typed data or a traversable object.
But the if-test only tests
is_object(). Shouldn't that then testif ($value instanceof Traversable || $value instanceof TypedDataInterface)?Also, why can't we do only
$value instanceof Traversable?I don't understand this "must convert the array value to object in setValue() to make it can be denormalized" at all. Can you point to an example of that?
What's a real world use case for this?
You added this in #10. Is it really necessary?
Should live in
namespace Drupal\Tests\serialization\Unit\Normalizer.But this is a kernel test (ideally it'd be a unit test), so
namespace Drupal\Tests\serialization\Kernelwould be the place.That's not the class it covers.
Should be renamed to
AnyNormalizerTest.We should also test with a basic object, which is *not*
instanceof Traversable.In other words: I think that we can simplify this patch a bit: if we make it only deal with
$value instanceof Traversable || is_array($value), then this normalizer becomes much simpler, therefore less dangerous and much more logical.Comment #21
wim leersI've done the trivial stuff for you: this fixes points 1, 4, 5 and 6.
Points 2, 3 and 7 can all be fixed by doing
Comment #22
wim leersAs of #2916252-52: [PP-1] Order's Adjustment can't be normalized and serialized, this blocks #2916252, which is a contrib issue.
Comment #23
lawxen commented@Wim Leers Thanks for your patient guidance.
Change:
Reply:
AnyNomarlizer use Symfony\Component\Serializer
The Symfony\Component\Serializer->normalize()
will help to check if the value has corresponding normalizer, traversable or is an array.
What AnyNormalzer should be down for object is to
Continue normalizing object which can be normalized
Of cause an non-normalizable object will come to an UnexpectedValueException.
If we want to make it less dangerous. some possible resolution:
This will add a repeated loop which has been checked in Symfony\Component\Serializer->normalize, low performance, deprecated.
($value instanceof Traversable || $value instanceof TypedDataInterface || $value instanceof EntityInterface)These can't include all normalizable object, and these will prevent extension for some custom normalizer which want to nomralize special value, deprecated.
After considering performance, full functional, expansibility, code quality, and risk
I suggest don't change this part
isset($value) && (is_object($value) || is_array($value))I change these comment to
And the real use is using RestApi or jsonapi to post a commerce order.
AdjustmentIterm.php
Comemrce order's commerce_adjustment field define an 'any' typed data with Adjustment object stored.
When use jsonapi to post data to commerce order, If add the transaction code in setValue(),
https://www.drupal.org/files/issues/2916252-51.patch
the posted array value will be denormalized to Adjustment object, then be denormalized to commerce_adjustment field.
without writing custom code in drupal.
(Android app or Frontend can't construct an drupal object.)
Comment #24
lawxen commentedComment #26
lawxen commentedAdd drupal8.4 version patch to make the update risk fall to minimum with the corresponding module's update like #2916252
$this->addCacheableDependency($context, $object);Comment #27
lawxen commentedComment #28
wim leersWow, great comment!
And thank you for your patience in waiting for feedback, I'm sorry for not reviewing your patch faster! Your persistence and attention to detail is impressive :) 👍
\Symfony\Component\Serializer\Serializer::normalize()already does theif (is_array($data) || $data instanceof \Traversable) {check.You said
When does
AnycontainTypedDataobjects? If it containsTypedDataobjects, it seems like it was wrong to useAnyin the first place?In other words: can you point to a an existing/real example of this?
Adjustmentdoes not implementedTypedDataInterfacenorEntityInterface. The patch I proposed at #2916252-53: [PP-1] Order's Adjustment can't be normalized and serialized makes it implement\Traversable.Does that mean we don't need this patch at all? I queued #2916252-54: [PP-1] Order's Adjustment can't be normalized and serialized for testing to find out. If that passes, we don't need this patch.
Comment #29
lawxen commented@Wim Leers
So I change the comment to make it more appropriate
Must combine this patch to make Adjustment can be output successful in jsonapi(jsonapi/commerce_order/default) or restapi.
so whether the test pass has nothing to do with whether we need this patch
Comment #30
wim leersI apologize for having lost track of this issue :( Thanks to @skyredwang for pinging me about it via https://twitter.com/skyredwang/status/948380889980915713!
The most important thing that is still missing here is integration test coverage. The
\Drupal\field_test\Plugin\Field\FieldType\TestObjectItemfield type contains a non-computed property using theAny@DataType. So I propose that you write a\Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestAnyTestthat adds a field of that type to theEntityTestentity type, much like\Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDateonlyTestor\Drupal\Tests\datetime\Functional\EntityResource\EntityTest\EntityTestDatetimeTestalready do. Then you can prove the correct behavior.OTOH, that won't do something like [#12358670-54], so it's probably better to write another field type or entity type that does something more like Drupal Commerce's
Adjustment.Comment #31
wim leersAlso: I don't quite trust the current patch (neither the implementation nor the test coverage), because I can quite easily make it pass while making its normalizer a no-op.
Apply this change to #29, and run tests, and you'll see that it still passes…
Comment #32
lawxen commented@Wim Leers Thanks for the advice
#31
AnyNomalizer has the ability to normalize object, but not just object, so It meets expectation.
These code will make
be an endless loop, So it can't pass this test.
#30
Good advice, I will do it later.
Comment #33
wim leersIf the change I show in #31 still passes tests, it means we have no tests that prove the need for this. That's why I said
Looks like you're going to write that test, great :) Once you've written that test, a change like the one I made in #31 should fail!
Comment #34
lawxen commented@Wim Leers
The change in #31
can't make test: \Drupal\Tests\serialization\Kernel\AnyNormalizerTest pass.
Comment #36
lawxen commentedConstruct a field and then Add rest test for AnyNormalize, @Wim Leers thanks for the guidance on #30,
But it still has a failing test of post waiting to be solved:
Comment #37
lawxen commentedFix post and patch failing test of #36:
Comment #39
lawxen commentedOh, a failing test happen, this patch fix it.
Comment #40
lawxen commentedI don't think the way i handle the denormalization is appropriate
Comment #42
berdirWondering how the new approach in #2895532: @DataType=map cannot be normalized, affects @FieldType=link, @FieldType=map will affect this, might just work for this as well...
Comment #43
wim leersRebased now that #2895532: @DataType=map cannot be normalized, affects @FieldType=link, @FieldType=map landed.
Indeed! So I tried. Unfortunately it's still necessary. At least the changes that this patch was making to
EntityResourceTestBaseare no longer necessary, #2895532: @DataType=map cannot be normalized, affects @FieldType=link, @FieldType=map improved that in a superior way 🙏Comment #45
lawxen commentedMove to Drupal\Tests\entity_test and use Drupal\Tests\entity_test\Functional\Rest\EntityTestResourceTestBase directly.
Comment #47
lawxen commentedoh, this change make so many tests fail, delete it first
Handle the failure test later:
Comment #48
lawxen commentedComment #49
lawxen commentedIN #45 I missed
->getValue()inSo it caused so many failure test.
Comment #50
lawxen commentedComment #51
lawxen commented2915705-43.patch --> 2915705-47.patch
Add change log:
Add this change to make the test 2915705-43.patch green.
Above three tests was copied from 'Map' and made some minimized change.
Comment #52
lawxen commentedWhen I write resource test for commerce's adjustment field.
I find EntityResourceTestBase didn't handle boolean value properly
Boolean value just be cast to string on "expected", not "actual" on the above two places.
This patch fix it and add corresponding tests.
Comment #54
wim leersThanks for pushing this forward again! Let's get this done this time, now that #2895532: @DataType=map cannot be normalized, affects @FieldType=link, @FieldType=map is finally committed.
Unfortunately, #52 failed, which seems to be the opposite of what you expected?
Comment #55
lawxen commented@Wim Leers
This will fix the failure test of #52 😊
Comment #56
lawxen commented😍 test green @Wim Leers
Comment #57
lawxen commented[Typed data layout_section can't be normalized if it stores object](https://www.drupal.org/project/drupal/issues/3012165#comment-12847218)
Typed data layout_section has the same problem with this issue, @Wim Leers
what about merging the two issue with one and do sth in TypedDataNormalizer directly.Two methods:
Comment #59
dravenkJust re-roll patch #55
Comment #62
yonailoWhat are we waiting for in this issue patch to be it commited ? I see the latest patch fails in two tests related to "EntityTestDatetimeTest", but it seems not related to this patch, I don't understand at all the tests but can someone look at this ?
I have found this issue when using content_sync module + drupal commerce, and a fix for drupal commerce (#2916252) is waiting for this to be commited... :(
Comment #66
sch4lly commentedRe-rolled to 9.3.0
Comment #67
spokjeComment #68
spokjeUsed
2915705-59.patchfor an MR with a reroll on9.3.xComment #70
spokjeComment #71
spokjeComment #72
spokjeComment #73
spokjeComment #74
bbralaBookmarking this for myself
Comment #75
spokjeJust a note: I believe all the blocker tags are outdated.
I came here to get Drupal Commerce order adjustment working with the Core RESTful Web Services.
Turns out enabling Core JSON:API and adding
commerce_apito my project, this works out of the box, even when just using REST and nothing of the JSON:API "shizzle".That also probably explains the lack of activity here.
Comment #76
bbralaHmm, I wonder... Does jsonapi fix it or the commerce_api? I know there is quite a few overwritten stuff in the commerce_api.
Comment #77
skyredwangCommerce API has its own solution. This ticket is for the core issue, which will not only affect JSON:API, but also default_content. (Can't remember too much detail.)
Comment #78
berdirdefault_content 2.x isn't using the core serialization API anymore, so that's not affected by this either I think.
Comment #83
jurgenhaasFixed a spacing problem in the MR so that the patch file applies to D10.
Comment #84
spokjeThanks @jurgenhaas, rebased MR on 10.1.x.
Comment #90
yevko commentedPatch that doesn't invoke an error on 11.2.3.
Declaration of Drupal\serialization\Normalizer\AnyNormalizer::normalize($object, $format = null, array $context = []) must be compatible with Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize(mixed $data, ?string $format = null, array $context = []): ArrayObject|array|string|int|float|bool|null in /app/public/core/modules/serialization/src/Normalizer/AnyNormalizer.php on line 20