Skip to content

Commit ac2c423

Browse files
committed
Add COLLECT_EXTRA_ATTRIBUTES_ERRORS and full deserialization path
1 parent c2af1fd commit ac2c423

17 files changed

+398
-30
lines changed

UPGRADE-6.2.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Serializer
9090
* Deprecate calling `AttributeMetadata::setSerializedName()`, `ClassMetadata::setClassDiscriminatorMapping()` without arguments
9191
* Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)`
9292
* Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)`
93+
* Deprecate `PartialDenormalizationException::getErrors()`, call `getNotNormalizableValueErrors()` instead
9394

9495
Validator
9596
---------

src/Symfony/Component/Console/Helper/Table.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public function setHeaders(array $headers): static
192192
/**
193193
* @return $this
194194
*/
195-
public function setRows(array $rows)
195+
public function setRows(array $rows): static
196196
{
197197
$this->rows = [];
198198

@@ -251,7 +251,7 @@ public function appendRow(TableSeparator|array $row): static
251251
/**
252252
* @return $this
253253
*/
254-
public function setRow(int|string $column, array $row): static
254+
public function setRow(int|string $column, array $row)
255255
{
256256
$this->rows[$column] = $row;
257257

src/Symfony/Component/PropertyAccess/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments
88
* Added method `isNullSafe()` to `PropertyPathInterface`
9+
* Add `PropertyPath::append()`
910

1011
6.0
1112
---

src/Symfony/Component/PropertyAccess/PropertyPath.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,33 @@ public function isNullSafe(int $index): bool
203203

204204
return $this->isNullSafe[$index];
205205
}
206+
207+
/**
208+
* Utility method for dealing with property paths.
209+
* For more extensive functionality, use instances of this class.
210+
*
211+
* Appends a path to a given property path.
212+
*
213+
* If the base path is empty, the appended path will be returned unchanged.
214+
* If the base path is not empty, and the appended path starts with a
215+
* squared opening bracket ("["), the concatenation of the two paths is
216+
* returned. Otherwise, the concatenation of the two paths is returned,
217+
* separated by a dot (".").
218+
*/
219+
public static function append(string $basePath, string $subPath): string
220+
{
221+
if ('' === $subPath) {
222+
return $basePath;
223+
}
224+
225+
if ('[' === $subPath[0]) {
226+
return $basePath.$subPath;
227+
}
228+
229+
if ('' === $basePath) {
230+
return $subPath;
231+
}
232+
233+
return $basePath.'.'.$subPath;
234+
}
206235
}

src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,24 @@ public function testIsIndexDoesNotAcceptNegativeIndices()
170170

171171
$propertyPath->isIndex(-1);
172172
}
173+
174+
/**
175+
* @dataProvider provideAppendPaths
176+
*/
177+
public function testAppend(string $basePath, string $subPath, string $expectedPath, string $message)
178+
{
179+
$this->assertSame($expectedPath, PropertyPath::append($basePath, $subPath), $message);
180+
}
181+
182+
public function provideAppendPaths()
183+
{
184+
return [
185+
['foo', '', 'foo', 'It returns the basePath if subPath is empty'],
186+
['', 'bar', 'bar', 'It returns the subPath if basePath is empty'],
187+
['foo', 'bar', 'foo.bar', 'It append the subPath to the basePath'],
188+
['foo', '[bar]', 'foo[bar]', 'It does not include the dot separator if subPath uses the array notation'],
189+
['0', 'bar', '0.bar', 'Leading zeros are kept.'],
190+
['0', 1, '0.1', 'Numeric subpaths do not cause PHP 7.4 errors.'],
191+
];
192+
}
173193
}

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ CHANGELOG
1111
* Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)`
1212
* Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)`
1313
* Add option YamlEncoder::YAML_INDENTATION to YamlEncoder constructor options to configure additional indentation for each level of nesting. This allows configuring indentation in the service configuration.
14+
* Add `COLLECT_EXTRA_ATTRIBUTES_ERRORS` option to `Serializer` to collect errors from nested denormalizations
15+
* Deprecate `PartialDenormalizationException::getErrors()`, call `getNotNormalizableValueErrors()` instead
1416

1517
6.1
1618
---

src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php

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

1212
namespace Symfony\Component\Serializer\Context;
1313

14+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
1415
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1516
use Symfony\Component\Serializer\Serializer;
1617

@@ -36,4 +37,9 @@ public function withCollectDenormalizationErrors(?bool $collectDenormalizationEr
3637
{
3738
return $this->with(DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS, $collectDenormalizationErrors);
3839
}
40+
41+
public function withCollectExtraAttributesErrors(?bool $collectExtraAttributesErrors): static
42+
{
43+
return $this->with(DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS, $collectExtraAttributesErrors);
44+
}
3945
}

src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,51 @@
1616
*/
1717
class PartialDenormalizationException extends UnexpectedValueException
1818
{
19-
private $data;
20-
private $errors;
19+
private ?ExtraAttributesException $extraAttributesError = null;
2120

22-
public function __construct($data, array $errors)
21+
public function __construct(
22+
private $data,
23+
/**
24+
* @var NotNormalizableValueException[]
25+
*/
26+
private array $notNormalizableErrors,
27+
array $extraAttributesErrors = []
28+
)
2329
{
2430
$this->data = $data;
25-
$this->errors = $errors;
31+
$this->notNormalizableErrors = $notNormalizableErrors;
32+
$extraAttributes = [];
33+
foreach ($extraAttributesErrors as $error) {
34+
$extraAttributes = \array_merge($extraAttributes, $error->getExtraAttributes());
35+
}
36+
if (\count($extraAttributes) > 0) {
37+
$this->extraAttributesError = new ExtraAttributesException($extraAttributes);
38+
}
2639
}
2740

2841
public function getData()
2942
{
3043
return $this->data;
3144
}
3245

46+
/**
47+
* @deprecated Use getNotNormalizableValueErrors() instead.
48+
*/
3349
public function getErrors(): array
3450
{
35-
return $this->errors;
51+
return $this->getNotNormalizableValueErrors();
52+
}
53+
54+
/**
55+
* @var NotNormalizableValueException[]
56+
*/
57+
public function getNotNormalizableValueErrors(): array
58+
{
59+
return $this->notNormalizableErrors;
60+
}
61+
62+
public function getExtraAttributesError(): ?ExtraAttributesException
63+
{
64+
return $this->extraAttributesError;
3665
}
3766
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\PropertyAccess\PropertyPath;
1415
use Symfony\Component\Serializer\Exception\CircularReferenceException;
1516
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1617
use Symfony\Component\Serializer\Exception\LogicException;
@@ -505,7 +506,7 @@ protected function getAttributeNormalizationContext(object $object, string $attr
505506
*/
506507
protected function getAttributeDenormalizationContext(string $class, string $attribute, array $context): array
507508
{
508-
$context['deserialization_path'] = ($context['deserialization_path'] ?? false) ? $context['deserialization_path'].'.'.$attribute : $attribute;
509+
$context['deserialization_path'] = PropertyPath::append($context['deserialization_path'] ?? '', $attribute);
509510

510511
if (null === $metadata = $this->getAttributeMetadata($class, $attribute)) {
511512
return $context;

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
1515
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1616
use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException;
17+
use Symfony\Component\PropertyAccess\PropertyPath;
1718
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
1819
use Symfony\Component\PropertyInfo\Type;
1920
use Symfony\Component\Serializer\Encoder\CsvEncoder;
@@ -225,12 +226,12 @@ protected function instantiateObject(array &$data, string $class, array &$contex
225226
{
226227
if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) {
227228
if (!isset($data[$mapping->getTypeProperty()])) {
228-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false);
229+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], PropertyPath::append($context['deserialization_path'] ?? '', $mapping->getTypeProperty()), false);
229230
}
230231

231232
$type = $data[$mapping->getTypeProperty()];
232233
if (null === ($mappedClass = $mapping->getClassForType($type))) {
233-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true);
234+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], PropertyPath::append($context['deserialization_path'] ?? '', $mapping->getTypeProperty()), true);
234235
}
235236

236237
if ($mappedClass !== $class) {
@@ -378,8 +379,12 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
378379
}
379380
}
380381

381-
if ($extraAttributes) {
382-
throw new ExtraAttributesException($extraAttributes);
382+
if (!$extraAttributes) {
383+
$extraAttributeException = new ExtraAttributesException(array_map(fn (string $extraAttribute) => PropertyPath::append($context['deserialization_path'] ?? '', $extraAttribute), $extraAttributes));
384+
if (!isset($context['extra_attributes_exceptions'])) {
385+
throw $extraAttributeException;
386+
}
387+
$context['extra_attributes_exceptions'][] = $extraAttributeException;
383388
}
384389

385390
return $object;
@@ -566,7 +571,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
566571
return $data;
567572
}
568573

569-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? $attribute);
574+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be one of "%s" ("%s" given).', $attribute, $currentClass, implode('", "', array_keys($expectedTypes)), get_debug_type($data)), $data, array_keys($expectedTypes), $context['deserialization_path'] ?? null);
570575
}
571576

572577
/**

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\PropertyAccess\PropertyPath;
1415
use Symfony\Component\PropertyInfo\Type;
1516
use Symfony\Component\Serializer\Exception\BadMethodCallException;
1617
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
@@ -47,7 +48,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
4748
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
4849
foreach ($data as $key => $value) {
4950
$subContext = $context;
50-
$subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]";
51+
$subContext['deserialization_path'] = PropertyPath::append($context['deserialization_path'] ?? '', "[$key]");
5152

5253
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
5354
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)), $key, [$builtinType], $subContext['deserialization_path'] ?? null, true);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,16 @@
2424
*/
2525
interface DenormalizerInterface
2626
{
27+
/**
28+
* Whether to collect all denormalization errors or to stop at first error.
29+
*/
2730
public const COLLECT_DENORMALIZATION_ERRORS = 'collect_denormalization_errors';
2831

32+
/**
33+
* Whether to collect all extra attributes errors or to stop at first nested error.
34+
*/
35+
public const COLLECT_EXTRA_ATTRIBUTES_ERRORS = 'collect_extra_attributes_errors';
36+
2937
/**
3038
* Denormalizes data back into an object of the given class.
3139
*

src/Symfony/Component/Serializer/Serializer.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,19 +212,27 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
212212
throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type));
213213
}
214214

215-
if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) {
216-
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
215+
$notNormalizableExceptions = [];
216+
if ($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS] ?? false) {
217217
$context['not_normalizable_value_exceptions'] = [];
218-
$errors = &$context['not_normalizable_value_exceptions'];
219-
$denormalized = $normalizer->denormalize($data, $type, $format, $context);
220-
if ($errors) {
221-
throw new PartialDenormalizationException($denormalized, $errors);
222-
}
218+
$notNormalizableExceptions = &$context['not_normalizable_value_exceptions'];
219+
}
220+
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
221+
222+
$extraAttributesExceptions = [];
223+
if ($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS] ?? false) {
224+
$context['extra_attributes_exceptions'] = [];
225+
$extraAttributesExceptions = &$context['extra_attributes_exceptions'];
226+
}
227+
unset($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS]);
228+
229+
$denormalized = $normalizer->denormalize($data, $type, $format, $context);
223230

224-
return $denormalized;
231+
if (\count($notNormalizableExceptions) > 0 || \count($extraAttributesExceptions) > 0) {
232+
throw new PartialDenormalizationException($denormalized, $notNormalizableExceptions, $extraAttributesExceptions);
225233
}
226234

227-
return $normalizer->denormalize($data, $type, $format, $context);
235+
return $denormalized;
228236
}
229237

230238
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool

src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Serializer\Context\SerializerContextBuilder;
16+
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
1617
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
1718
use Symfony\Component\Serializer\Serializer;
1819

@@ -38,6 +39,7 @@ public function testWithers(array $values)
3839
$context = $this->contextBuilder
3940
->withEmptyArrayAsObject($values[Serializer::EMPTY_ARRAY_AS_OBJECT])
4041
->withCollectDenormalizationErrors($values[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])
42+
->withCollectExtraAttributesErrors($values[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS])
4143
->toArray();
4244

4345
$this->assertSame($values, $context);
@@ -51,11 +53,13 @@ public function withersDataProvider(): iterable
5153
yield 'With values' => [[
5254
Serializer::EMPTY_ARRAY_AS_OBJECT => true,
5355
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => false,
56+
DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => false,
5457
]];
5558

5659
yield 'With null values' => [[
5760
Serializer::EMPTY_ARRAY_AS_OBJECT => null,
5861
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => null,
62+
DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS => null,
5963
]];
6064
}
6165
}

src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ final class Php74Full
3333
public TestFoo $nestedObject;
3434
/** @var Php74Full[] */
3535
public $anotherCollection;
36+
public TestFoo $nestedObject2;
3637
}
3738

3839

0 commit comments

Comments
 (0)