Skip to content

Commit 99cfee3

Browse files
committed
feat(metadata): use TypeInfo's Type
1 parent ad54075 commit 99cfee3

23 files changed

+383
-134
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"symfony/property-info": "^6.4 || ^7.0",
115115
"symfony/serializer": "^6.4 || ^7.0",
116116
"symfony/translation-contracts": "^3.3",
117+
"symfony/type-info": "^7.2",
117118
"symfony/web-link": "^6.4 || ^7.0",
118119
"willdurand/negotiation": "^3.1"
119120
},

src/Metadata/ApiProperty.php

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313

1414
namespace ApiPlatform\Metadata;
1515

16-
use Symfony\Component\PropertyInfo\Type;
16+
use ApiPlatform\Metadata\Util\PropertyInfoToTypeInfoHelper;
17+
use Symfony\Component\PropertyInfo\Type as LegacyType;
1718
use Symfony\Component\Serializer\Attribute\Context;
1819
use Symfony\Component\Serializer\Attribute\Groups;
1920
use Symfony\Component\Serializer\Attribute\Ignore;
2021
use Symfony\Component\Serializer\Attribute\MaxDepth;
2122
use Symfony\Component\Serializer\Attribute\SerializedName;
2223
use Symfony\Component\Serializer\Attribute\SerializedPath;
24+
use Symfony\Component\TypeInfo\Type;
2325

2426
/**
2527
* ApiProperty annotation.
@@ -31,6 +33,15 @@ final class ApiProperty
3133
{
3234
private ?array $types;
3335
private ?array $serialize;
36+
private ?Type $phpType;
37+
38+
/**
39+
* Used to know if only legacy types are defined without triggering deprecation.
40+
* To be removed in 5.0.
41+
*
42+
* @internal
43+
*/
44+
public bool $usesLegacyType = false;
3445

3546
/**
3647
* @param bool|null $readableLink https://api-platform.com/docs/core/serialization/#force-iri-with-relations-of-the-same-type-parentchilds-relations
@@ -47,10 +58,11 @@ final class ApiProperty
4758
* @param string|\Stringable|null $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
4859
* @param string[] $types the RDF types of this property
4960
* @param string[] $iris
50-
* @param Type[] $builtinTypes
61+
* @param LegacyType[] $builtinTypes
5162
* @param string|null $uriTemplate (experimental) whether to return the subRessource collection IRI instead of an iterable of IRI
5263
* @param string|null $property The property name
5364
* @param Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|array<array-key, Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth> $serialize Serializer attributes
65+
* @param Type $phpType The internal PHP type
5466
*/
5567
public function __construct(
5668
private ?string $description = null,
@@ -206,6 +218,8 @@ public function __construct(
206218
array|string|null $types = null,
207219
/*
208220
* The related php types.
221+
*
222+
* deprecated since 4.1, use "phpType" instead.
209223
*/
210224
private ?array $builtinTypes = null,
211225
private ?array $schema = null,
@@ -221,9 +235,23 @@ public function __construct(
221235
*/
222236
private ?bool $hydra = null,
223237
private array $extraProperties = [],
238+
?Type $phpType = null,
224239
) {
225240
$this->types = \is_string($types) ? (array) $types : $types;
226241
$this->serialize = \is_array($serialize) ? $serialize : [$serialize];
242+
$this->phpType = $phpType;
243+
244+
if ($this->builtinTypes) {
245+
// trigger_deprecation('api_platform/metadata', '4.1', 'The "builtinTypes" argument of "%s()" is deprecated, use "phpType" instead.');
246+
247+
$this->usesLegacyType = true;
248+
249+
if (!$this->phpType) {
250+
$this->phpType = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($this->builtinTypes);
251+
}
252+
} elseif ($this->phpType) {
253+
$this->builtinTypes = PropertyInfoToTypeInfoHelper::convertTypeToLegacyTypes($this->phpType);
254+
}
227255
}
228256

229257
public function getProperty(): ?string
@@ -490,20 +518,43 @@ public function withTypes(array|string $types = []): static
490518
}
491519

492520
/**
493-
* @return Type[]
521+
* deprecated since 4.1, use "getPhpType" instead.
522+
*
523+
* @return LegacyType[]
494524
*/
495525
public function getBuiltinTypes(): ?array
496526
{
527+
// trigger_deprecation('api-platform/metadata', '4.1', 'The "%s()" method is deprecated, use "%s::getPhpType()" instead.', __METHOD__, self::class);
528+
497529
return $this->builtinTypes;
498530
}
499531

500532
/**
501-
* @param Type[] $builtinTypes
533+
* deprecated since 4.1, use "withPhpType" instead.
534+
*
535+
* @param LegacyType[] $builtinTypes
502536
*/
503537
public function withBuiltinTypes(array $builtinTypes = []): static
504538
{
539+
// trigger_deprecation('api-platform/metadata', '4.1', 'The "%s()" method is deprecated, use "%s::withPhpType()" instead.', __METHOD__, self::class);
540+
505541
$self = clone $this;
506542
$self->builtinTypes = $builtinTypes;
543+
$self->phpType = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($builtinTypes);
544+
545+
return $self;
546+
}
547+
548+
public function getPhpType(): ?Type
549+
{
550+
return $this->phpType;
551+
}
552+
553+
public function withPhpType(?Type $phpType): self
554+
{
555+
$self = clone $this;
556+
$self->phpType = $phpType;
557+
$self->builtinTypes = PropertyInfoToTypeInfoHelper::convertTypeToLegacyTypes($phpType);
507558

508559
return $self;
509560
}

src/Metadata/Extractor/XmlPropertyExtractor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ protected function extractPath(string $path): void
7474
'genId' => $this->phpize($property, 'genId', 'bool'),
7575
'uriTemplate' => $this->phpize($property, 'uriTemplate', 'string'),
7676
'property' => $this->phpize($property, 'property', 'string'),
77+
'phpType' => $this->phpize($property, 'phpType', 'string'),
7778
];
7879
}
7980
}

src/Metadata/Extractor/YamlPropertyExtractor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private function buildProperties(array $resourcesYaml): void
9595
'genId' => $this->phpize($propertyValues, 'genId', 'bool'),
9696
'uriTemplate' => $this->phpize($propertyValues, 'uriTemplate', 'string'),
9797
'property' => $this->phpize($propertyValues, 'property', 'string'),
98+
'phpType' => $this->phpize($propertyValues, 'phpType', 'string'),
9899
];
99100
}
100101
}

src/Metadata/Extractor/schema/properties.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<xsd:attribute name="property" type="xsd:string"/>
4848
<xsd:attribute name="uriTemplate" type="xsd:string"/>
4949
<xsd:attribute name="hydra" type="xsd:boolean"/>
50+
<xsd:attribute name="phpType" type="xsd:string"/>
5051
</xsd:complexType>
5152

5253
<xsd:complexType name="types">

src/Metadata/IdentifiersExtractor.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
2424
use Symfony\Component\PropertyAccess\PropertyAccess;
2525
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
26+
use Symfony\Component\TypeInfo\Type;
27+
use Symfony\Component\TypeInfo\Type\CollectionType;
28+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
29+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
2630

2731
/**
2832
* {@inheritdoc}
@@ -110,21 +114,25 @@ private function getIdentifierValue(object $item, string $class, string $propert
110114
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
111115
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
112116

113-
$types = $propertyMetadata->getBuiltinTypes();
114-
if (null === ($type = $types[0] ?? null)) {
117+
if (null === $type = $propertyMetadata->getPhpType()) {
115118
continue;
116119
}
117120

118-
try {
119-
if ($type->isCollection()) {
120-
$collectionValueType = $type->getCollectionValueTypes()[0] ?? null;
121+
$collectionValueIsIdentifiedByClass = function (Type $type) use (&$collectionValueIsIdentifiedByClass, $class): bool {
122+
return match (true) {
123+
$type instanceof CollectionType => $type->getCollectionValueType()->isIdentifiedBy($class),
124+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($collectionValueIsIdentifiedByClass),
125+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($collectionValueIsIdentifiedByClass),
126+
default => false,
127+
};
128+
};
121129

122-
if (null !== $collectionValueType && $collectionValueType->getClassName() === $class) {
123-
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, \sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
124-
}
130+
try {
131+
if ($type->isSatisfiedBy($collectionValueIsIdentifiedByClass)) {
132+
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, \sprintf('%s[0].%s', $propertyName, $property)), $parameterName);
125133
}
126134

127-
if ($type->getClassName() === $class) {
135+
if ($type->isIdentifiedBy($class)) {
128136
return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName);
129137
}
130138
} catch (NoSuchPropertyException $e) {

src/Metadata/Property/Factory/AttributePropertyMetadataFactory.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,23 @@ private function createMetadata(ApiProperty $attribute, ?ApiProperty $propertyMe
121121
}
122122

123123
foreach (get_class_methods(ApiProperty::class) as $method) {
124-
if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches) && null !== $val = $attribute->{$method}()) {
125-
$propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val);
124+
if (preg_match('/^(?:get|is)(.*)/', (string) $method, $matches)) {
125+
// BC layer, to remove in 5.0
126+
if ('getBuiltinTypes' === $method) {
127+
if (!$attribute->usesLegacyType) {
128+
continue;
129+
}
130+
131+
if ($builtinTypes = $attribute->getBuiltinTypes()) {
132+
$propertyMetadata = $propertyMetadata->withBuiltinTypes($builtinTypes);
133+
}
134+
135+
continue;
136+
}
137+
138+
if (null !== $val = $attribute->{$method}()) {
139+
$propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val);
140+
}
126141
}
127142
}
128143

src/Metadata/Property/Factory/ExtractorPropertyMetadataFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
use ApiPlatform\Metadata\ApiProperty;
1818
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
1919
use ApiPlatform\Metadata\Extractor\PropertyExtractorInterface;
20-
use Symfony\Component\PropertyInfo\Type;
20+
use Symfony\Component\PropertyInfo\Type as LegacyType;
21+
use Symfony\Component\TypeInfo\Type;
2122

2223
/**
2324
* Creates properties's metadata using an extractor.
@@ -60,7 +61,9 @@ public function create(string $resourceClass, string $property, array $options =
6061

6162
foreach ($propertyMetadata as $key => $value) {
6263
if ('builtinTypes' === $key && null !== $value) {
63-
$value = array_map(fn (string $builtinType): Type => new Type($builtinType), $value);
64+
$value = array_map(static fn (string $builtinType): LegacyType => new LegacyType($builtinType), $value);
65+
} elseif ('phpType' === $key && null !== $value) {
66+
$value = Type::builtin($value);
6467
}
6568

6669
$methodName = 'with'.ucfirst($key);

src/Metadata/Property/Factory/PropertyInfoPropertyMetadataFactory.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515

1616
use ApiPlatform\Metadata\ApiProperty;
1717
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
18-
use Doctrine\Common\Collections\ArrayCollection;
1918
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
20-
use Symfony\Component\PropertyInfo\Type;
2119

2220
/**
2321
* PropertyInfo metadata loader decorator.
@@ -45,17 +43,8 @@ public function create(string $resourceClass, string $property, array $options =
4543
}
4644
}
4745

48-
if (!$propertyMetadata->getBuiltinTypes()) {
49-
$types = $this->propertyInfo->getTypes($resourceClass, $property, $options) ?? [];
50-
51-
foreach ($types as $i => $type) {
52-
// Temp fix for https://github.com/symfony/symfony/pull/52699
53-
if (ArrayCollection::class === $type->getClassName()) {
54-
$types[$i] = new Type($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes());
55-
}
56-
}
57-
58-
$propertyMetadata = $propertyMetadata->withBuiltinTypes($types);
46+
if (!$propertyMetadata->getPhpType()) {
47+
$propertyMetadata = $propertyMetadata->withPhpType($this->propertyInfo->getType($resourceClass, $property, $options));
5948
}
6049

6150
if (null === $propertyMetadata->getDescription() && null !== $description = $this->propertyInfo->getShortDescription($resourceClass, $property, $options)) {

src/Metadata/Property/Factory/SerializerPropertyMetadataFactory.php

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
use ApiPlatform\Metadata\Util\ResourceClassInfoTrait;
2020
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
2121
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface as SerializerClassMetadataFactoryInterface;
22+
use Symfony\Component\TypeInfo\Type;
23+
use Symfony\Component\TypeInfo\Type\CollectionType;
24+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
25+
use Symfony\Component\TypeInfo\Type\ObjectType;
26+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
2227

2328
/**
2429
* Populates read/write and link status using serialization groups.
@@ -60,17 +65,24 @@ public function create(string $resourceClass, string $property, array $options =
6065
}
6166

6267
$propertyMetadata = $this->transformReadWrite($propertyMetadata, $resourceClass, $property, $normalizationGroups, $denormalizationGroups, $ignoredAttributes);
63-
$types = $propertyMetadata->getBuiltinTypes() ?? [];
6468

65-
if (!$this->isResourceClass($resourceClass) && $types) {
66-
foreach ($types as $builtinType) {
67-
if ($builtinType->isCollection()) {
68-
return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
69-
}
69+
$type = $propertyMetadata->getPhpType();
70+
if ($type && !$this->isResourceClass($resourceClass)) {
71+
$typeIsCollection = static function (Type $type) use (&$typeIsCollection): bool {
72+
return match (true) {
73+
$type instanceof CollectionType => true,
74+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsCollection),
75+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsCollection),
76+
default => false,
77+
};
78+
};
79+
80+
if ($type->isSatisfiedBy($typeIsCollection)) {
81+
return $propertyMetadata->withReadableLink(true)->withWritableLink(true);
7082
}
7183
}
7284

73-
return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups, $types);
85+
return $this->transformLinkStatus($propertyMetadata, $normalizationGroups, $denormalizationGroups, $type);
7486
}
7587

7688
/**
@@ -112,45 +124,47 @@ private function transformReadWrite(ApiProperty $propertyMetadata, string $resou
112124
* @param string[]|null $normalizationGroups
113125
* @param string[]|null $denormalizationGroups
114126
*/
115-
private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?array $types = null): ApiProperty
127+
private function transformLinkStatus(ApiProperty $propertyMetadata, ?array $normalizationGroups = null, ?array $denormalizationGroups = null, ?Type $type = null): ApiProperty
116128
{
117129
// No need to check link status if property is not readable and not writable
118130
if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) {
119131
return $propertyMetadata;
120132
}
121133

122-
foreach ($types as $type) {
123-
if (
124-
$type->isCollection()
125-
&& $collectionValueType = $type->getCollectionValueTypes()[0] ?? null
126-
) {
127-
$relatedClass = $collectionValueType->getClassName();
128-
} else {
129-
$relatedClass = $type->getClassName();
130-
}
131-
132-
// if property is not a resource relation, don't set link status (as it would have no meaning)
133-
if (null === $relatedClass || !$this->isResourceClass($relatedClass)) {
134-
continue;
135-
}
134+
if (!$type) {
135+
return $propertyMetadata;
136+
}
136137

137-
// find the resource class
138-
// this prevents serializer groups on non-resource child class from incorrectly influencing the decision
139-
if (null !== $this->resourceClassResolver) {
140-
$relatedClass = $this->resourceClassResolver->getResourceClass(null, $relatedClass);
141-
}
138+
/** @var class-string|null $className */
139+
$className = null;
140+
$typeIsResourceClass = function (Type $type) use (&$typeIsResourceClass, &$className): bool {
141+
return match (true) {
142+
$type instanceof CollectionType => $type->getCollectionValueType()->isSatisfiedBy($typeIsResourceClass),
143+
$type instanceof WrappingTypeInterface => $type->wrappedTypeIsSatisfiedBy($typeIsResourceClass),
144+
$type instanceof CompositeTypeInterface => $type->composedTypesAreSatisfiedBy($typeIsResourceClass),
145+
default => $type instanceof ObjectType && $this->isResourceClass($className = $type->getClassName()),
146+
};
147+
};
148+
149+
// if property is not a resource relation, don't set link status (as it would have no meaning)
150+
if (!$type->isSatisfiedBy($typeIsResourceClass)) {
151+
return $propertyMetadata;
152+
}
142153

143-
$relatedGroups = $this->getClassSerializerGroups($relatedClass);
154+
// find the resource class
155+
// this prevents serializer groups on non-resource child class from incorrectly influencing the decision
156+
if (null !== $this->resourceClassResolver) {
157+
$className = $this->resourceClassResolver->getResourceClass(null, $className);
158+
}
144159

145-
if (null === $propertyMetadata->isReadableLink()) {
146-
$propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
147-
}
160+
$relatedGroups = $this->getClassSerializerGroups($className);
148161

149-
if (null === $propertyMetadata->isWritableLink()) {
150-
$propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
151-
}
162+
if (null === $propertyMetadata->isReadableLink()) {
163+
$propertyMetadata = $propertyMetadata->withReadableLink(null !== $normalizationGroups && !empty(array_intersect($normalizationGroups, $relatedGroups)));
164+
}
152165

153-
return $propertyMetadata;
166+
if (null === $propertyMetadata->isWritableLink()) {
167+
$propertyMetadata = $propertyMetadata->withWritableLink(null !== $denormalizationGroups && !empty(array_intersect($denormalizationGroups, $relatedGroups)));
154168
}
155169

156170
return $propertyMetadata;

0 commit comments

Comments
 (0)