Skip to content

Commit adf10e3

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

18 files changed

+434
-24
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: 1 addition & 1 deletion
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

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 `Symfony\Component\PropertyAccess\PropertyPath::append()`
910

1011
6.0
1112
---

src/Symfony/Component/PropertyAccess/PropertyPath.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,29 @@ 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+
if ('[' === $subPath[0]) {
223+
return $basePath.$subPath;
224+
}
225+
226+
return '' !== $basePath ? $basePath.'.'.$subPath : $subPath;
227+
}
228+
229+
return $basePath;
230+
}
206231
}

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($basePath, $subPath, $expectedPath, $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: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,45 @@
1717
class PartialDenormalizationException extends UnexpectedValueException
1818
{
1919
private $data;
20-
private $errors;
20+
/**
21+
* @var NotNormalizableValueException[]
22+
*/
23+
private array $notNormalizableErrors;
24+
private ?ExtraAttributesException $extraAttributesError = null;
2125

22-
public function __construct($data, array $errors)
26+
public function __construct($data, array $notNormalizableErrors, array $extraAttributesErrors = [])
2327
{
2428
$this->data = $data;
25-
$this->errors = $errors;
29+
$this->notNormalizableErrors = $notNormalizableErrors;
30+
$extraAttributes = [];
31+
foreach ($extraAttributesErrors as $error) {
32+
\array_push($extraAttributes, ...$error->getExtraAttributes());
33+
}
34+
if (\count($extraAttributes) > 0) {
35+
$this->extraAttributesError = new ExtraAttributesException($extraAttributes);
36+
}
2637
}
2738

2839
public function getData()
2940
{
3041
return $this->data;
3142
}
3243

44+
/**
45+
* @deprecated Use getNotNormalizableValueErrors() instead.
46+
*/
3347
public function getErrors(): array
3448
{
35-
return $this->errors;
49+
return $this->getNotNormalizableValueErrors();
50+
}
51+
52+
public function getNotNormalizableValueErrors(): array
53+
{
54+
return $this->notNormalizableErrors;
55+
}
56+
57+
public function getExtraAttributesError(): ?ExtraAttributesException
58+
{
59+
return $this->extraAttributesError;
3660
}
3761
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2323
use Symfony\Component\Serializer\SerializerAwareInterface;
2424
use Symfony\Component\Serializer\SerializerAwareTrait;
25+
use Symfony\Component\Serializer\Util\PropertyPath;
2526

2627
/**
2728
* Normalizer implementation.
@@ -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: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
2929
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
3030
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
31+
use Symfony\Component\Serializer\Util\PropertyPath;
3132

3233
/**
3334
* Base class for a normalizer dealing with objects.
@@ -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 (!empty($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;
@@ -447,14 +452,14 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
447452
} elseif ('true' === $data || '1' === $data) {
448453
$data = true;
449454
} else {
450-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null);
455+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? $attribute);
451456
}
452457
break;
453458
case Type::BUILTIN_TYPE_INT:
454459
if (ctype_digit('-' === $data[0] ? substr($data, 1) : $data)) {
455460
$data = (int) $data;
456461
} else {
457-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null);
462+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? $attribute);
458463
}
459464
break;
460465
case Type::BUILTIN_TYPE_FLOAT:
@@ -466,7 +471,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
466471
'NaN' => \NAN,
467472
'INF' => \INF,
468473
'-INF' => -\INF,
469-
default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null),
474+
default => throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $currentClass, $data), $data, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? $attribute),
470475
};
471476
}
472477
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Serializer\Exception\BadMethodCallException;
1616
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1717
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
18+
use Symfony\Component\Serializer\Util\PropertyPath;
1819

1920
/**
2021
* Denormalizes arrays of objects.
@@ -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: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,19 +212,31 @@ 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+
$notNormalizableExceptions = [];
215216
if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) {
217+
if ($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]) {
218+
$context['not_normalizable_value_exceptions'] = [];
219+
$notNormalizableExceptions = &$context['not_normalizable_value_exceptions'];
220+
}
216221
unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]);
217-
$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+
}
223+
224+
$extraAttributesExceptions = [];
225+
if (isset($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS])) {
226+
if ($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS]) {
227+
$context['extra_attributes_exceptions'] = [];
228+
$extraAttributesExceptions = &$context['extra_attributes_exceptions'];
222229
}
230+
unset($context[DenormalizerInterface::COLLECT_EXTRA_ATTRIBUTES_ERRORS]);
231+
}
232+
233+
$denormalized = $normalizer->denormalize($data, $type, $format, $context);
223234

224-
return $denormalized;
235+
if (\count($notNormalizableExceptions) > 0 || \count($extraAttributesExceptions) > 0) {
236+
throw new PartialDenormalizationException($denormalized, $notNormalizableExceptions ?? [], $extraAttributesExceptions ?? []);
225237
}
226238

227-
return $normalizer->denormalize($data, $type, $format, $context);
239+
return $denormalized;
228240
}
229241

230242
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)