-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Serializer] Instantiator - Add an interface and default implementation to instantiate objects #30956
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
Conversation
d115b49
to
c6168b5
Compare
Probably out of scope and irrelevant but ... we recently added the VarExporter component which already includes an Instantiator. Are we duplicating efforts here? Thanks. |
@javiereguiluz Like i said on top, i was aware of that, but unfortunetaly we cannot use it as a default implementation since the var export implementation try to skip the constructor where we want to use it here, as it's the current behaviour of existing normalizers. Like If someone was using constructor to set default values for its class, using the var exporter would break its behaviour. One thing that would be possible however is to create a Denormalizer that use the Var Exporter Component, but it's out of scope for this PR. |
} elseif ($constructorParameter->isDefaultValueAvailable()) { | ||
$params[] = $constructorParameter->getDefaultValue(); | ||
} else { | ||
throw new MissingConstructorArgumentsException(sprintf('Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.', $class, $constructorParameter->name)); |
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.
In case of missing constructor arguments, you probably should fall back on using the property access component to arbitrarily set properties onto the object. Problem is that your code change with time, but serialized entities stored somewhere don't.
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.
Just forget the previous comment, sorry for the noise. But it raise an interesting question: the instanciator sets some values using the constructor, then the API using it (for example, the normalizer because it's today the only API using it I know of) will set some others, it seems like each component responsibility is unclear, they both walk on each other foot.
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.
Instantiator will only set values related to constructor, if there is other data to set, it will be done after by the ObjectNormalizer
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.
How will the normalizer now which properties have already been set or not if the instanciator becomes an opaque interface ? It seems that it will then be forced to re-inject all properties ? Maybe not since you pass an array reference, you can remove constructor-set values from the array, but it's a very weird interface then, you have two different outputs: the object, and the modified array. It's not cognitively-easy. I know do understand your answer below thought.
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.
Related to #30956 (comment)
Actually we don't have a simpler way to pass both object and new data array, so we use the reference to handle the data. It's probably not the best way to do it but it can be improved.
I think we should more focus on code duplication in Serializer before focusing on attended behavior even if they do use references.
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.
Yes I finally understood why you're doing it this way now. This is weird because the instanciator and the normalizer then share some bits of internal logic then, whereas having an external interface main benefit is "divide and conquer", making every component simpler and avoid cognitive surcharge when reading bare algorithm (also make it simpler to maintain). I agree this is not an easy one, thanks for taking the time to answer.
@joelwurtz do you think you'll be able to finish this PR anytime soon? |
f933bc3
to
720d6f2
Compare
src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Serializer/Tests/Instantiator/InstantiatorTest.php
Outdated
Show resolved
Hide resolved
0c08336
to
d327945
Compare
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.
Thank you for this PR.
I've got some comments and questions.
return $this->error; | ||
} | ||
|
||
public function hasFailed(): 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.
Is this method really needed?
Is it failed when it doesn't have an object or when it does not have an error?
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.
It is failed when the $object
property is null
. It's used in https://github.com/symfony/symfony/pull/38487/files#diff-cd90b97b50abf20fa94468689e7486afR315-R317 to know when we need to throw an Exception or not. We could make the condition directly but I think it's better DX to make it a class method.
return $this->context; | ||
} | ||
|
||
public function getError(): ?string |
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.
Shouldn't this return a Throwable instead?
@@ -306,6 +312,7 @@ protected function prepareForDenormalization($data) | |||
} | |||
|
|||
/** | |||
<<<<<<< HEAD |
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.
Oops =)
|
||
protected function getConstructor(\ReflectionClass $reflectionClass): ?\ReflectionMethod | ||
{ | ||
return $reflectionClass->getConstructor(); |
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.
Couldn't the context hold information what other method to call?
That means that we can also declare this class as final
and we dont need to use inheritance like this.
@@ -118,6 +125,26 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory | |||
} | |||
$this->classDiscriminatorResolver = $classDiscriminatorResolver; | |||
$this->objectClassResolver = $objectClassResolver; | |||
|
|||
$this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory(); |
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.
Why not pass the $childContextFactory
to parent::__construct()
?
*/ | ||
class StaticConstructorNormalizer extends ObjectNormalizer | ||
class StaticConstructorInstantiator extends Instantiator |
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.
Let's try to find a way to to support static constructors without extending Instantiator
We've just moved away from |
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
…on to instantiate objects symfony#30956
Related to #30818, Replace #30925
This add a new interface and default implementation to instantiate objects
OBJECT_TO_POPULATE was not keep here, as i don't think it should be the responsability of an instantiator to handle that. And if we want to have this responsability we can always add a new implementation with a decoration system.
I try to look at var exporter instantiator also and unfortunetaly @nicolas-grekas i cannot use this, since the behavior of this component is to not use the constructor, in the serializer we want to use it, or at least it used to do that and we cannot change this behavior.
But we can use var exporter implementation in a future PR (will not be the default however just another way of doing it) here if someone does not want to call the constructor.