Skip to content

Commit 8f36f2a

Browse files
committed
[Serializer] Instantiator - Add an interface and default implementation to instantiate objects #30956
1 parent de1c216 commit 8f36f2a

17 files changed

+757
-144
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Context;
13+
14+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
15+
16+
/**
17+
* Create a child context during serialization/deserialization process.
18+
*
19+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
20+
*/
21+
class ChildContextFactory implements ChildContextFactoryInterface
22+
{
23+
public const ATTRIBUTES = AbstractObjectNormalizer::ATTRIBUTES;
24+
25+
public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array
26+
{
27+
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
28+
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
29+
} else {
30+
unset($parentContext[self::ATTRIBUTES]);
31+
}
32+
33+
return $parentContext;
34+
}
35+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Context;
13+
14+
/**
15+
* Defines the interface to create a child context during serialization/deserialization or instantiation process.
16+
*
17+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
18+
*/
19+
interface ChildContextFactoryInterface
20+
{
21+
public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array;
22+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Context;
13+
14+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
15+
16+
/**
17+
* Create a child context with cache_key during serialization/deserialization or instantiation process.
18+
*
19+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
20+
*/
21+
class ObjectChildContextFactory extends ChildContextFactory
22+
{
23+
public const EXCLUDE_FROM_CACHE_KEY = AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY;
24+
public const IGNORED_ATTRIBUTES = AbstractObjectNormalizer::IGNORED_ATTRIBUTES;
25+
26+
public function create(array $parentContext, string $attribute, ?string $format = null, array $defaultContext = []): array
27+
{
28+
$parentContext = parent::create($parentContext, $attribute, $format, $defaultContext);
29+
$parentContext['cache_key'] = $this->getAttributesCacheKey($parentContext, $format, $defaultContext);
30+
31+
return $parentContext;
32+
}
33+
34+
/**
35+
* Builds the cache key for the attributes cache.
36+
*
37+
* The key must be different for every option in the context that could change which attributes should be handled.
38+
*
39+
* @return bool|string
40+
*/
41+
private function getAttributesCacheKey(array $context, ?string $format = null, array $defaultContext = [])
42+
{
43+
foreach ($context[self::EXCLUDE_FROM_CACHE_KEY] ?? $defaultContext[self::EXCLUDE_FROM_CACHE_KEY] ?? [] as $key) {
44+
unset($context[$key]);
45+
}
46+
unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
47+
unset($context['cache_key']); // avoid artificially different keys
48+
49+
try {
50+
return md5($format.serialize([
51+
'context' => $context,
52+
'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $defaultContext[self::IGNORED_ATTRIBUTES] ?? [],
53+
]));
54+
} catch (\Exception $exception) {
55+
// The context cannot be serialized, skip the cache
56+
return false;
57+
}
58+
}
59+
}

src/Symfony/Component/Serializer/Instantiator/Instantiator.php

Lines changed: 192 additions & 21 deletions
Large diffs are not rendered by default.

src/Symfony/Component/Serializer/Instantiator/InstantiatorInterface.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
1515

1616
/**
17+
* Describes the interface to instantiate an object using constructor parameters when needed.
18+
*
1719
* @author Jérôme Desjardins <jewome62@gmail.com>
20+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
1821
*/
1922
interface InstantiatorInterface
2023
{
2124
/**
22-
* Instantiate a new object.
25+
* Instantiates a new object.
2326
*
2427
* @throws MissingConstructorArgumentsException When some arguments are missing to use the constructor
25-
*
26-
* @return mixed
2728
*/
28-
public function instantiate(string $class, $data, $format = null, array $context = []);
29+
public function instantiate(string $class, array $data, array $context, string $format = null): InstantiatorResult;
2930
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Instantiator;
13+
14+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
15+
16+
/**
17+
* Contains the result of an instantiation process.
18+
*
19+
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
20+
*/
21+
final class InstantiatorResult
22+
{
23+
private $object;
24+
private $data;
25+
private $context;
26+
private $error;
27+
28+
public function __construct(?object $object, array $data, array $context, string $error = null)
29+
{
30+
$this->object = $object;
31+
$this->data = $data;
32+
$this->context = $context;
33+
$this->error = $error;
34+
}
35+
36+
public function getObject(): ?object
37+
{
38+
return $this->object;
39+
}
40+
41+
public function getUnusedData(): array
42+
{
43+
return $this->data;
44+
}
45+
46+
public function getUnusedContext(): array
47+
{
48+
return $this->context;
49+
}
50+
51+
public function getError(): ?\Throwable
52+
{
53+
if (null === $this->error) {
54+
return null;
55+
}
56+
57+
return new MissingConstructorArgumentsException($this->error);
58+
}
59+
60+
public function hasFailed(): bool
61+
{
62+
return null === $this->object;
63+
}
64+
}

src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\Serializer\Context\ChildContextFactoryInterface;
15+
use Symfony\Component\Serializer\Context\ObjectChildContextFactory;
1416
use Symfony\Component\Serializer\Exception\CircularReferenceException;
1517
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1618
use Symfony\Component\Serializer\Exception\LogicException;
@@ -133,10 +135,12 @@ abstract class AbstractNormalizer implements NormalizerInterface, DenormalizerIn
133135
*/
134136
protected $nameConverter;
135137

138+
protected $childContextFactory;
139+
136140
/**
137141
* Sets the {@link ClassMetadataFactoryInterface} to use.
138142
*/
139-
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [])
143+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, array $defaultContext = [], ChildContextFactoryInterface $childContextFactory = null)
140144
{
141145
$this->classMetadataFactory = $classMetadataFactory;
142146
$this->nameConverter = $nameConverter;
@@ -157,6 +161,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
157161
if (isset($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER]) && !\is_callable($this->defaultContext[self::CIRCULAR_REFERENCE_HANDLER])) {
158162
throw new InvalidArgumentException(sprintf('Invalid callback found in the "%s" default context option.', self::CIRCULAR_REFERENCE_HANDLER));
159163
}
164+
165+
$this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory();
160166
}
161167

162168
/**
@@ -436,15 +442,11 @@ protected function denormalizeParameter(\ReflectionClass $class, \ReflectionPara
436442

437443
/**
438444
* @internal
445+
*
446+
* @deprecated the "createChildContext" method is deprecated, use Symfony\Component\Serializer\Context\ChildContextFactory::create() instead
439447
*/
440448
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
441449
{
442-
if (isset($parentContext[self::ATTRIBUTES][$attribute])) {
443-
$parentContext[self::ATTRIBUTES] = $parentContext[self::ATTRIBUTES][$attribute];
444-
} else {
445-
unset($parentContext[self::ATTRIBUTES]);
446-
}
447-
448-
return $parentContext;
450+
return $this->childContextFactory->create($parentContext, $attribute, $format);
449451
}
450452
}

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1616
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
1717
use Symfony\Component\PropertyInfo\Type;
18+
use Symfony\Component\Serializer\Context\ChildContextFactoryInterface;
19+
use Symfony\Component\Serializer\Context\ObjectChildContextFactory;
1820
use Symfony\Component\Serializer\Encoder\CsvEncoder;
1921
use Symfony\Component\Serializer\Encoder\JsonEncoder;
2022
use Symfony\Component\Serializer\Encoder\XmlEncoder;
2123
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
2224
use Symfony\Component\Serializer\Exception\LogicException;
25+
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
2326
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
24-
use Symfony\Component\Serializer\Exception\RuntimeException;
27+
use Symfony\Component\Serializer\Instantiator\InstantiatorInterface;
2528
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
2629
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
2730
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -33,8 +36,10 @@
3336
*
3437
* @author Kévin Dunglas <dunglas@gmail.com>
3538
*/
36-
abstract class AbstractObjectNormalizer extends AbstractNormalizer
39+
abstract class AbstractObjectNormalizer extends AbstractNormalizer implements DenormalizerAwareInterface
3740
{
41+
use DenormalizerAwareTrait;
42+
3843
/**
3944
* Set to true to respect the max depth metadata on fields.
4045
*/
@@ -93,6 +98,7 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
9398
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
9499

95100
private $propertyTypeExtractor;
101+
private $instantiator;
96102
private $typesCache = [];
97103
private $attributesCache = [];
98104

@@ -103,9 +109,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
103109
*/
104110
protected $classDiscriminatorResolver;
105111

106-
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [])
112+
public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyTypeExtractorInterface $propertyTypeExtractor = null, ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, callable $objectClassResolver = null, array $defaultContext = [], ChildContextFactoryInterface $childContextFactory = null, InstantiatorInterface $instantiator = null)
107113
{
108-
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext);
114+
$this->childContextFactory = $childContextFactory ?? new ObjectChildContextFactory();
115+
parent::__construct($classMetadataFactory, $nameConverter, $defaultContext, $this->childContextFactory);
109116

110117
if (isset($this->defaultContext[self::MAX_DEPTH_HANDLER]) && !\is_callable($this->defaultContext[self::MAX_DEPTH_HANDLER])) {
111118
throw new InvalidArgumentException(sprintf('The "%s" given in the default context is not callable.', self::MAX_DEPTH_HANDLER));
@@ -120,6 +127,11 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory
120127
}
121128
$this->classDiscriminatorResolver = $classDiscriminatorResolver;
122129
$this->objectClassResolver = $objectClassResolver;
130+
131+
if (null === $instantiator) {
132+
throw new InvalidArgumentException(sprintf('Instantiator parameter is required, please use "%s" to instantiate your Normalizer.', get_parent_class($this).'Factory'));
133+
}
134+
$this->instantiator = $instantiator;
123135
}
124136

125137
/**
@@ -210,29 +222,6 @@ public function normalize($object, string $format = null, array $context = [])
210222
return $data;
211223
}
212224

213-
/**
214-
* {@inheritdoc}
215-
*/
216-
protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes, string $format = null)
217-
{
218-
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
219-
if (!isset($data[$mapping->getTypeProperty()])) {
220-
throw new RuntimeException(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class));
221-
}
222-
223-
$type = $data[$mapping->getTypeProperty()];
224-
if (null === ($mappedClass = $mapping->getClassForType($type))) {
225-
throw new RuntimeException(sprintf('The type "%s" has no mapped class for the abstract object "%s".', $type, $class));
226-
}
227-
228-
if ($mappedClass !== $class) {
229-
return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format);
230-
}
231-
}
232-
233-
return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format);
234-
}
235-
236225
/**
237226
* Gets and caches attributes for the given object, format and context.
238227
*
@@ -307,8 +296,14 @@ public function denormalize($data, string $type, string $format = null, array $c
307296
$normalizedData = $this->prepareForDenormalization($data);
308297
$extraAttributes = [];
309298

310-
$reflectionClass = new \ReflectionClass($type);
311-
$object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format);
299+
$instantiatorResult = $this->instantiator->instantiate($type, $normalizedData, $context, $format);
300+
if ($instantiatorResult->hasFailed()) {
301+
throw new MissingConstructorArgumentsException($instantiatorResult->getError());
302+
}
303+
$object = $instantiatorResult->getObject();
304+
$normalizedData = $instantiatorResult->getUnusedData();
305+
$context = $instantiatorResult->getUnusedContext();
306+
312307
$resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : \get_class($object);
313308

314309
foreach ($normalizedData as $attribute => $value) {
@@ -603,13 +598,12 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str
603598
* {@inheritdoc}
604599
*
605600
* @internal
601+
*
602+
* @deprecated the "createChildContext" method is deprecated, use Symfony\Component\Serializer\Context\ObjectChildContextFactory::create() instead
606603
*/
607604
protected function createChildContext(array $parentContext, string $attribute, ?string $format): array
608605
{
609-
$context = parent::createChildContext($parentContext, $attribute, $format);
610-
$context['cache_key'] = $this->getCacheKey($format, $context);
611-
612-
return $context;
606+
return $this->childContextFactory->create($parentContext, $attribute, $format);
613607
}
614608

615609
/**

0 commit comments

Comments
 (0)