Skip to content

Commit 827e1f7

Browse files
soyukamtarld
andauthored
feat(elasticsearch): use TypeInfo type (#7098)
Co-authored-by: Mathias Arlaud <mathias.arlaud@gmail.com>
1 parent 1d579d0 commit 827e1f7

9 files changed

+285
-101
lines changed

src/Elasticsearch/Filter/AbstractFilter.php

+112-4
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2020
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2121
use ApiPlatform\Metadata\ResourceClassResolverInterface;
22-
use Symfony\Component\PropertyInfo\Type;
22+
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
23+
use Symfony\Component\PropertyInfo\Type as LegacyType;
2324
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
25+
use Symfony\Component\TypeInfo\Type;
26+
use Symfony\Component\TypeInfo\Type\CollectionType;
27+
use Symfony\Component\TypeInfo\Type\CompositeTypeInterface;
28+
use Symfony\Component\TypeInfo\Type\ObjectType;
29+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
2430

2531
/**
2632
* Abstract class with helpers for easing the implementation of a filter.
@@ -31,7 +37,9 @@
3137
*/
3238
abstract class AbstractFilter implements FilterInterface
3339
{
34-
use FieldDatatypeTrait { getNestedFieldPath as protected; }
40+
use FieldDatatypeTrait {
41+
getNestedFieldPath as protected;
42+
}
3543

3644
public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, protected ?NameConverterInterface $nameConverter = null, protected ?array $properties = null)
3745
{
@@ -70,8 +78,108 @@ protected function hasProperty(string $resourceClass, string $property): bool
7078
* - is the decomposed given property an association?
7179
* - the resource class of the decomposed given property
7280
* - the property name of the decomposed given property
81+
*
82+
* @return array{0: ?Type, 1: ?bool, 2: ?class-string, 3: ?string}
7383
*/
7484
protected function getMetadata(string $resourceClass, string $property): array
85+
{
86+
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
87+
return $this->getLegacyMetadata($resourceClass, $property);
88+
}
89+
90+
$noop = [null, null, null, null];
91+
92+
if (!$this->hasProperty($resourceClass, $property)) {
93+
return $noop;
94+
}
95+
96+
$properties = explode('.', $property);
97+
$totalProperties = \count($properties);
98+
$currentResourceClass = $resourceClass;
99+
$hasAssociation = false;
100+
$currentProperty = null;
101+
$type = null;
102+
103+
foreach ($properties as $index => $currentProperty) {
104+
try {
105+
$propertyMetadata = $this->propertyMetadataFactory->create($currentResourceClass, $currentProperty);
106+
} catch (PropertyNotFoundException) {
107+
return $noop;
108+
}
109+
110+
// check each type before deciding if it's noop or not
111+
// e.g: maybe the first type is noop, but the second is valid
112+
$isNoop = false;
113+
114+
++$index;
115+
116+
$type = $propertyMetadata->getNativeType();
117+
118+
if (null === $type) {
119+
return $noop;
120+
}
121+
122+
foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) {
123+
$builtinType = $t;
124+
125+
while ($builtinType instanceof WrappingTypeInterface) {
126+
$builtinType = $builtinType->getWrappedType();
127+
}
128+
129+
if (!$builtinType instanceof ObjectType && !$t instanceof CollectionType) {
130+
if ($totalProperties === $index) {
131+
break 2;
132+
}
133+
134+
$isNoop = true;
135+
136+
continue;
137+
}
138+
139+
if ($t instanceof CollectionType) {
140+
$t = $t->getCollectionValueType();
141+
$builtinType = $t;
142+
143+
while ($builtinType instanceof WrappingTypeInterface) {
144+
$builtinType = $builtinType->getWrappedType();
145+
}
146+
147+
if (!$builtinType instanceof ObjectType) {
148+
if ($totalProperties === $index) {
149+
break 2;
150+
}
151+
152+
$isNoop = true;
153+
154+
continue;
155+
}
156+
}
157+
158+
$className = $builtinType->getClassName();
159+
160+
if ($isResourceClass = $this->resourceClassResolver->isResourceClass($className)) {
161+
$currentResourceClass = $className;
162+
} elseif ($totalProperties !== $index) {
163+
$isNoop = true;
164+
165+
continue;
166+
}
167+
168+
$hasAssociation = $totalProperties === $index && $isResourceClass;
169+
$isNoop = false;
170+
171+
break;
172+
}
173+
}
174+
175+
if ($isNoop) {
176+
return $noop;
177+
}
178+
179+
return [$type, $hasAssociation, $currentResourceClass, $currentProperty];
180+
}
181+
182+
protected function getLegacyMetadata(string $resourceClass, string $property): array
75183
{
76184
$noop = [null, null, null, null];
77185

@@ -108,7 +216,7 @@ protected function getMetadata(string $resourceClass, string $property): array
108216
foreach ($types as $type) {
109217
$builtinType = $type->getBuiltinType();
110218

111-
if (Type::BUILTIN_TYPE_OBJECT !== $builtinType && Type::BUILTIN_TYPE_ARRAY !== $builtinType) {
219+
if (LegacyType::BUILTIN_TYPE_OBJECT !== $builtinType && LegacyType::BUILTIN_TYPE_ARRAY !== $builtinType) {
112220
if ($totalProperties === $index) {
113221
break 2;
114222
}
@@ -124,7 +232,7 @@ protected function getMetadata(string $resourceClass, string $property): array
124232
continue;
125233
}
126234

127-
if (Type::BUILTIN_TYPE_ARRAY === $builtinType && Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) {
235+
if (LegacyType::BUILTIN_TYPE_ARRAY === $builtinType && LegacyType::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) {
128236
if ($totalProperties === $index) {
129237
break 2;
130238
}

src/Elasticsearch/Filter/AbstractSearchFilter.php

+51-24
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2222
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2323
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
24-
use Symfony\Component\PropertyInfo\Type;
24+
use Symfony\Component\PropertyInfo\Type as LegacyType;
2525
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26+
use Symfony\Component\TypeInfo\Type;
27+
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
28+
use Symfony\Component\TypeInfo\TypeIdentifier;
2629

2730
/**
2831
* Abstract class with helpers for easing the implementation of a search filter like a term filter or a match filter.
@@ -109,27 +112,40 @@ public function getDescription(string $resourceClass): array
109112
*/
110113
abstract protected function getQuery(string $property, array $values, ?string $nestedPath): array;
111114

112-
/**
113-
* Converts the given {@see Type} in PHP type.
114-
*/
115-
protected function getPhpType(Type $type): string
115+
protected function getPhpType(LegacyType|Type $type): string
116116
{
117-
switch ($builtinType = $type->getBuiltinType()) {
118-
case Type::BUILTIN_TYPE_ARRAY:
119-
case Type::BUILTIN_TYPE_INT:
120-
case Type::BUILTIN_TYPE_FLOAT:
121-
case Type::BUILTIN_TYPE_BOOL:
122-
case Type::BUILTIN_TYPE_STRING:
123-
return $builtinType;
124-
case Type::BUILTIN_TYPE_OBJECT:
125-
if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) {
126-
return \DateTimeInterface::class;
127-
}
117+
if ($type instanceof LegacyType) {
118+
switch ($builtinType = $type->getBuiltinType()) {
119+
case LegacyType::BUILTIN_TYPE_ARRAY:
120+
case LegacyType::BUILTIN_TYPE_INT:
121+
case LegacyType::BUILTIN_TYPE_FLOAT:
122+
case LegacyType::BUILTIN_TYPE_BOOL:
123+
case LegacyType::BUILTIN_TYPE_STRING:
124+
return $builtinType;
125+
case LegacyType::BUILTIN_TYPE_OBJECT:
126+
if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) {
127+
return \DateTimeInterface::class;
128+
}
129+
130+
// no break
131+
default:
132+
return 'string';
133+
}
134+
}
135+
136+
if ($type->isIdentifiedBy(TypeIdentifier::ARRAY, TypeIdentifier::INT, TypeIdentifier::FLOAT, TypeIdentifier::BOOL, TypeIdentifier::STRING)) {
137+
while ($type instanceof WrappingTypeInterface) {
138+
$type = $type->getWrappedType();
139+
}
140+
141+
return (string) $type;
142+
}
128143

129-
// no break
130-
default:
131-
return 'string';
144+
if ($type->isIdentifiedBy(\DateTimeInterface::class)) {
145+
return \DateTimeInterface::class;
132146
}
147+
148+
return 'string';
133149
}
134150

135151
/**
@@ -164,15 +180,26 @@ protected function getIdentifierValue(string $iri, string $property): mixed
164180
return $iri;
165181
}
166182

167-
/**
168-
* Are the given values valid according to the given {@see Type}?
169-
*/
170-
protected function hasValidValues(array $values, Type $type): bool
183+
protected function hasValidValues(array $values, LegacyType|Type $type): bool
171184
{
185+
if ($type instanceof LegacyType) {
186+
foreach ($values as $value) {
187+
if (
188+
null !== $value
189+
&& LegacyType::BUILTIN_TYPE_INT === $type->getBuiltinType()
190+
&& false === filter_var($value, \FILTER_VALIDATE_INT)
191+
) {
192+
return false;
193+
}
194+
}
195+
196+
return true;
197+
}
198+
172199
foreach ($values as $value) {
173200
if (
174201
null !== $value
175-
&& Type::BUILTIN_TYPE_INT === $type->getBuiltinType()
202+
&& $type->isIdentifiedBy(TypeIdentifier::INT)
176203
&& false === filter_var($value, \FILTER_VALIDATE_INT)
177204
) {
178205
return false;

src/Elasticsearch/Tests/Extension/SortExtensionTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2323
use PHPUnit\Framework\TestCase;
2424
use Prophecy\PhpUnit\ProphecyTrait;
25-
use Symfony\Component\PropertyInfo\Type;
2625
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26+
use Symfony\Component\TypeInfo\Type;
2727

2828
class SortExtensionTest extends TestCase
2929
{
@@ -55,10 +55,10 @@ public function testApplyToCollection(): void
5555

5656
public function testApplyToCollectionWithNestedProperty(): void
5757
{
58-
$fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class));
58+
$fooType = Type::list(Type::object(Foo::class));
5959

6060
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
61-
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled();
61+
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled();
6262

6363
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
6464
$resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled();

src/Elasticsearch/Tests/Filter/MatchFilterTest.php

+16-16
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
use Prophecy\Argument;
2828
use Prophecy\PhpUnit\ProphecyTrait;
2929
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
30-
use Symfony\Component\PropertyInfo\Type;
3130
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
31+
use Symfony\Component\TypeInfo\Type;
3232

3333
class MatchFilterTest extends TestCase
3434
{
@@ -55,8 +55,8 @@ public function testApply(): void
5555
$propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar']))->shouldBeCalled();
5656

5757
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
58-
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled();
59-
$propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled();
58+
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled();
59+
$propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled();
6060

6161
$foo = new Foo();
6262
$foo->setName('Xavier');
@@ -89,12 +89,12 @@ public function testApply(): void
8989

9090
public function testApplyWithNestedArrayProperty(): void
9191
{
92-
$fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class));
93-
$barType = new Type(Type::BUILTIN_TYPE_STRING);
92+
$fooType = Type::list(Type::object(Foo::class));
93+
$barType = Type::string();
9494

9595
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
96-
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled();
97-
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled();
96+
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled();
97+
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled();
9898

9999
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
100100
$resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled();
@@ -121,12 +121,12 @@ public function testApplyWithNestedArrayProperty(): void
121121

122122
public function testApplyWithNestedObjectProperty(): void
123123
{
124-
$fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class);
125-
$barType = new Type(Type::BUILTIN_TYPE_STRING);
124+
$fooType = Type::object(Foo::class);
125+
$barType = Type::string();
126126

127127
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
128-
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled();
129-
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled();
128+
$propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled();
129+
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled();
130130

131131
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
132132
$resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled();
@@ -156,7 +156,7 @@ public function testApplyWithInvalidFilters(): void
156156
$propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'bar']))->shouldBeCalled();
157157

158158
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
159-
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled();
159+
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled();
160160
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled();
161161

162162
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
@@ -183,11 +183,11 @@ public function testGetDescription(): void
183183
$propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar', 'date', 'weird']))->shouldBeCalled();
184184

185185
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
186-
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled();
187-
$propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled();
186+
$propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled();
187+
$propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled();
188188
$propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled();
189-
$propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]))->shouldBeCalled();
190-
$propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_RESOURCE)]))->shouldBeCalled();
189+
$propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withNativeType(Type::object(\DateTimeImmutable::class)))->shouldBeCalled();
190+
$propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withNativeType(Type::resource()))->shouldBeCalled();
191191

192192
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
193193
$resourceClassResolverProphecy->isResourceClass(\DateTimeImmutable::class)->willReturn(false)->shouldBeCalled();

0 commit comments

Comments
 (0)