Skip to content

Commit 7c796de

Browse files
soyukamtarld
andauthored
feat(serializer): type info (#7104)
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent a3e5e53 commit 7c796de

File tree

10 files changed

+805
-333
lines changed

10 files changed

+805
-333
lines changed

features/main/validation.feature

-67
Original file line numberDiff line numberDiff line change
@@ -87,74 +87,7 @@ Feature: Using validations groups
8787
And the JSON node "detail" should be equal to "test: This value should not be null."
8888
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
8989

90-
@!mongodb
9190
@createSchema
92-
Scenario: Create a resource with collectDenormalizationErrors
93-
When I add "Content-type" header equal to "application/ld+json"
94-
And I send a "POST" request to "/dummy_collect_denormalization" with body:
95-
"""
96-
{
97-
"foo": 3,
98-
"bar": "baz",
99-
"qux": true,
100-
"uuid": "y",
101-
"relatedDummy": 8,
102-
"relatedDummies": 76
103-
}
104-
"""
105-
Then the response status code should be 422
106-
And the response should be in JSON
107-
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
108-
And the JSON should be a superset of:
109-
"""
110-
{
111-
"@context": "/contexts/ConstraintViolation",
112-
"@type": "ConstraintViolation",
113-
"hydra:title": "An error occurred",
114-
"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.",
115-
"violations": [
116-
{
117-
"propertyPath": "baz",
118-
"message": "This value should be of type string.",
119-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
120-
"hint": "Failed to create object because the class misses the \"baz\" property."
121-
},
122-
{
123-
"propertyPath": "qux",
124-
"message": "This value should be of type string.",
125-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
126-
},
127-
{
128-
"propertyPath": "foo",
129-
"message": "This value should be of type bool.",
130-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
131-
},
132-
{
133-
"propertyPath": "bar",
134-
"message": "This value should be of type int.",
135-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
136-
},
137-
{
138-
"propertyPath": "uuid",
139-
"message": "This value should be of type uuid.",
140-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
141-
"hint": "Invalid UUID string: y"
142-
},
143-
{
144-
"propertyPath": "relatedDummy",
145-
"message": "This value should be of type array|string.",
146-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40",
147-
"hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given."
148-
},
149-
{
150-
"propertyPath": "relatedDummies",
151-
"message": "This value should be of type array.",
152-
"code": "ba785a8c-82cb-4283-967c-3cf342181b40"
153-
}
154-
]
155-
}
156-
"""
157-
15891
@!mongodb
15992
Scenario: Get violations constraints
16093
When I add "Accept" header equal to "application/json"

src/Elasticsearch/Util/FieldDatatypeTrait.php

+4-20
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
1717
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1818
use ApiPlatform\Metadata\ResourceClassResolverInterface;
19+
use ApiPlatform\Metadata\Util\TypeHelper;
1920
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
2021
use Symfony\Component\PropertyInfo\Type as LegacyType;
2122
use Symfony\Component\TypeInfo\Type;
22-
use Symfony\Component\TypeInfo\Type\CollectionType;
23-
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
2423
use Symfony\Component\TypeInfo\Type\ObjectType;
25-
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
2624

2725
/**
2826
* Field datatypes helpers.
@@ -108,13 +106,8 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s
108106
/** @var class-string|null $className */
109107
$className = null;
110108

111-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
112-
return match (true) {
113-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
114-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
115-
$type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
116-
default => false,
117-
};
109+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
110+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
118111
};
119112

120113
if ($type->isSatisfiedBy($typeIsResourceClass)) {
@@ -123,16 +116,7 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s
123116
return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath";
124117
}
125118

126-
$collectionValueTypeIsResourceClass = function (Type $type) use (&$collectionValueTypeIsResourceClass, &$className): bool {
127-
return match (true) {
128-
$type instanceof CollectionType => $type->getCollectionValueType() instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getCollectionValueType()->getClassName()),
129-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueTypeIsResourceClass),
130-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueTypeIsResourceClass),
131-
default => false,
132-
};
133-
};
134-
135-
if ($type->isSatisfiedBy($collectionValueTypeIsResourceClass)) {
119+
if (TypeHelper::getCollectionValueType($type)?->isSatisfiedBy($typeIsResourceClass)) {
136120
$nestedPath = $this->getNestedFieldPath($className, implode('.', $properties));
137121

138122
return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath";

src/Hal/Serializer/ItemNormalizer.php

+2-6
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,8 @@ private function getComponents(object $object, ?string $format, array $context):
193193

194194
// prevent declaring $attribute as attribute if it's already declared as relationship
195195
$isRelationship = false;
196-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
197-
return match (true) {
198-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
199-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
200-
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
201-
};
196+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
197+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
202198
};
203199

204200
foreach ($types as $type) {

src/Hydra/Serializer/DocumentationNormalizer.php

+4-13
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,8 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null
414414
/** @var class-string|null $className */
415415
$className = null;
416416

417-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
418-
return match (true) {
419-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
420-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
421-
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
422-
};
417+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
418+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
423419
};
424420

425421
if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) {
@@ -515,13 +511,8 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool
515511
return false;
516512
}
517513

518-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
519-
return match (true) {
520-
$type instanceof CollectionType => false,
521-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
522-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
523-
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($type->getClassName()),
524-
};
514+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
515+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
525516
};
526517

527518
return $nativeType->isSatisfiedBy($typeIsResourceClass);

src/JsonApi/JsonSchema/SchemaFactory.php

+4-18
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@
2222
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2323
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2424
use ApiPlatform\Metadata\ResourceClassResolverInterface;
25+
use ApiPlatform\Metadata\Util\TypeHelper;
2526
use ApiPlatform\State\ApiResource\Error;
2627
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
2728
use Symfony\Component\TypeInfo\Type;
28-
use Symfony\Component\TypeInfo\Type\CollectionType;
2929
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
3030
use Symfony\Component\TypeInfo\Type\ObjectType;
31-
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
3231

3332
/**
3433
* Decorator factory which adds JSON:API properties to the JSON Schema document.
@@ -331,25 +330,12 @@ private function getRelationship(string $resourceClass, string $property, ?array
331330
/** @var class-string|null $className */
332331
$className = null;
333332

334-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
335-
return match (true) {
336-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
337-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
338-
default => $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
339-
};
340-
};
341-
342-
$collectionValueIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
343-
return match (true) {
344-
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
345-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
346-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
347-
default => false,
348-
};
333+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
334+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
349335
};
350336

351337
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
352-
if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
338+
if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) {
353339
$isMany = true;
354340
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
355341
$isOne = true;

src/JsonApi/Serializer/ItemNormalizer.php

+4-19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2424
use ApiPlatform\Metadata\UrlGeneratorInterface;
2525
use ApiPlatform\Metadata\Util\ClassInfoTrait;
26+
use ApiPlatform\Metadata\Util\TypeHelper;
2627
use ApiPlatform\Serializer\AbstractItemNormalizer;
2728
use ApiPlatform\Serializer\CacheKeyTrait;
2829
use ApiPlatform\Serializer\ContextTrait;
@@ -38,10 +39,8 @@
3839
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
3940
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
4041
use Symfony\Component\TypeInfo\Type;
41-
use Symfony\Component\TypeInfo\Type\CollectionType;
4242
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
4343
use Symfony\Component\TypeInfo\Type\ObjectType;
44-
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
4544

4645
/**
4746
* Converts between objects and array.
@@ -376,28 +375,14 @@ private function getComponents(object $object, ?string $format, array $context):
376375
/** @var class-string|null $className */
377376
$className = null;
378377

379-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
380-
return match (true) {
381-
$type instanceof ObjectType => $this->resourceClassResolver->isResourceClass($className = $type->getClassName()),
382-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
383-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
384-
default => false,
385-
};
386-
};
387-
388-
$collectionValueIsResourceClass = function (Type $type) use ($typeIsResourceClass, &$collectionValueIsResourceClass): bool {
389-
return match (true) {
390-
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
391-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueIsResourceClass),
392-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueIsResourceClass),
393-
default => false,
394-
};
378+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
379+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
395380
};
396381

397382
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
398383
$isOne = $isMany = false;
399384

400-
if ($t->isSatisfiedBy($collectionValueIsResourceClass)) {
385+
if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) {
401386
$isMany = true;
402387
} elseif ($t->isSatisfiedBy($typeIsResourceClass)) {
403388
$isOne = true;

src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php

+2-8
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
use Symfony\Component\TypeInfo\Type;
2727
use Symfony\Component\TypeInfo\Type\BuiltinType;
2828
use Symfony\Component\TypeInfo\Type\CollectionType;
29-
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
3029
use Symfony\Component\TypeInfo\Type\IntersectionType;
3130
use Symfony\Component\TypeInfo\Type\ObjectType;
3231
use Symfony\Component\TypeInfo\Type\UnionType;
@@ -107,13 +106,8 @@ private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySch
107106
{
108107
$type = $propertyMetadata->getNativeType();
109108

110-
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass): bool {
111-
return match (true) {
112-
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
113-
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
114-
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
115-
default => $type instanceof ObjectType && $this->isResourceClass($type->getClassName()),
116-
};
109+
$typeIsResourceClass = function (Type $type) use (&$className): bool {
110+
return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
117111
};
118112

119113
if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && !$type?->isSatisfiedBy($typeIsResourceClass)) {

0 commit comments

Comments
 (0)