From f0cc2d408e619f4d0926e6c0090c8d25bfe7170f Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Mon, 24 Feb 2025 15:22:50 +0100 Subject: [PATCH 1/3] feat(serializer): use TypeInfo --- features/main/validation.feature | 67 --- src/Serializer/AbstractItemNormalizer.php | 362 +++++++++++-- .../Tests/AbstractItemNormalizerTest.php | 477 +++++++++++++----- tests/Functional/ValidationTest.php | 128 +++++ 4 files changed, 785 insertions(+), 249 deletions(-) create mode 100644 tests/Functional/ValidationTest.php diff --git a/features/main/validation.feature b/features/main/validation.feature index 82d93f939cc..40e22bdfb3a 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -87,74 +87,7 @@ Feature: Using validations groups And the JSON node "detail" should be equal to "test: This value should not be null." And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - @!mongodb @createSchema - Scenario: Create a resource with collectDenormalizationErrors - When I add "Content-type" header equal to "application/ld+json" - And I send a "POST" request to "/dummy_collect_denormalization" with body: - """ - { - "foo": 3, - "bar": "baz", - "qux": true, - "uuid": "y", - "relatedDummy": 8, - "relatedDummies": 76 - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON should be a superset of: - """ - { - "@context": "/contexts/ConstraintViolation", - "@type": "ConstraintViolation", - "hydra:title": "An error occurred", - "hydra:description": "baz: This value should be of type string.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.", - "violations": [ - { - "propertyPath": "baz", - "message": "This value should be of type string.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "Failed to create object because the class misses the \"baz\" property." - }, - { - "propertyPath": "qux", - "message": "This value should be of type string.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "foo", - "message": "This value should be of type bool.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "bar", - "message": "This value should be of type int.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "uuid", - "message": "This value should be of type uuid.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "Invalid UUID string: y" - }, - { - "propertyPath": "relatedDummy", - "message": "This value should be of type array|string.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given." - }, - { - "propertyPath": "relatedDummies", - "message": "This value should be of type array.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - } - ] - } - """ - @!mongodb Scenario: Get violations constraints When I add "Accept" header equal to "application/json" diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 7c12bf211d6..b6527ddcf58 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -29,7 +29,8 @@ use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\LogicException; @@ -42,7 +43,14 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\NullableType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Base item normalizer. @@ -231,7 +239,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, } if (!\is_array($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, ['array', 'string'], $context['deserialization_path'] ?? null); } $previousObject = $this->clone($objectToPopulate); @@ -501,14 +509,18 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v } /** + * @deprecated since 4.1, use "validateAttributeType" instead + * * Validates the type of the value. Allows using integers as floats for JSON formats. * * @throws NotNormalizableValueException */ - protected function validateType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void + protected function validateType(string $attribute, LegacyType $type, mixed $value, ?string $format = null, array $context = []): void { + trigger_deprecation('api-platform/serializer', '4.1', 'The "%s()" method is deprecated, use "%s::validateAttributeType()" instead.', __METHOD__, self::class); + $builtinType = $type->getBuiltinType(); - if (Type::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) { + if (LegacyType::BUILTIN_TYPE_FLOAT === $builtinType && null !== $format && str_contains($format, 'json')) { $isValid = \is_float($value) || \is_int($value); } else { $isValid = \call_user_func('is_'.$builtinType, $value); @@ -520,14 +532,36 @@ protected function validateType(string $attribute, Type $type, mixed $value, ?st } /** + * Validates the type of the value. Allows using integers as floats for JSON formats. + * + * @throws NotNormalizableValueException + */ + protected function validateAttributeType(string $attribute, Type $type, mixed $value, ?string $format = null, array $context = []): void + { + if ($type->isIdentifiedBy(TypeIdentifier::FLOAT) && null !== $format && str_contains($format, 'json')) { + $isValid = \is_float($value) || \is_int($value); + } else { + $isValid = $type->accepts($value); + } + + if (!$isValid) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value)), $value, [(string) $type], $context['deserialization_path'] ?? null); + } + } + + /** + * @deprecated since 4.1, use "denormalizeObjectCollection" instead. + * * Denormalizes a collection of objects. * * @throws NotNormalizableValueException */ - protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array + protected function denormalizeCollection(string $attribute, ApiProperty $propertyMetadata, LegacyType $type, string $className, mixed $value, ?string $format, array $context): array { + trigger_deprecation('api-platform/serializer', '4.1', 'The "%s()" method is deprecated, use "%s::denormalizeObjectCollection()" instead.', __METHOD__, self::class); + if (!\is_array($value)) { - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, ['array'], $context['deserialization_path'] ?? null); } $values = []; @@ -561,6 +595,44 @@ protected function denormalizeCollection(string $attribute, ApiProperty $propert return $values; } + /** + * Denormalizes a collection of objects. + * + * @throws NotNormalizableValueException + */ + protected function denormalizeObjectCollection(string $attribute, ApiProperty $propertyMetadata, Type $type, string $className, mixed $value, ?string $format, array $context): array + { + if (!\is_array($value)) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array", "%s" given.', $attribute, \gettype($value)), $value, ['array'], $context['deserialization_path'] ?? null); + } + + $values = []; + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); + + foreach ($value as $index => $obj) { + $currentChildContext = $childContext; + if (isset($childContext['deserialization_path'])) { + $currentChildContext['deserialization_path'] = "{$childContext['deserialization_path']}[{$index}]"; + } + + if ($type instanceof CollectionType) { + $collectionKeyType = $type->getCollectionKeyType(); + + while ($collectionKeyType instanceof WrappingTypeInterface) { + $collectionKeyType = $type->getWrappedType(); + } + + if (!$collectionKeyType->accepts($index)) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the key "%s" must be "%s", "%s" given.', $index, $type->getCollectionKeyType(), \gettype($index)), $index, [(string) $collectionKeyType], ($context['deserialization_path'] ?? false) ? \sprintf('key(%s)', $context['deserialization_path']) : null, true); + } + } + + $values[$index] = $this->denormalizeRelation($attribute, $propertyMetadata, $className, $obj, $format, $currentChildContext); + } + + return $values; + } + /** * Denormalizes a relation. * @@ -622,10 +694,10 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope } if (!\is_array($value)) { - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, ['array', 'string'], $context['deserialization_path'] ?? null, true); } - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, ['array', 'string'], $context['deserialization_path'] ?? null, true); } /** @@ -672,15 +744,154 @@ protected function getAttributeValue(object $object, string $attribute, ?string return $this->propertyAccessor->getValue($object, $attribute); } - $types = $propertyMetadata->getBuiltinTypes() ?? []; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + + foreach ($types as $type) { + if ( + $type->isCollection() + && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) + && ($className = $collectionValueType->getClassName()) + && $this->resourceClassResolver->isResourceClass($className) + ) { + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); + + // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content + // @see ApiPlatform\JsonApi\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content + if ('jsonld' === $format && $itemUriTemplate = $propertyMetadata->getUriTemplate()) { + $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation( + operationName: $itemUriTemplate, + forceCollection: true, + httpOperation: true + ); + + return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + + if (!is_iterable($attributeValue)) { + throw new UnexpectedValueException('Unexpected non-iterable value for to-many relation.'); + } + + $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); + + $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); + $context['data'] = $data; + $context['type'] = $type; + + if ($this->tagCollector) { + $this->tagCollector->collect($context); + } + + return $data; + } + + if ( + ($className = $type->getClassName()) + && $this->resourceClassResolver->isResourceClass($className) + ) { + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); + unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']); + if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) { + $operation = $this->resourceMetadataCollectionFactory->create($className)->getOperation( + operationName: $uriTemplate, + httpOperation: true + ); + + return $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + + if (!\is_object($attributeValue) && null !== $attributeValue) { + throw new UnexpectedValueException('Unexpected non-object value for to-one relation.'); + } + + $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); + + $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); + $context['data'] = $data; + $context['type'] = $type; + + if ($this->tagCollector) { + $this->tagCollector->collect($context); + } + + return $data; + } + + if (!$this->serializer instanceof NormalizerInterface) { + throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); + } + + unset( + $context['resource_class'], + $context['force_resource_class'], + $context['uri_variables'], + ); + + // Anonymous resources + if ($className) { + $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); + $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true; + + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + + return $this->serializer->normalize($attributeValue, $format, $childContext); + } + + if ('array' === $type->getBuiltinType()) { + if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) { + $context = $this->createOperationContext($context, $className); + } + + $childContext = $this->createChildContext($context, $attribute, $format); + $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? true; + + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + + return $this->serializer->normalize($attributeValue, $format, $childContext); + } + } + + if (!$this->serializer instanceof NormalizerInterface) { + throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', NormalizerInterface::class)); + } + + unset( + $context['resource_class'], + $context['force_resource_class'], + $context['uri_variables'] + ); + + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + + return $this->serializer->normalize($attributeValue, $format, $context); + } + + $type = $propertyMetadata->getNativeType(); + + $nullable = false; + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + $nullable = true; + } + + // TODO check every foreach composite to see if null is an issue + $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]); + $className = null; + $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { + return match (true) { + $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), + $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), + $type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), + default => false, + }; + }; foreach ($types as $type) { - if ( - $type->isCollection() - && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) - && ($className = $collectionValueType->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) - ) { + if ($type instanceof CollectionType && $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass)) { $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content @@ -705,7 +916,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string $data = $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); $context['data'] = $data; - $context['type'] = $type; + $context['type'] = ($nullable && $type instanceof Type) ? Type::nullable($type) : $type; if ($this->tagCollector) { $this->tagCollector->collect($context); @@ -714,10 +925,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string return $data; } - if ( - ($className = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) - ) { + if ($type->isSatisfiedBy($typeIsResourceClass)) { $childContext = $this->createChildContext($this->createOperationContext($context, $className), $attribute, $format); unset($childContext['iri'], $childContext['uri_variables'], $childContext['item_uri_template']); if ('jsonld' === $format && $uriTemplate = $propertyMetadata->getUriTemplate()) { @@ -739,7 +947,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string $data = $this->normalizeRelation($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); $context['data'] = $data; - $context['type'] = $type; + $context['type'] = $nullable ? Type::nullable($type) : $type; if ($this->tagCollector) { $this->tagCollector->collect($context); @@ -768,9 +976,9 @@ protected function getAttributeValue(object $object, string $attribute, ?string return $this->serializer->normalize($attributeValue, $format, $childContext); } - if ('array' === $type->getBuiltinType()) { - if ($className = ($type->getCollectionValueTypes()[0] ?? null)?->getClassName()) { - $context = $this->createOperationContext($context, $className); + if ($type instanceof CollectionType) { + if (($subType = $type->getCollectionValueType()) instanceof ObjectType) { + $context = $this->createOperationContext($context, $subType->getClassName()); } $childContext = $this->createChildContext($context, $attribute, $format); @@ -873,7 +1081,6 @@ private function createAttributeValue(string $attribute, mixed $value, ?string $ throw $exception; } $context['not_normalizable_value_exceptions'][] = $exception; - throw $exception; } } @@ -881,36 +1088,64 @@ private function createAttributeValue(string $attribute, mixed $value, ?string $ private function createAndValidateAttributeValue(string $attribute, mixed $value, ?string $format = null, array $context = []): mixed { $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $this->getFactoryOptions($context)); - $types = $propertyMetadata->getBuiltinTypes() ?? []; + + $types = []; + $type = null; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + } else { + $type = $propertyMetadata->getNativeType(); + $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]); + } + + $className = null; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType ? $this->resourceClassResolver->isResourceClass($className = $type->getClassName()) : false; + }; + $isMultipleTypes = \count($types) > 1; $denormalizationException = null; - foreach ($types as $type) { - if (null === $value && ($type->isNullable() || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) { + foreach ($types as $t) { + if ($type instanceof Type) { + $isNullable = $type->isNullable(); + } else { + $isNullable = $t->isNullable(); + } + + if (null === $value && ($isNullable || ($context[static::DISABLE_TYPE_ENFORCEMENT] ?? false))) { return $value; } - $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; + $collectionValueType = null; + + if ($t instanceof CollectionType) { + $collectionValueType = $t->getCollectionValueType(); + } elseif ($t instanceof LegacyType) { + $collectionValueType = $t->getCollectionValueTypes()[0] ?? null; + } /* From @see AbstractObjectNormalizer::validateAndDenormalize() */ // Fix a collection that contains the only one element // This is special to xml format only if ('xml' === $format && null !== $collectionValueType && (!\is_array($value) || !\is_int(key($value)))) { - $value = [$value]; + $isMixedType = $collectionValueType instanceof Type && $collectionValueType->isIdentifiedBy(TypeIdentifier::MIXED); + if (!$isMixedType) { + $value = [$value]; + } } - if ( - $type->isCollection() - && null !== $collectionValueType - && null !== ($className = $collectionValueType->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) + if (($collectionValueType instanceof Type && $collectionValueType->isSatisfiedBy($typeIsResourceClass)) + || ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) ) { $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); $context['resource_class'] = $resourceClass; unset($context['uri_variables']); try { - return $this->denormalizeCollection($attribute, $propertyMetadata, $type, $resourceClass, $value, $format, $context); + return $t instanceof Type + ? $this->denormalizeObjectCollection($attribute, $propertyMetadata, $t, $resourceClass, $value, $format, $context) + : $this->denormalizeCollection($attribute, $propertyMetadata, $t, $resourceClass, $value, $format, $context); } catch (NotNormalizableValueException $e) { // union/intersect types: try the next type, if not valid, an exception will be thrown at the end if ($isMultipleTypes) { @@ -924,8 +1159,8 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value } if ( - null !== ($className = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) + ($t instanceof Type && $t->isSatisfiedBy($typeIsResourceClass)) + || ($t instanceof LegacyType && null !== ($className = $t->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) ) { $resourceClass = $this->resourceClassResolver->getResourceClass(null, $className); $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format); @@ -945,10 +1180,8 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value } if ( - $type->isCollection() - && null !== $collectionValueType - && null !== ($className = $collectionValueType->getClassName()) - && \is_array($value) + ($t instanceof CollectionType && $collectionValueType instanceof ObjectType && (null !== ($className = $collectionValueType->getClassName()))) + || ($t instanceof LegacyType && $t->isCollection() && null !== $collectionValueType && null !== ($className = $collectionValueType->getClassName())) ) { if (!$this->serializer instanceof DenormalizerInterface) { throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); @@ -970,7 +1203,14 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value } } - if (null !== $className = $type->getClassName()) { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } + + if ( + $t instanceof ObjectType + || ($t instanceof LegacyType && null !== $t->getClassName()) + ) { if (!$this->serializer instanceof DenormalizerInterface) { throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); } @@ -978,7 +1218,7 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value unset($context['resource_class'], $context['uri_variables']); try { - return $this->serializer->denormalize($value, $className, $format, $context); + return $this->serializer->denormalize($value, $t->getClassName(), $format, $context); } catch (NotNormalizableValueException $e) { // union/intersect types: try the next type, if not valid, an exception will be thrown at the end if ($isMultipleTypes) { @@ -996,12 +1236,17 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value // if a value is meant to be a string, float, int or a boolean value from the serialized representation. // That's why we have to transform the values, if one of these non-string basic datatypes is expected. if (\is_string($value) && (XmlEncoder::FORMAT === $format || CsvEncoder::FORMAT === $format)) { - if ('' === $value && $type->isNullable() && \in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_BOOL, Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_FLOAT], true)) { + if ('' === $value && $isNullable && ( + ($t instanceof Type && $t->isIdentifiedBy(TypeIdentifier::BOOL, TypeIdentifier::INT, TypeIdentifier::FLOAT)) + || ($t instanceof LegacyType && \in_array($t->getBuiltinType(), [LegacyType::BUILTIN_TYPE_BOOL, LegacyType::BUILTIN_TYPE_INT, LegacyType::BUILTIN_TYPE_FLOAT], true)) + )) { return null; } - switch ($type->getBuiltinType()) { - case Type::BUILTIN_TYPE_BOOL: + $typeIdentifier = $t instanceof BuiltinType ? $t->getTypeIdentifier() : TypeIdentifier::tryFrom($t->getBuiltinType()); + + switch ($typeIdentifier) { + case TypeIdentifier::BOOL: // according to http://www.w3.org/TR/xmlschema-2/#boolean, valid representations are "false", "true", "0" and "1" if ('false' === $value || '0' === $value) { $value = false; @@ -1012,10 +1257,10 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value if ($isMultipleTypes) { break 2; } - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_BOOL], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be bool ("%s" given).', $attribute, $className, $value), $value, ['bool'], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_INT: + case TypeIdentifier::INT: if (ctype_digit($value) || ('-' === $value[0] && ctype_digit(substr($value, 1)))) { $value = (int) $value; } else { @@ -1023,10 +1268,10 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value if ($isMultipleTypes) { break 2; } - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_INT], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be int ("%s" given).', $attribute, $className, $value), $value, ['int'], $context['deserialization_path'] ?? null); } break; - case Type::BUILTIN_TYPE_FLOAT: + case TypeIdentifier::FLOAT: if (is_numeric($value)) { return (float) $value; } @@ -1043,7 +1288,7 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value if ($isMultipleTypes) { break 3; } - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $className, $value), $value, [Type::BUILTIN_TYPE_FLOAT], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute for class "%s" must be float ("%s" given).', $attribute, $className, $value), $value, ['float'], $context['deserialization_path'] ?? null); } } } @@ -1053,18 +1298,27 @@ private function createAndValidateAttributeValue(string $attribute, mixed $value } try { - $this->validateType($attribute, $type, $value, $format, $context); + $t instanceof Type + ? $this->validateAttributeType($attribute, $t, $value, $format, $context) + : $this->validateType($attribute, $t, $value, $format, $context); + $denormalizationException = null; break; } catch (NotNormalizableValueException $e) { // union/intersect types: try the next type if (!$isMultipleTypes) { throw $e; } + + $denormalizationException ??= $e; } } if ($denormalizationException) { + if ($type instanceof Type && $type->isSatisfiedBy(static fn ($type) => $type instanceof BuiltinType) && !$type->isSatisfiedBy($typeIsResourceClass)) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" attribute must be "%s", "%s" given.', $attribute, $type, \gettype($value)), $value, array_map(strval(...), $types), $context['deserialization_path'] ?? null); + } + throw $denormalizationException; } diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index b469d436923..2f95391c1e3 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -45,13 +45,15 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\TypeInfo\Type; /** * @author Amrouche Hamza @@ -101,14 +103,26 @@ public function testNormalize(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'alias', 'relatedDummy', 'relatedDummies'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false)); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(true)->withWritable(false)->withReadableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); @@ -156,8 +170,15 @@ public function testNormalizeWithSecuredProperty(): void $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/secured_dummies/1'); @@ -218,23 +239,35 @@ public function testNormalizePropertyAsIriWithUriTemplate(): void ); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( - (new ApiProperty())->withReadable(true)->withUriTemplate('/property-collection-relations')->withBuiltinTypes([ - new Type('iterable', false, null, true, new Type('int', false, null, false), new Type('object', false, PropertyCollectionIriOnlyRelation::class, false)), - ]) - ); - $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'iterableIri', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( - (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations')->withBuiltinTypes([ - new Type('iterable', false, null, true, new Type('int', false, null, false), new Type('object', false, PropertyCollectionIriOnlyRelation::class, false)), - ]) - ); - - $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'toOneRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( - (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations/{id}')->withBuiltinTypes([ - new Type('object', false, PropertyCollectionIriOnlyRelation::class, false), - ]) - ); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/property-collection-relations')->withBuiltinTypes([ + new LegacyType('iterable', false, null, true, new LegacyType('int', false, null, false), new LegacyType('object', false, PropertyCollectionIriOnlyRelation::class, false)), + ]) + ); + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'iterableIri', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations')->withBuiltinTypes([ + new LegacyType('iterable', false, null, true, new LegacyType('int', false, null, false), new LegacyType('object', false, PropertyCollectionIriOnlyRelation::class, false)), + ]) + ); + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'toOneRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations/{id}')->withBuiltinTypes([ + new LegacyType('object', false, PropertyCollectionIriOnlyRelation::class, false), + ]) + ); + } else { + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/property-collection-relations')->withNativeType(Type::list(Type::object(PropertyCollectionIriOnlyRelation::class))) + ); + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'iterableIri', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations')->withNativeType(Type::iterable(Type::object(PropertyCollectionIriOnlyRelation::class))) + ); + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'toOneRelation', ['normalization_groups' => null, 'denormalization_groups' => null, 'operation_name' => null])->willReturn( + (new ApiProperty())->withReadable(true)->withUriTemplate('/parent/{parentId}/another-collection-operations/{id}')->withNativeType(Type::object(PropertyCollectionIriOnlyRelation::class)) + ); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($propertyCollectionIriOnly, UrlGeneratorInterface::ABS_URL, null, Argument::any())->willReturn('/property-collection-relations', '/parent/42/another-collection-operations'); @@ -284,8 +317,15 @@ public function testDenormalizeWithSecuredProperty(): void $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -327,8 +367,15 @@ public function testDenormalizeCreateWithDeniedPostDenormalizeSecuredProperty(): $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')->withDefault('')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')->withDefault('')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')->withDefault('')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -373,8 +420,15 @@ public function testDenormalizeUpdateWithSecuredProperty(): void $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('true')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('true')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('true')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -426,8 +480,15 @@ public function testDenormalizeUpdateWithDeniedSecuredProperty(): void $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('false')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('false')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('false')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -479,8 +540,15 @@ public function testDenormalizeUpdateWithDeniedPostDenormalizeSecuredProperty(): $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -528,12 +596,22 @@ public function testNormalizeReadableLinks(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy', 'relatedDummies'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(true)->withWritable(false)->withReadableLink(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withReadable(true)->withWritable(false)->withReadableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(true)->withWritable(false)->withReadableLink(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); @@ -587,11 +665,20 @@ public function testNormalizePolymorphicRelations(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(DummyTableInheritanceRelated::class, [])->willReturn(new PropertyNameCollection(['children'])); - $abstractDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyTableInheritance::class); - $abstractDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $abstractDummyType); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $abstractDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DummyTableInheritance::class); + $abstractDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $abstractDummyType); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withBuiltinTypes([$abstractDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withBuiltinTypes([$abstractDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + } else { + $abstractDummyType = Type::object(DummyTableInheritance::class); + $abstractDummiesType = Type::collection(Type::object(ArrayCollection::class), $abstractDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withNativeType($abstractDummiesType)->withReadable(true)->withWritable(false)->withReadableLink(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); @@ -641,14 +728,26 @@ public function testDenormalize(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getResourceFromIri('/dummies/1', Argument::type('array'))->willReturn($relatedDummy1); @@ -751,16 +850,30 @@ public function testDenormalizeWritableLinks(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', 'relatedDummy', 'relatedDummies', 'relatedDummiesWithUnionTypes'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); - $relatedDummiesWithUnionTypesIntType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); - $relatedDummiesWithUnionTypesFloatType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_FLOAT), $relatedDummyType); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummiesWithUnionTypes', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesWithUnionTypesIntType, $relatedDummiesWithUnionTypesFloatType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); + $relatedDummiesWithUnionTypesIntType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); + $relatedDummiesWithUnionTypesFloatType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT), $relatedDummyType); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummiesWithUnionTypes', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesWithUnionTypesIntType, $relatedDummiesWithUnionTypesFloatType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + $relatedDummiesWithUnionTypesIntType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + $relatedDummiesWithUnionTypesFloatType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::float()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummiesWithUnionTypes', [])->willReturn((new ApiProperty())->withNativeType(Type::union($relatedDummiesWithUnionTypesIntType, $relatedDummiesWithUnionTypesFloatType))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -771,6 +884,7 @@ public function testDenormalizeWritableLinks(): void $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(ArrayCollection::class)->willReturn(false); $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -805,9 +919,17 @@ public function testBadRelationType(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( - (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) - ); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -838,9 +960,17 @@ public function testBadRelationTypeWithExceptionToValidationErrors(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( - (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) - ); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -873,9 +1003,17 @@ public function testDeserializationPathForNotDenormalizableRelations(): void $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) - ); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class))])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true) + ); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn( + (new ApiProperty())->withNativeType(Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class)))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true) + ); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getResourceFromIri(Argument::cetera())->willThrow(new InvalidArgumentException('Invalid IRI')); @@ -891,18 +1029,9 @@ public function testDeserializationPathForNotDenormalizableRelations(): void $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 = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer { + }; + $normalizer->setSerializer($serializerProphecy->reveal()); $errors = []; @@ -928,9 +1057,17 @@ public function testInnerDocumentNotAllowed(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( - (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) - ); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -964,7 +1101,13 @@ public function testBadType(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -993,7 +1136,13 @@ public function testTypeChecksCanBeDisabled(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1026,7 +1175,13 @@ public function testJsonAllowIntAsFloat(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1075,18 +1230,28 @@ public function testDenormalizeBadKeyType(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummies'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); - - $type = new Type( - Type::BUILTIN_TYPE_OBJECT, - false, - ArrayCollection::class, - true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class) - ); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$type])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $type = new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, + false, + ArrayCollection::class, + true, + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$type])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $type = Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class), Type::int()); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($type)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1117,7 +1282,13 @@ public function testNullable(): void $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING, true)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING, true)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + } else { + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::nullable(Type::string()))->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); // @phpstan-ignore-line + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1159,18 +1330,35 @@ public function testDenormalizeBasicTypePropertiesFromXml(): void ])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float3', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNaN', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); - $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNegInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float3', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNaN', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNegInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + } else { + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue1', [])->willReturn((new ApiProperty())->withNativeType(Type::bool())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse1', [])->willReturn((new ApiProperty())->withNativeType(Type::bool())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue2', [])->willReturn((new ApiProperty())->withNativeType(Type::bool())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse2', [])->willReturn((new ApiProperty())->withNativeType(Type::bool())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int1', [])->willReturn((new ApiProperty())->withNativeType(Type::int())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int2', [])->willReturn((new ApiProperty())->withNativeType(Type::int())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float1', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float2', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float3', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNaN', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatInf', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNegInf', [])->willReturn((new ApiProperty())->withNativeType(Type::float())->withDescription('')->withReadable(false)->withWritable(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1234,11 +1422,20 @@ public function testDenormalizeCollectionDecodedFromXmlWithOneChild(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummies'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -1274,7 +1471,13 @@ public function testDenormalizePopulatingNonCloneableObject(): void $propertyNameCollectionFactoryProphecy->create(NonCloneableDummy::class, [])->willReturn(new PropertyNameCollection(['name'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + } else { + $propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); @@ -1308,7 +1511,13 @@ public function testDenormalizeObjectWithNullDisabledTypeEnforcement(): void $propertyNameCollectionFactoryProphecy->create(DtoWithNullValue::class, [])->willReturn(new PropertyNameCollection(['dummy'])); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(DtoWithNullValue::class, 'dummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, nullable: true)])->withDescription('')->withReadable(true)->withWritable(true)); + + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(DtoWithNullValue::class, 'dummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, nullable: true)])->withDescription('')->withReadable(true)->withWritable(true)); + } else { + $propertyMetadataFactoryProphecy->create(DtoWithNullValue::class, 'dummy', [])->willReturn((new ApiProperty())->withNativeType(Type::nullable(Type::object()))->withDescription('')->withReadable(true)->withWritable(true)); // @phpstan-ignore-line + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); @@ -1344,14 +1553,26 @@ public function testCacheKey(): void $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['name', 'alias', 'relatedDummy', 'relatedDummies'])); - $relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); - $relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType); - - $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false)); + // BC layer for api-platform/metadata < 4.1 + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $relatedDummyType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class); + $relatedDummiesType = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), $relatedDummyType); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false)); + } else { + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), $relatedDummyType, Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(true)->withWritable(false)->withReadableLink(false)); + } $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); diff --git a/tests/Functional/ValidationTest.php b/tests/Functional/ValidationTest.php new file mode 100644 index 00000000000..e75921f0ac5 --- /dev/null +++ b/tests/Functional/ValidationTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyWithCollectDenormalizationErrors; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; + +/** + * Tests denormalization error collection feature. + */ +final class ValidationTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = false; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [DummyWithCollectDenormalizationErrors::class, RelatedDummy::class]; + } + + public function testPostWithDenormalizationErrorsCollected(): void + { + $client = static::createClient(); + + $response = $client->request('POST', '/dummy_collect_denormalization', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'foo' => 3, + 'bar' => 'baz', + 'qux' => true, + 'uuid' => 'y', + 'relatedDummy' => 8, + 'relatedDummies' => 76, + ], + ]); + + $this->assertResponseStatusCodeSame(422); + $this->assertResponseHeaderSame('content-type', 'application/problem+json; charset=utf-8'); + + $this->assertJsonContains([ + '@context' => '/contexts/ConstraintViolation', + '@type' => 'ConstraintViolation', + 'hydra:title' => 'An error occurred', + ]); + + $content = $response->toArray(false); + $this->assertArrayHasKey('violations', $content); + $violations = $content['violations']; + $this->assertIsArray($violations); + $this->assertCount(7, $violations); + + $findViolation = static function (string $propertyPath) use ($violations): ?array { + foreach ($violations as $violation) { + if (($violation['propertyPath'] ?? null) === $propertyPath) { + return $violation; + } + } + + return null; + }; + + $violationBaz = $findViolation('baz'); + $this->assertNotNull($violationBaz, 'Violation for "baz" not found.'); + $this->assertSame('This value should be of type string.', $violationBaz['message']); + $this->assertArrayHasKey('hint', $violationBaz); + $this->assertSame('Failed to create object because the class misses the "baz" property.', $violationBaz['hint']); + + $violationQux = $findViolation('qux'); + $this->assertNotNull($violationQux); + + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->assertSame('This value should be of type string.', $violationQux['message']); + } else { + $this->assertSame('This value should be of type null|string.', $violationQux['message']); + } + + $violationFoo = $findViolation('foo'); + $this->assertNotNull($violationFoo); + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->assertSame('This value should be of type bool.', $violationFoo['message']); + } else { + $this->assertSame('This value should be of type bool|null.', $violationFoo['message']); + } + + $violationBar = $findViolation('bar'); + $this->assertNotNull($violationBar); + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->assertSame('This value should be of type int.', $violationBar['message']); + } else { + $this->assertSame('This value should be of type int|null.', $violationBar['message']); + } + + $violationUuid = $findViolation('uuid'); + $this->assertNotNull($violationUuid); + $this->assertNotNull($violationUuid); + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->assertSame('This value should be of type uuid.', $violationUuid['message']); + } else { + $this->assertSame('This value should be of type Ramsey\Uuid\UuidInterface|null.', $violationUuid['message']); + } + + $violationRelatedDummy = $findViolation('relatedDummy'); + $this->assertNotNull($violationRelatedDummy); + $this->assertSame('This value should be of type array|string.', $violationRelatedDummy['message']); + + $violationRelatedDummies = $findViolation('relatedDummies'); + $this->assertNotNull($violationRelatedDummies); + $this->assertSame('This value should be of type array.', $violationRelatedDummies['message']); + } +} From f27003ee3a5996f8e608e6f316a6f9b71128fa83 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 24 Apr 2025 10:02:16 +0200 Subject: [PATCH 2/3] refactor: isResourceClass can be simplified --- src/Elasticsearch/Util/FieldDatatypeTrait.php | 24 ++++--------------- src/Hal/Serializer/ItemNormalizer.php | 8 ++----- .../Serializer/DocumentationNormalizer.php | 17 ++++--------- src/JsonApi/JsonSchema/SchemaFactory.php | 22 ++++------------- src/JsonApi/Serializer/ItemNormalizer.php | 23 ++++-------------- .../Factory/SchemaPropertyMetadataFactory.php | 10 ++------ src/Serializer/AbstractItemNormalizer.php | 9 ++----- 7 files changed, 22 insertions(+), 91 deletions(-) diff --git a/src/Elasticsearch/Util/FieldDatatypeTrait.php b/src/Elasticsearch/Util/FieldDatatypeTrait.php index b0c1a4a57c2..25a0fe81bc8 100644 --- a/src/Elasticsearch/Util/FieldDatatypeTrait.php +++ b/src/Elasticsearch/Util/FieldDatatypeTrait.php @@ -16,13 +16,11 @@ use ApiPlatform\Metadata\Exception\PropertyNotFoundException; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\TypeHelper; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\TypeInfo\Type; -use Symfony\Component\TypeInfo\Type\CollectionType; -use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; use Symfony\Component\TypeInfo\Type\ObjectType; -use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; /** * Field datatypes helpers. @@ -108,13 +106,8 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s /** @var class-string|null $className */ $className = null; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - $type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - default => false, - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; if ($type->isSatisfiedBy($typeIsResourceClass)) { @@ -123,16 +116,7 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; } - $collectionValueTypeIsResourceClass = function (Type $type) use (&$collectionValueTypeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof CollectionType => $type->getCollectionValueType() instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getCollectionValueType()->getClassName()), - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueTypeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueTypeIsResourceClass), - default => false, - }; - }; - - if ($type->isSatisfiedBy($collectionValueTypeIsResourceClass)) { + if (TypeHelper::getCollectionValueType($type)?->isSatisfiedBy($typeIsResourceClass)) { $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index d54648c0d03..ae68b658144 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -193,12 +193,8 @@ private function getComponents(object $object, ?string $format, array $context): // prevent declaring $attribute as attribute if it's already declared as relationship $isRelationship = false; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; foreach ($types as $type) { diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 925f66db81e..ff6fca7bb19 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -414,12 +414,8 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null /** @var class-string|null $className */ $className = null; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) { @@ -515,13 +511,8 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool return false; } - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool { - return match (true) { - $type instanceof CollectionType => false, - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName()), - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; return $nativeType->isSatisfiedBy($typeIsResourceClass); diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index 8602cc7c442..312e64704b6 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -22,13 +22,12 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\TypeHelper; use ApiPlatform\State\ApiResource\Error; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\TypeInfo\Type; -use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; use Symfony\Component\TypeInfo\Type\ObjectType; -use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; /** * Decorator factory which adds JSON:API properties to the JSON Schema document. @@ -331,25 +330,12 @@ private function getRelationship(string $resourceClass, string $property, ?array /** @var class-string|null $className */ $className = null; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - }; - }; - - $collectionValueIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool { - return match (true) { - $type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass), - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => false, - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { - if ($t->isSatisfiedBy($collectionValueIsResourceClass)) { + if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) { $isMany = true; } elseif ($t->isSatisfiedBy($typeIsResourceClass)) { $isOne = true; diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index 7e274efeca4..179462f57f2 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -23,6 +23,7 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\TypeHelper; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ContextTrait; @@ -38,10 +39,8 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\TypeInfo\Type; -use Symfony\Component\TypeInfo\Type\CollectionType; use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; use Symfony\Component\TypeInfo\Type\ObjectType; -use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; /** * Converts between objects and array. @@ -376,28 +375,14 @@ private function getComponents(object $object, ?string $format, array $context): /** @var class-string|null $className */ $className = null; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => false, - }; - }; - - $collectionValueIsResourceClass = function (Type $type) use ($typeIsResourceClass, &$collectionValueIsResourceClass): bool { - return match (true) { - $type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass), - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueIsResourceClass), - default => false, - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { $isOne = $isMany = false; - if ($t->isSatisfiedBy($collectionValueIsResourceClass)) { + if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) { $isMany = true; } elseif ($t->isSatisfiedBy($typeIsResourceClass)) { $isOne = true; diff --git a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php index 4ca1f46fb56..d7ee5b847d8 100644 --- a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php +++ b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php @@ -26,7 +26,6 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\CollectionType; -use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; use Symfony\Component\TypeInfo\Type\IntersectionType; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\UnionType; @@ -107,13 +106,8 @@ private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySch { $type = $propertyMetadata->getNativeType(); - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool { - return match (true) { - $type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass), - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - default => $type instanceof ObjectType && $this->isResourceClass($type->getClassName()), - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && !$type?->isSatisfiedBy($typeIsResourceClass)) { diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index b6527ddcf58..18af71756c1 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -881,13 +881,8 @@ protected function getAttributeValue(object $object, string $attribute, ?string // TODO check every foreach composite to see if null is an issue $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]); $className = null; - $typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool { - return match (true) { - $type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass), - $type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass), - $type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()), - default => false, - }; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); }; foreach ($types as $type) { From 568a3834aef1f8a6447030eae506ef3783049bb8 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 24 Apr 2025 14:18:26 +0200 Subject: [PATCH 3/3] skip mongoodb --- tests/Functional/ValidationTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Functional/ValidationTest.php b/tests/Functional/ValidationTest.php index e75921f0ac5..8f6adabddbd 100644 --- a/tests/Functional/ValidationTest.php +++ b/tests/Functional/ValidationTest.php @@ -38,6 +38,11 @@ public static function getResources(): array public function testPostWithDenormalizationErrorsCollected(): void { + $container = static::getContainer(); + if ('mongodb' === $container->getParameter('kernel.environment')) { + $this->markTestSkipped(); + } + $client = static::createClient(); $response = $client->request('POST', '/dummy_collect_denormalization', [