-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Serializer] Normalizer incorrectly caching allowedAttributes when typed properties are used. #36594
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
Comments
Hey, thanks for your report! |
Just a quick reminder to make a comment on this. If I don't hear anything I'll close this. |
I have a similar problem. Even if we don't serialize them at once, but one after another, UninitializedPropertyException is thrown.
|
Hey, thanks for your report! |
I had the same problem few months earlier. Let me check again if the issue is still there. |
This is still an issue and I think it's quite critical... probably this is not so spread just because not much people use PHP 8 yet in production application Preconditions:
Description: So in my use case I have a queue processed by a consumer configured using symfony messenger component A simplified example - each message in queue contains reference (ID) to a Doctrine entity, and MessageHandler uses serializer to serialize a DTO object to JSON, like this:
So what actually happens... but if the second message's entity has empty I hope my description helps someone... UPD: Temporary workaround we are using now is to force workers to process only 1 message per run so that cache will not be used |
Do someone want to submit a PR for this issue? |
@lyrixx the question is - what is the proper fix for this issue.. The only solution I can think of so far - is to just remove usage of if (!$reflProperty->isInitialized($object)) {
unset($attributes[$reflProperty->name]);
continue;
} ... caching attributes per class is wrong since attributes list may differ for different instances of the same class or even for the same object after it's updated. Imagine a use case when you may need to serialize the same object twice - the second time after setting a single property that wasn't initialized on the first call, for example: $object = new Dummy();
$object->foo = 42;
$json = $serializer->serialize($object, 'json');
// send it somewhere..
$object->bar = 'baz';
$json = $serializer->serialize($object, 'json');
// send json with additional `bar` property somewhere else... in this case if attributes are cached on the first call, the second json string will not contain So, here is the patch I'm currently using in my project and it works fine for me... diff --git a/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php b/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php
index fbb8b86..c62d67b 100644
--- a/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php
+++ b/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php
@@ -94,7 +94,6 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
private $propertyTypeExtractor;
private $typesCache = [];
- private $attributesCache = [];
private $objectClassResolver;
@@ -234,25 +233,15 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
}
/**
- * Gets and caches attributes for the given object, format and context.
+ * Gets attributes for the given object, format and context.
*
* @return string[]
*/
protected function getAttributes(object $object, ?string $format, array $context)
{
- $class = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
- $key = $class.'-'.$context['cache_key'];
-
- if (isset($this->attributesCache[$key])) {
- return $this->attributesCache[$key];
- }
-
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
if (false !== $allowedAttributes) {
- if ($context['cache_key']) {
- $this->attributesCache[$key] = $allowedAttributes;
- }
return $allowedAttributes;
}
@@ -263,10 +252,6 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
array_unshift($attributes, $mapping->getTypeProperty());
}
- if ($context['cache_key'] && \stdClass::class !== $class) {
- $this->attributesCache[$key] = $attributes;
- }
-
return $attributes;
}
also added a test to avoid problems in future since it's pretty important in my project class SerializerTest extends KernelTestCase
{
/**
* Test isolated serialization calls (new serializer instance for each assert).
*/
public function testIsolatedCalls(): void
{
self::bootKernel();
$this->assertAllPropertiesSerialized();
self::bootKernel(); // reboot to reset serializer instance
$this->assertUninitializedPropertiesIgnored();
}
/**
* Test that first serialization with all properties being set
* does not break the second serialization with uninitialized property
* since extra attributes should be serialized on the first call.
*
* @depends testIsolatedCalls
*/
public function testTwoConsecutiveCalls(): void
{
self::bootKernel(); // both asserts use same serializer instance
$this->assertAllPropertiesSerialized();
$this->assertUninitializedPropertiesIgnored();
}
/**
* Test that first serialization with uninitialized property
* does not break the second serialization with all properties being set
* since extra attributes should be serialized on the second call.
*
* @depends testIsolatedCalls
*/
public function testTwoReverseConsecutiveCalls(): void
{
self::bootKernel(); // both asserts use same serializer instance
$this->assertUninitializedPropertiesIgnored();
$this->assertAllPropertiesSerialized();
}
private function assertAllPropertiesSerialized(): void
{
$user = new DummyUser();
$user->firstName = 'foo';
$user->middleName = 'bar';
$user->lastName = 'baz';
$this->assertSame(
'{"firstName":"foo","middleName":"bar","lastName":"baz"}',
self::$container->get('serializer')->serialize($user, 'json')
);
}
private function assertUninitializedPropertiesIgnored(): void
{
$user = new DummyUser();
$user->firstName = 'foo';
$user->lastName = 'baz';
$this->assertSame(
'{"firstName":"foo","lastName":"baz"}',
self::$container->get('serializer')->serialize($user, 'json')
);
}
}
class DummyUser
{
public string $firstName;
public string $middleName;
public string $lastName;
} If this solutions looks acceptable, I can create a PR |
I already noticed this issue, and IMHO, it should be fixed. But we must be very aware that some very minor could hurt performance a lot. And, while I did not bench your patch, I think it'll decrease performance a lot. I don't have a better patch ATM, and I'm running out of time for working on this issue :/ (note: I have edited your comment to add some color) |
@lyrixx As another potential solution, instead of ignoring uninitialized properties while collecting attributes in |
I think I remember another issue about this topic. But there is already some support for what your say :
But you idea seems good. Wanna give a try? |
Thanks for sharing the link to I do wanna give it a try, but I don't have enough time at the moment, so I'll keep that in mind and see if I can get to it in the next few weeks.. |
* 5.4: (35 commits) fix: Improve FR validators translation [Notifier] Add push channel to notifier Fix CS [Lock] Split PdoStore into DoctrineDbalStore [Cache] Split PdoAdapter into DoctrineDbalAdapter Add swedish translation for issue #43458 [HttpClient] fix collecting debug info on destruction of CurlResponse Fix CS added missing thai translations Add missing translations for Chinese (zh_TW) [DependencyInjection] fix "url" env var processor update translation [Serializer] #36594 attributes cache breaks normalization Remove untranslated translation for Afrikaans [Validator] Add missing validator polish translation [Security,Validator] Added missing Latvian translations #41053 Add the missing translations for Indonesian (id) [Validator] Add missing Lithuanian translation [Validator] Add missing Czech translation replace "ispravna" with "važeća" in translating "valid HTML/CSS" ...
Symfony version(s) affected: 5.0.5
Description
When using typed properties in a class, the serializer correctly skips those that are not initialized yet. However, when serializing a collection of objects that are instances of the same class, the serializer's attribute caching kicks in, which may lead to "Typed property must not be accessed before initialization" errors, e.g. when property "a" of object 1 is initialized, but on object 2 it is not.
How to reproduce
(where
$serializer
is the default serializer service that is configured by Symfony when enabling the serializer component)Possible Solution
The "allowed" status of typed properties must not be cached and checked every time an object is serialized.
The text was updated successfully, but these errors were encountered: