diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 7063d3d19f..5f60aacc6f 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -15,10 +15,11 @@ use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Exception\InvalidArgumentException as LegacyInvalidArgumentException; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -244,7 +245,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]); } catch (ItemNotFoundException $e) { throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } catch (InvalidArgumentException $e) { + } catch (LegacyInvalidArgumentException|InvalidArgumentException $e) { throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); } } @@ -541,9 +542,14 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); $collectionKeyTypes = $type->getCollectionKeyTypes(); foreach ($value as $index => $obj) { + $currentChildContext = $childContext; + if (isset($childContext['deserialization_path'])) { + $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]"; + } + // no typehint provided on collection key if (!$collectionKeyTypes) { - $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext); + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext); continue; } @@ -554,7 +560,7 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert continue; } - $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $childContext); + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext); continue 2; } throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $collectionKeyTypes[0]->getBuiltinType(), \gettype($index)), $index, [$collectionKeyTypes[0]->getBuiltinType()], ($context['deserialization_path'] ?? false) ? \sprintf('key(%s)', $context['deserialization_path']) : null, true); @@ -590,7 +596,7 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope ); return null; - } catch (InvalidArgumentException $e) { + } catch (LegacyInvalidArgumentException|InvalidArgumentException $e) { if (!isset($context['not_normalizable_value_exceptions'])) { throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); } diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 55c5be3d95..95a7f1d824 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\IriConverterInterface; @@ -1034,6 +1035,56 @@ public function testBadRelationTypeWithExceptionToValidationErrors(): void $this->assertNull($actual->relatedDummy); } + public function testDeserializationPathForNotDenormalizableRelations(): void + { + $data = [ + 'relatedDummies' => ['wrong'], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummies'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class))])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getResourceFromIri(Argument::cetera())->willThrow(new InvalidArgumentException('Invalid IRI')); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $errors = []; + $actual = $normalizer->denormalize($data, Dummy::class, null, ['not_normalizable_value_exceptions' => &$errors]); + $this->assertEmpty($actual->relatedDummies); + $this->assertCount(1, $errors); // @phpstan-ignore-line method.impossibleType (false positive) + $this->assertInstanceOf(NotNormalizableValueException::class, $errors[0]); + $this->assertSame('relatedDummies[0]', $errors[0]->getPath()); + } + public function testInnerDocumentNotAllowed(): void { $this->expectException(UnexpectedValueException::class);