-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[Messenger] Fix DeduplicateStamp serialization
#62779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 7.4
Are you sure you want to change the base?
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
beb1993 to
a291b54
Compare
DeduplicateStamp serialization
| return $this->isOnlyDeduplicatedInQueue(); | ||
| } | ||
|
|
||
| public function isOnlyDeduplicatedInQueue(): bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should be able to skip adding a new method by adding an annotation on the current one, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests are failing without this method.
If I tried something like #[SerializedName()] on the previous method it's ignored.
Did you have something else in mind ?
The ObjectNormalizer is just looking at method/property without checking attributes
symfony/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
Lines 64 to 128 in e880e53
| protected function extractAttributes(object $object, ?string $format = null, array $context = []): array | |
| { | |
| if (\stdClass::class === $object::class) { | |
| return array_keys((array) $object); | |
| } | |
| // If not using groups, detect manually | |
| $attributes = []; | |
| // methods | |
| $class = ($this->objectClassResolver)($object); | |
| $reflClass = new \ReflectionClass($class); | |
| foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) { | |
| if ( | |
| 0 !== $reflMethod->getNumberOfRequiredParameters() | |
| || $reflMethod->isStatic() | |
| || $reflMethod->isConstructor() | |
| || $reflMethod->isDestructor() | |
| ) { | |
| continue; | |
| } | |
| $name = $reflMethod->name; | |
| $attributeName = null; | |
| // ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel | |
| if (match ($name[0]) { | |
| 'g' => str_starts_with($name, 'get') && isset($name[$i = 3]), | |
| 'h' => str_starts_with($name, 'has') && isset($name[$i = 3]), | |
| 'c' => str_starts_with($name, 'can') && isset($name[$i = 3]), | |
| 'i' => str_starts_with($name, 'is') && isset($name[$i = 2]), | |
| default => false, | |
| } && !ctype_lower($name[$i])) { | |
| if ($this->hasProperty($reflMethod, $name)) { | |
| $attributeName = $name; | |
| } else { | |
| $attributeName = substr($name, $i); | |
| if (!$reflClass->hasProperty($attributeName)) { | |
| $attributeName = lcfirst($attributeName); | |
| } | |
| } | |
| } | |
| if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) { | |
| $attributes[$attributeName] = true; | |
| } | |
| } | |
| // properties | |
| foreach ($reflClass->getProperties() as $reflProperty) { | |
| if (!$reflProperty->isPublic()) { | |
| continue; | |
| } | |
| if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) { | |
| continue; | |
| } | |
| $attributes[$reflProperty->name] = true; | |
| } | |
| return array_keys($attributes); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there no way to split the Serializer concern from the exact API of the stamp? I doesn't feel right to me to add a deprecation to fit some external constraints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand but currently I didn't find one.
If I start parsing Attribute with
new ObjectNormalizer(
classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()),
propertyTypeExtractor: new ReflectionExtractor()
),
I get the following exception:
| throw new MappingException(\sprintf('SerializedName on "%s::%s()" cannot be added. SerializedName can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); |
So in order to serialize stamps, they must respect some constraints.
This is documented here: https://symfonycasts.com/screencast/messenger/serializer-headers-options#shouldnt-we-always-use-the-symfony-serializer
The reason is that the Symfony serializer requires your classes to follow a few rules in order to be serialized and unserialized correctly - like each property needs a setter method or a constructor argument where the name matches the property name. If your class doesn't follow those rules, you can end up with a property that is set on the original object, but suddenly becomes null when it's read from the transport. No fun.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked at the other stamps and all of them are either implementing NonSendableStampInterface or exposing correctly named getter for all the properties.
The DeduplicateStamp is the only one with an exception for onlyDeduplicateInQueue.
The deprecation shouldn't be a big deal since no-one except the Middleware should call the onlyDeduplicateInQueue method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum, won't this concern be fixed after #62772?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-answering: nope, but this still reminds us about the enable_getter_setter_extraction context option. Maybe that's the way to go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how you want to pass enable_getter_setter_extraction ; I tried multiple way without effect.
If you look at the ObjectNormalizer,
symfony/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
Lines 77 to 125 in e880e53
| foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflMethod) { | |
| if ( | |
| 0 !== $reflMethod->getNumberOfRequiredParameters() | |
| || $reflMethod->isStatic() | |
| || $reflMethod->isConstructor() | |
| || $reflMethod->isDestructor() | |
| ) { | |
| continue; | |
| } | |
| $name = $reflMethod->name; | |
| $attributeName = null; | |
| // ctype_lower check to find out if method looks like accessor but actually is not, e.g. hash, cancel | |
| if (match ($name[0]) { | |
| 'g' => str_starts_with($name, 'get') && isset($name[$i = 3]), | |
| 'h' => str_starts_with($name, 'has') && isset($name[$i = 3]), | |
| 'c' => str_starts_with($name, 'can') && isset($name[$i = 3]), | |
| 'i' => str_starts_with($name, 'is') && isset($name[$i = 2]), | |
| default => false, | |
| } && !ctype_lower($name[$i])) { | |
| if ($this->hasProperty($reflMethod, $name)) { | |
| $attributeName = $name; | |
| } else { | |
| $attributeName = substr($name, $i); | |
| if (!$reflClass->hasProperty($attributeName)) { | |
| $attributeName = lcfirst($attributeName); | |
| } | |
| } | |
| } | |
| if (null !== $attributeName && $this->isAllowedAttribute($object, $attributeName, $format, $context)) { | |
| $attributes[$attributeName] = true; | |
| } | |
| } | |
| // properties | |
| foreach ($reflClass->getProperties() as $reflProperty) { | |
| if (!$reflProperty->isPublic()) { | |
| continue; | |
| } | |
| if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name, $format, $context)) { | |
| continue; | |
| } | |
| $attributes[$reflProperty->name] = true; | |
| } |
As soon as I pass a classMetadataFactory with an attribute loader, I also encounter the exception because the method is not is/set/get/has/can
| $accessorOrMutator = preg_match('/^(get|is|has|set|can)(.+)$/i', $method->name, $matches); |
I'm far away to be expert with Serializer, so maybe you have some code suggestion (you can even try my branch, test is executable with ./phpunit src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php --filter=testEncodedWithStampsIsDecodable) but so far I really don't see the way to avoid the method renaming @nicolas-grekas ; is it really a big deal ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. All those extraction rules are wild to me. Some parts accept only prefixed accessors, others are fine with same-name method/properties. I don't know the reasons, this is blurry to me...
What about a normalizer/denormalizer instead? That could help fix both the issues with Key and onlyDeduplicateInQueue, isn't it? For Key, I mean fix the issue on 7.3 without LockKeyNormalizer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All those extraction rules are wild to me. Some parts accept only prefixed accessors, others are fine with same-name method/properties. I don't know the reasons, this is blurry to me...
Honestly, to me too.
What about a normalizer/denormalizer instead? That could help fix both the issues with Key and onlyDeduplicateInQueue, isn't it? For Key, I mean fix the issue on 7.3 without LockKeyNormalizer.
While I understand the idea (and the why), isn't it kinda too-specific/overcomplicated:
- The DeduplicateStamp will be the only one with a Normalizer, while all the over stamps are working great alone.
- People instantiating manually "custom" serializer (like it's done here: ) will need to add the Normalizer themself
$normalizers = [new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()]; - We will need to auto-register this normalizer and add it too the create method
$normalizers = [new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()]; - I feel like the DeduplicateInQueue will re-implement the logic from the LockKeyNormalizer
I feel like we're searching for a complex solution to avoir renaming a method ; issue I would have avoid if I have thought about the Serializer when implementing the DeduplicateStamp.
But if it's ok to create a new Normalizer in 7.3, it would be better to backport the LockKeyNormalizer in 7.3 then
7.4 for bugfix since it requires the LockKeyNormalizer