Description
> Context:
ObjectNormalizer from Symfony's Serializer component.
> Goal:
Automating the deserialization of objects that depend on services from the "Service Container" or parameters from the "Parameter Bag," avoiding the need to create a specific denormalizer for each class or object.
> Use Case:
Enable deserialization/denormalization into domain objects commonly used in a DDD (Domain-Driven Design) architecture. These domain objects include properties as well as functionalities implemented via services.
Domain objects are instantiated by factories that ensure the injection of dependencies.
> Functional Proposal:
Facilitate the denormalization process for objects with service dependencies by leveraging their factories.
Factories should be identified and made callable by the ObjectNormalizer
.
> Technical Proposal:
1) Characterizing Factories:
- Factories are identified as tagged services.
- Tag:
- Attribute
name
(e.g.,serializer.denormalizer.factory
) - Attribute
type
= the type of the object to instantiate (FQCN).
- Attribute
- Creation of an interface (e.g.,
FactoryInterface
) that declares a method (e.g.,getMethodName
) to return the name of the method used to instantiate the object.
2) Accessing Factories from the Normalizer:
Two Strategies :
- Strategy 1 : Injection via constructor of ObjectNormalizer
-
Add a
$factories
property to the constructor of theObjectNormalizer
class. -
Use the
#[AutowireIterator]
attribute to automatically inject all tagged factory services.#[AutowireIterator(tag: 'serializer.denormalizer.factory', indexAttribute: 'type')]
-
The
$factories
property traverses the inheritance tree to reach the abstract classAbstractNormalizer
, where theinstantiateObject
method is modified to invoke the factories if needed (according to the state of the denormalization). -
Advantage: Automatic injection.
-
Disadvantage: Adds complexity to the constructor with an additional parameter.
- Strategy 2 : Passing via Context
-
Creation of a context option
DENORMALIZER_FACTORIES
as an associative array:object type => service factory instance
-
In the abstract class AbstractNormalizer: modify the instantiateObject method to invoke the factories if needed (according to the state of the denormalization).
-
Advantage: Does not add complexity to the constructor with an additional parameter. The definition of factories to use benefits from the flexibility of the context.
-
Disadvantage: Injection is not automatic. A default context must be created, which requires creating a decorator for the ObjectNormalizer to add a context based on a factory collection service, which also needs to be implemented.
Example
Interface :
interface FactoryInterface
{
public function getMethodName(): string;
}
In the method Symfony\Component\Serializer\Normalizer\AbstractNormalizer:instantiateObject(...) :
-
BEFORE :
if ($missingConstructorArguments) { throw new MissingConstructorArgumentsException(\sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class); }
-
AFTER :
if ($missingConstructorArguments) { if ($constructor->isConstructor() && null !== $factory = $this->getFactory($reflectionClass)) { $factoryMethodName = $factory->getMethodName(); $orderedParams = $this->reorderArguments($params, get_class($factory), $factoryMethodName); return $factory->$factoryMethodName(...$orderedParams); } throw new MissingConstructorArgumentsException(\sprintf('Cannot create an instance of "%s" from serialized data because its constructor requires the following parameters to be present : "$%s".', $class, implode('", "$', $missingConstructorArguments)), 0, null, $missingConstructorArguments, $class); }
NOTE :
- The method
$this->reorderArguments()
ensures that the arguments are passed in the correct order to the factory method. - The method
$this->getFactory()
retrieves the factory corresponding to the type $reflectionClass->name. Its implementation will depend on the chosen strategy ("Injection via the constructor of ObjectNormalizer" or "Passing via Context").