diff --git a/src/Elasticsearch/Filter/AbstractFilter.php b/src/Elasticsearch/Filter/AbstractFilter.php index a989cd18339..a305a57e03b 100644 --- a/src/Elasticsearch/Filter/AbstractFilter.php +++ b/src/Elasticsearch/Filter/AbstractFilter.php @@ -19,8 +19,14 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +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; /** * Abstract class with helpers for easing the implementation of a filter. @@ -31,7 +37,9 @@ */ abstract class AbstractFilter implements FilterInterface { - use FieldDatatypeTrait { getNestedFieldPath as protected; } + use FieldDatatypeTrait { + getNestedFieldPath as protected; + } public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, protected ?NameConverterInterface $nameConverter = null, protected ?array $properties = null) { @@ -70,8 +78,108 @@ protected function hasProperty(string $resourceClass, string $property): bool * - is the decomposed given property an association? * - the resource class of the decomposed given property * - the property name of the decomposed given property + * + * @return array{0: ?Type, 1: ?bool, 2: ?class-string, 3: ?string} */ protected function getMetadata(string $resourceClass, string $property): array + { + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + return $this->getLegacyMetadata($resourceClass, $property); + } + + $noop = [null, null, null, null]; + + if (!$this->hasProperty($resourceClass, $property)) { + return $noop; + } + + $properties = explode('.', $property); + $totalProperties = \count($properties); + $currentResourceClass = $resourceClass; + $hasAssociation = false; + $currentProperty = null; + $type = null; + + foreach ($properties as $index => $currentProperty) { + try { + $propertyMetadata = $this->propertyMetadataFactory->create($currentResourceClass, $currentProperty); + } catch (PropertyNotFoundException) { + return $noop; + } + + // check each type before deciding if it's noop or not + // e.g: maybe the first type is noop, but the second is valid + $isNoop = false; + + ++$index; + + $type = $propertyMetadata->getNativeType(); + + if (null === $type) { + return $noop; + } + + foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { + $builtinType = $t; + + while ($builtinType instanceof WrappingTypeInterface) { + $builtinType = $builtinType->getWrappedType(); + } + + if (!$builtinType instanceof ObjectType && !$t instanceof CollectionType) { + if ($totalProperties === $index) { + break 2; + } + + $isNoop = true; + + continue; + } + + if ($t instanceof CollectionType) { + $t = $t->getCollectionValueType(); + $builtinType = $t; + + while ($builtinType instanceof WrappingTypeInterface) { + $builtinType = $builtinType->getWrappedType(); + } + + if (!$builtinType instanceof ObjectType) { + if ($totalProperties === $index) { + break 2; + } + + $isNoop = true; + + continue; + } + } + + $className = $builtinType->getClassName(); + + if ($isResourceClass = $this->resourceClassResolver->isResourceClass($className)) { + $currentResourceClass = $className; + } elseif ($totalProperties !== $index) { + $isNoop = true; + + continue; + } + + $hasAssociation = $totalProperties === $index && $isResourceClass; + $isNoop = false; + + break; + } + } + + if ($isNoop) { + return $noop; + } + + return [$type, $hasAssociation, $currentResourceClass, $currentProperty]; + } + + protected function getLegacyMetadata(string $resourceClass, string $property): array { $noop = [null, null, null, null]; @@ -108,7 +216,7 @@ protected function getMetadata(string $resourceClass, string $property): array foreach ($types as $type) { $builtinType = $type->getBuiltinType(); - if (Type::BUILTIN_TYPE_OBJECT !== $builtinType && Type::BUILTIN_TYPE_ARRAY !== $builtinType) { + if (LegacyType::BUILTIN_TYPE_OBJECT !== $builtinType && LegacyType::BUILTIN_TYPE_ARRAY !== $builtinType) { if ($totalProperties === $index) { break 2; } @@ -124,7 +232,7 @@ protected function getMetadata(string $resourceClass, string $property): array continue; } - if (Type::BUILTIN_TYPE_ARRAY === $builtinType && Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { + if (LegacyType::BUILTIN_TYPE_ARRAY === $builtinType && LegacyType::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { if ($totalProperties === $index) { break 2; } diff --git a/src/Elasticsearch/Filter/AbstractSearchFilter.php b/src/Elasticsearch/Filter/AbstractSearchFilter.php index 318289dd697..a20fe911f97 100644 --- a/src/Elasticsearch/Filter/AbstractSearchFilter.php +++ b/src/Elasticsearch/Filter/AbstractSearchFilter.php @@ -21,8 +21,11 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * 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 */ abstract protected function getQuery(string $property, array $values, ?string $nestedPath): array; - /** - * Converts the given {@see Type} in PHP type. - */ - protected function getPhpType(Type $type): string + protected function getPhpType(LegacyType|Type $type): string { - switch ($builtinType = $type->getBuiltinType()) { - case Type::BUILTIN_TYPE_ARRAY: - case Type::BUILTIN_TYPE_INT: - case Type::BUILTIN_TYPE_FLOAT: - case Type::BUILTIN_TYPE_BOOL: - case Type::BUILTIN_TYPE_STRING: - return $builtinType; - case Type::BUILTIN_TYPE_OBJECT: - if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { - return \DateTimeInterface::class; - } + if ($type instanceof LegacyType) { + switch ($builtinType = $type->getBuiltinType()) { + case LegacyType::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_STRING: + return $builtinType; + case LegacyType::BUILTIN_TYPE_OBJECT: + if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { + return \DateTimeInterface::class; + } + + // no break + default: + return 'string'; + } + } + + if ($type->isIdentifiedBy(TypeIdentifier::ARRAY, TypeIdentifier::INT, TypeIdentifier::FLOAT, TypeIdentifier::BOOL, TypeIdentifier::STRING)) { + while ($type instanceof WrappingTypeInterface) { + $type = $type->getWrappedType(); + } + + return (string) $type; + } - // no break - default: - return 'string'; + if ($type->isIdentifiedBy(\DateTimeInterface::class)) { + return \DateTimeInterface::class; } + + return 'string'; } /** @@ -164,15 +180,26 @@ protected function getIdentifierValue(string $iri, string $property): mixed return $iri; } - /** - * Are the given values valid according to the given {@see Type}? - */ - protected function hasValidValues(array $values, Type $type): bool + protected function hasValidValues(array $values, LegacyType|Type $type): bool { + if ($type instanceof LegacyType) { + foreach ($values as $value) { + if ( + null !== $value + && LegacyType::BUILTIN_TYPE_INT === $type->getBuiltinType() + && false === filter_var($value, \FILTER_VALIDATE_INT) + ) { + return false; + } + } + + return true; + } + foreach ($values as $value) { if ( null !== $value - && Type::BUILTIN_TYPE_INT === $type->getBuiltinType() + && $type->isIdentifiedBy(TypeIdentifier::INT) && false === filter_var($value, \FILTER_VALIDATE_INT) ) { return false; diff --git a/src/Elasticsearch/Tests/Extension/SortExtensionTest.php b/src/Elasticsearch/Tests/Extension/SortExtensionTest.php index f317a46e1a4..db72e689550 100644 --- a/src/Elasticsearch/Tests/Extension/SortExtensionTest.php +++ b/src/Elasticsearch/Tests/Extension/SortExtensionTest.php @@ -22,8 +22,8 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class SortExtensionTest extends TestCase { @@ -55,10 +55,10 @@ public function testApplyToCollection(): void public function testApplyToCollectionWithNestedProperty(): void { - $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)); + $fooType = Type::list(Type::object(Foo::class)); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Filter/MatchFilterTest.php b/src/Elasticsearch/Tests/Filter/MatchFilterTest.php index 82bf5623180..ebcc14aebcc 100644 --- a/src/Elasticsearch/Tests/Filter/MatchFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/MatchFilterTest.php @@ -27,8 +27,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class MatchFilterTest extends TestCase { @@ -55,8 +55,8 @@ public function testApply(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $foo = new Foo(); $foo->setName('Xavier'); @@ -89,12 +89,12 @@ public function testApply(): void public function testApplyWithNestedArrayProperty(): void { - $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)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -121,12 +121,12 @@ public function testApplyWithNestedArrayProperty(): void public function testApplyWithNestedObjectProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::object(Foo::class); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -156,7 +156,7 @@ public function testApplyWithInvalidFilters(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -183,11 +183,11 @@ public function testGetDescription(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar', 'date', 'weird']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_RESOURCE)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withNativeType(Type::object(\DateTimeImmutable::class)))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withNativeType(Type::resource()))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(\DateTimeImmutable::class)->willReturn(false)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Filter/OrderFilterTest.php b/src/Elasticsearch/Tests/Filter/OrderFilterTest.php index 87f3186afb9..3aefbadeecb 100644 --- a/src/Elasticsearch/Tests/Filter/OrderFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/OrderFilterTest.php @@ -24,8 +24,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class OrderFilterTest extends TestCase { @@ -47,7 +47,7 @@ public function testConstruct(): void public function testApply(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); $nameConverterProphecy->normalize('name', Foo::class, null, Argument::type('array'))->willReturn('name')->shouldBeCalled(); @@ -69,12 +69,12 @@ public function testApply(): void public function testApplyWithNestedProperty(): void { - $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)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -114,7 +114,7 @@ public function testApplyWithInvalidOrderFilter(): void public function testApplyWithInvalidTypeAndInvalidDirection(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); @@ -137,7 +137,7 @@ public function testApplyWithInvalidTypeAndInvalidDirection(): void public function testDescription(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $orderFilter = new OrderFilter( diff --git a/src/Elasticsearch/Tests/Filter/TermFilterTest.php b/src/Elasticsearch/Tests/Filter/TermFilterTest.php index a2836365d40..e7062bbbf84 100644 --- a/src/Elasticsearch/Tests/Filter/TermFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/TermFilterTest.php @@ -27,8 +27,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class TermFilterTest extends TestCase { @@ -55,8 +55,8 @@ public function testApply(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $foo = new Foo(); $foo->setName('Xavier'); @@ -89,12 +89,12 @@ public function testApply(): void public function testApplyWithNestedArrayProperty(): void { - $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)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -121,12 +121,12 @@ public function testApplyWithNestedArrayProperty(): void public function testApplyWithNestedObjectProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::object(Foo::class); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -156,7 +156,7 @@ public function testApplyWithInvalidFilters(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -183,11 +183,11 @@ public function testGetDescription(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar', 'date', 'weird']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_RESOURCE)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withNativeType(Type::object(\DateTimeImmutable::class)))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withNativeType(Type::resource()))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(\DateTimeImmutable::class)->willReturn(false)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php b/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php index cebb490d217..fec6ba90695 100644 --- a/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php +++ b/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\TypeInfo\Type; class FieldDatatypeTraitTest extends TestCase { @@ -71,7 +71,7 @@ public function testGetNestedFieldPathWithPropertyWithoutType(): void public function testGetNestedFieldPathWithInvalidCollectionType(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $fieldDatatype = self::createFieldDatatypeInstance($propertyMetadataFactoryProphecy->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal()); @@ -90,14 +90,14 @@ public function testIsNestedField(): void private function getValidFieldDatatype() { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = 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)); - $bazType = new Type(Type::BUILTIN_TYPE_STRING, false, Foo::class); + $fooType = Type::object(Foo::class); + $barType = Type::list(Type::object(Foo::class)); + $bazType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'baz')->willReturn((new ApiProperty())->withBuiltinTypes([$bazType])); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'baz')->willReturn((new ApiProperty())->withNativeType($bazType)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); diff --git a/src/Elasticsearch/Util/FieldDatatypeTrait.php b/src/Elasticsearch/Util/FieldDatatypeTrait.php index 6defd345353..b0c1a4a57c2 100644 --- a/src/Elasticsearch/Util/FieldDatatypeTrait.php +++ b/src/Elasticsearch/Util/FieldDatatypeTrait.php @@ -16,7 +16,13 @@ use ApiPlatform\Metadata\Exception\PropertyNotFoundException; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use Symfony\Component\PropertyInfo\Type; +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. @@ -64,29 +70,72 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s return null; } - $types = $propertyMetadata->getBuiltinTypes() ?? []; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + + foreach ($types as $type) { + if ( + LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() + && null !== ($nextResourceClass = $type->getClassName()) + && $this->resourceClassResolver->isResourceClass($nextResourceClass) + ) { + $nestedPath = $this->getNestedFieldPath($nextResourceClass, implode('.', $properties)); + + return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; + } + + if ( + null !== ($type = $type->getCollectionValueTypes()[0] ?? null) + && LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() + && null !== ($className = $type->getClassName()) + && $this->resourceClassResolver->isResourceClass($className) + ) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; + } + } - foreach ($types as $type) { - if ( - Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() - && null !== ($nextResourceClass = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($nextResourceClass) - ) { - $nestedPath = $this->getNestedFieldPath($nextResourceClass, implode('.', $properties)); + return null; + } - return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; - } + $type = $propertyMetadata->getNativeType(); - if ( - null !== ($type = $type->getCollectionValueTypes()[0] ?? null) - && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() - && null !== ($className = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) - ) { - $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + if (null === $type) { + return null; + } - return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; - } + /** @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, + }; + }; + + if ($type->isSatisfiedBy($typeIsResourceClass)) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + 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)) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; } return null; diff --git a/src/Elasticsearch/composer.json b/src/Elasticsearch/composer.json index 332889c837c..4bf43edabdd 100644 --- a/src/Elasticsearch/composer.json +++ b/src/Elasticsearch/composer.json @@ -33,12 +33,12 @@ "symfony/property-access": "^6.4 || ^7.0", "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", + "symfony/type-info": "^7.3-dev", "symfony/uid": "^6.4 || ^7.0" }, "require-dev": { "phpspec/prophecy-phpunit": "^2.2", - "phpunit/phpunit": "^11.2", - "symfony/type-info": "^7.3-dev" + "phpunit/phpunit": "^11.2" }, "autoload": { "psr-4": {