Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit daebc6d

Browse filesBrowse files
committed
Add COLLECT_EXTRA_ATTRIBUTES_ERRORS and full deserialization path
1 parent c2af1fd commit daebc6d
Copy full SHA for daebc6d

18 files changed

+434
-30
lines changed

‎UPGRADE-6.2.md

Copy file name to clipboardExpand all lines: UPGRADE-6.2.md
+1-6
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ PropertyAccess
6666
--------------
6767

6868
* Deprecate calling `PropertyAccessorBuilder::setCacheItemPool()` without arguments
69-
* Implementing the `PropertyPathInterface` without implementing the `isNullSafe()` method is deprecated
70-
71-
Messenger
72-
--------
73-
74-
* Deprecate `MessageHandlerInterface` and `MessageSubscriberInterface`, use the `AsMessageHandler` attribute instead
7569

7670
Security
7771
--------
@@ -90,6 +84,7 @@ Serializer
9084
* Deprecate calling `AttributeMetadata::setSerializedName()`, `ClassMetadata::setClassDiscriminatorMapping()` without arguments
9185
* Change the signature of `AttributeMetadataInterface::setSerializedName()` to `setSerializedName(?string)`
9286
* Change the signature of `ClassMetadataInterface::setClassDiscriminatorMapping()` to `setClassDiscriminatorMapping(?ClassDiscriminatorMapping)`
87+
* Deprecate `PartialDenormalizationException::getErrors()`, call `getNotNormalizableValueErrors()` instead
9388

9489
Validator
9590
---------

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Helper/Table.php
+1-1
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/CHANGELOG.md
+1
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/PropertyPath.php
+25
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/PropertyAccess/Tests/PropertyPathTest.php
+20
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/CHANGELOG.md
+2
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Context/SerializerContextBuilder.php
+6
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Exception/PartialDenormalizationException.php
+28-4
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+2-1
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php
+12-7
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php
+2-1
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/DenormalizerInterface.php
+8
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Serializer.php
+19-7
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Context/SerializerContextBuilderTest.php
+4
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php
+1
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.