From 0f8509ce3c57f178ae1f59d6da734b0c992bccf1 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 10 Aug 2023 15:53:56 +0200 Subject: [PATCH] graphql ok --- src/Api/IdentifiersExtractor.php | 150 +++++++++++++++++- src/Api/IdentifiersExtractorInterface.php | 4 +- src/Api/IriConverterInterface.php | 4 +- src/Api/ResourceClassResolverInterface.php | 6 +- src/Api/UriVariablesConverterInterface.php | 24 +-- src/Elasticsearch/composer.json | 145 +++++++++-------- src/Exception/InvalidArgumentException.php | 6 +- src/GraphQl/Serializer/ItemNormalizer.php | 2 +- .../Tests/Serializer/ItemNormalizerTest.php | 2 +- src/GraphQl/composer.json | 20 ++- src/Metadata/IdentifiersExtractor.php | 5 +- .../IdentifiersExtractorInterface.php | 1 - .../ApiResource/DummyWithEnumIdentifier.php | 37 +++++ .../Fixtures/ApiResource/RelationMultiple.php | 64 ++++++++ .../State/RelationMultipleProvider.php | 53 +++++++ .../Tests}/IdentifiersExtractorTest.php | 14 +- src/Metadata/UriVariablesConverter.php | 90 +++++++++++ .../UriVariablesConverterInterface.php | 36 +++++ src/Metadata/Util/ResourceClassInfoTrait.php | 4 +- src/Serializer/AbstractItemNormalizer.php | 8 +- src/Serializer/ItemNormalizer.php | 6 +- .../Tests/AbstractItemNormalizerTest.php | 4 +- .../Fixtures/ApiResource/DtoWithNullValue.php | 2 +- .../Fixtures/ApiResource/RelatedDummy.php | 1 - .../Fixtures/ApiResource/SecuredDummy.php | 1 - src/Serializer/Tests/ItemNormalizerTest.php | 8 +- .../Tests/SerializerContextBuilderTest.php | 2 +- .../SerializerFilterContextBuilderTest.php | 2 +- src/Symfony/Routing/IriConverter.php | 2 +- .../Util/RequestAttributesExtractor.php | 1 + ...ntViolationListAwareExceptionInterface.php | 2 +- src/Util/RequestAttributesExtractor.php | 1 + 32 files changed, 570 insertions(+), 137 deletions(-) create mode 100644 src/Metadata/Tests/Fixtures/ApiResource/DummyWithEnumIdentifier.php create mode 100644 src/Metadata/Tests/Fixtures/ApiResource/RelationMultiple.php create mode 100644 src/Metadata/Tests/Fixtures/State/RelationMultipleProvider.php rename {tests/Api => src/Metadata/Tests}/IdentifiersExtractorTest.php (95%) create mode 100644 src/Metadata/UriVariablesConverter.php create mode 100644 src/Metadata/UriVariablesConverterInterface.php diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php index b58f6f046af..54d1987cce1 100644 --- a/src/Api/IdentifiersExtractor.php +++ b/src/Api/IdentifiersExtractor.php @@ -13,10 +13,154 @@ namespace ApiPlatform\Api; -class_exists(\ApiPlatform\Metadata\IdentifiersExtractor::class); +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -if (false) { - final class IdentifiersExtractor extends \ApiPlatform\Metadata\IdentifiersExtractor +/** + * {@inheritdoc} + * + * @deprecated use ApiPlatform\Metadata\IdentifiersExtractor instead + * + * @author Antoine Bluchet + */ +final class IdentifiersExtractor implements IdentifiersExtractorInterface +{ + use ResourceClassInfoTrait; + private readonly PropertyAccessorInterface $propertyAccessor; + + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null) + { + $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceClassResolver = $resourceClassResolver; + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); + } + + /** + * {@inheritdoc} + * + * TODO: 3.0 identifiers should be stringable? + */ + public function getIdentifiersFromItem(object $item, Operation $operation = null, array $context = []): array + { + if (!$this->isResourceClass($this->getObjectClass($item))) { + return ['id' => $this->propertyAccessor->getValue($item, 'id')]; + } + + if ($operation && $operation->getClass()) { + return $this->getIdentifiersFromOperation($item, $operation, $context); + } + + $resourceClass = $this->getResourceClass($item, true); + $operation ??= $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true); + + return $this->getIdentifiersFromOperation($item, $operation, $context); + } + + private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array + { + if ($operation instanceof HttpOperation) { + $links = $operation->getUriVariables(); + } elseif ($operation instanceof GraphQlOperation) { + $links = $operation->getLinks(); + } + + $identifiers = []; + foreach ($links ?? [] as $link) { + if (1 < (is_countable($link->getIdentifiers()) ? \count($link->getIdentifiers()) : 0)) { + $compositeIdentifiers = []; + foreach ($link->getIdentifiers() as $identifier) { + $compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName()); + } + + $identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers); + continue; + } + + $parameterName = $link->getParameterName(); + $identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $link->getIdentifiers()[0], $parameterName, $link->getToProperty()); + } + + return $identifiers; + } + + /** + * Gets the value of the given class property. + */ + private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, string $toProperty = null): float|bool|int|string { + if ($item instanceof $class) { + try { + return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName); + } catch (NoSuchPropertyException $e) { + throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); + } + } + + if ($toProperty) { + return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName); + } + + $resourceClass = $this->getResourceClass($item, true); + foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); + + $types = $propertyMetadata->getBuiltinTypes(); + if (null === ($type = $types[0] ?? null)) { + continue; + } + + try { + if ($type->isCollection()) { + $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; + + if (null !== $collectionValueType && $collectionValueType->getClassName() === $class) { + return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, sprintf('%s[0].%s', $propertyName, $property)), $parameterName); + } + } + + if ($type->getClassName() === $class) { + return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName); + } + } catch (NoSuchPropertyException $e) { + throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); + } + } + + throw new RuntimeException('Not able to retrieve identifiers.'); + } + + /** + * TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior. + * + * @param mixed|\Stringable $identifierValue + */ + private function resolveIdentifierValue(mixed $identifierValue, string $parameterName): float|bool|int|string + { + if (null === $identifierValue) { + throw new RuntimeException('No identifier value found, did you forget to persist the entity?'); + } + + if (\is_scalar($identifierValue)) { + return $identifierValue; + } + + if ($identifierValue instanceof \Stringable) { + return (string) $identifierValue; + } + + if ($identifierValue instanceof \BackedEnum) { + return (string) $identifierValue->value; + } + + throw new RuntimeException(sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName)); } } diff --git a/src/Api/IdentifiersExtractorInterface.php b/src/Api/IdentifiersExtractorInterface.php index 7def441a8aa..757d37e1171 100644 --- a/src/Api/IdentifiersExtractorInterface.php +++ b/src/Api/IdentifiersExtractorInterface.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Api; -class_alias(\ApiPlatform\Metadata\IdentifiersExtractorInterface::class, \ApiPlatform\Api\IdentifiersExtractorInterface::class); +class_exists(\ApiPlatform\Metadata\IdentifiersExtractorInterface::class); -if (false) { +if (!class_exists(IdentifiersExtractorInterface::class)) { interface IdentifiersExtractorInterface extends \ApiPlatform\Metadata\IdentifiersExtractorInterface { } diff --git a/src/Api/IriConverterInterface.php b/src/Api/IriConverterInterface.php index dfcd05528ce..59a798403ac 100644 --- a/src/Api/IriConverterInterface.php +++ b/src/Api/IriConverterInterface.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Api; -class_alias(\ApiPlatform\Metadata\IriConverterInterface::class, \ApiPlatform\Api\IriConverterInterface::class); +class_exists(\ApiPlatform\Metadata\IriConverterInterface::class); -if (false) { +if (!class_exists(IriConverterInterface::class)) { interface IriConverterInterface extends \ApiPlatform\Metadata\IriConverterInterface { } diff --git a/src/Api/ResourceClassResolverInterface.php b/src/Api/ResourceClassResolverInterface.php index 1e33eaef9a6..469ae947c85 100644 --- a/src/Api/ResourceClassResolverInterface.php +++ b/src/Api/ResourceClassResolverInterface.php @@ -13,8 +13,10 @@ namespace ApiPlatform\Api; -class_alias(\ApiPlatform\Metadata\ResourceClassResolverInterface::class, \ApiPlatform\Api\ResourceClassResolverInterface::class); +class_exists(\ApiPlatform\Metadata\ResourceClassResolverInterface::class); if (false) { - interface ResourceClassResolverInterface extends \ApiPlatform\Metadata\ResourceClassResolverInterface {} + interface ResourceClassResolverInterface extends \ApiPlatform\Metadata\ResourceClassResolverInterface + { + } } diff --git a/src/Api/UriVariablesConverterInterface.php b/src/Api/UriVariablesConverterInterface.php index 917fc7b9de3..4ed3c921c68 100644 --- a/src/Api/UriVariablesConverterInterface.php +++ b/src/Api/UriVariablesConverterInterface.php @@ -13,24 +13,10 @@ namespace ApiPlatform\Api; -use ApiPlatform\Exception\InvalidIdentifierException; +class_exists(\ApiPlatform\Metadata\UriVariablesConverterInterface::class); -/** - * Identifier converter. - * - * @author Antoine Bluchet - */ -interface UriVariablesConverterInterface -{ - /** - * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. - * - * @param array $data URI variables to convert to PHP values - * @param string $class The class to which the URI variables belong to - * - * @throws InvalidIdentifierException - * - * @return array Array indexed by identifiers properties with their values denormalized - */ - public function convert(array $data, string $class, array $context = []): array; +if (!class_exists(UriVariablesConverterInterface::class)) { + interface UriVariablesConverterInterface extends \ApiPlatform\Metadata\UriVariablesConverterInterface + { + } } diff --git a/src/Elasticsearch/composer.json b/src/Elasticsearch/composer.json index cf08a2ca8e0..5fe72aca5aa 100644 --- a/src/Elasticsearch/composer.json +++ b/src/Elasticsearch/composer.json @@ -1,77 +1,82 @@ { - "name": "api-platform/elasticseach", - "description": "Elasticsearch support", - "type": "library", - "keywords": [ - "Filter", - "Elasticsearch" - ], - "homepage": "https://api-platform.com", - "license": "MIT", - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr" + "name": "api-platform/elasticseach", + "description": "Elasticsearch support", + "type": "library", + "keywords": [ + "Filter", + "Elasticsearch" + ], + "homepage": "https://api-platform.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.1", + "api-platform/metadata": "*@dev || ^3.1", + "api-platform/state": "*@dev || ^3.1", + "api-platform/serializer": "*@dev || ^3.1", + "elasticsearch/elasticsearch": "^7.11.0", + "symfony/cache": "^6.1", + "symfony/console": "^6.2", + "symfony/property-info": "^6.1", + "symfony/serializer": "^6.1", + "symfony/uid": "^6.1", + "symfony/property-access": "^6.1" }, - { - "name": "API Platform Community", - "homepage": "https://api-platform.com/community/contributors" - } - ], - "require": { - "php": ">=8.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", - "elasticsearch/elasticsearch": "^7.11.0", - "symfony/cache": "^6.1", - "symfony/console": "^6.2", - "symfony/property-info": "^6.1", - "symfony/serializer": "^6.1", - "symfony/uid": "^6.1", - "symfony/property-access": "^6.1" - }, - "conflict": { - "elasticsearch/elasticsearch": ">=8.0" - }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.1" - }, - "autoload": { - "psr-4": { - "ApiPlatform\\Elasticsearch\\": "" + "conflict": { + "elasticsearch/elasticsearch": ">=8.0" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "config": { - "preferred-install": { - "*": "dist" + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "symfony/phpunit-bridge": "^6.1" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Elasticsearch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, - "sort-packages": true, - "allow-plugins": { - "composer/package-versions-deprecated": true, - "phpstan/extension-installer": true - } - }, - "extra": { - "branch-alias": { - "dev-main": "3.2.x-dev" + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true + } }, - "symfony": { - "require": "^6.1" - } - }, - "repositories": [ - { - "type": "path", - "url": "../Metadata" + "extra": { + "branch-alias": { + "dev-main": "3.2.x-dev" + }, + "symfony": { + "require": "^6.1" + } }, - { - "type": "path", - "url": "../State" - } - ] + "repositories": [ + { + "type": "path", + "url": "../Metadata" + }, + { + "type": "path", + "url": "../State" + }, + { + "type": "path", + "url": "../Serializer" + } + ] } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index bddf0a57112..0a2c938bd7b 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -13,11 +13,15 @@ namespace ApiPlatform\Exception; +use ApiPlatform\Metadata\Exception\InvalidArgumentException as MetadataInvalidArgumentException; + /** * Invalid argument exception. * * @author Kévin Dunglas + * + * @deprecated use \ApiPlatform\Metadata\Exception\InvalidArgumentException */ -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +class InvalidArgumentException extends MetadataInvalidArgumentException { } diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php index 1f301a0b464..551cedcdb83 100644 --- a/src/GraphQl/Serializer/ItemNormalizer.php +++ b/src/GraphQl/Serializer/ItemNormalizer.php @@ -13,8 +13,8 @@ namespace ApiPlatform\GraphQl\Serializer; -use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; diff --git a/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php b/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php index 6ed9f621afa..f619093bcab 100644 --- a/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php +++ b/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php @@ -13,10 +13,10 @@ namespace ApiPlatform\GraphQl\Tests\Serializer; -use ApiPlatform\Api\IdentifiersExtractorInterface; use ApiPlatform\GraphQl\Serializer\ItemNormalizer; use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; diff --git a/src/GraphQl/composer.json b/src/GraphQl/composer.json index b3531abd098..f41f36ba3ad 100644 --- a/src/GraphQl/composer.json +++ b/src/GraphQl/composer.json @@ -22,6 +22,7 @@ "require": { "php": ">=8.1", "api-platform/metadata": "*@dev || ^3.1", + "api-platform/serializer": "*@dev || ^3.1", "api-platform/state": "*@dev || ^3.1", "symfony/property-info": "^6.1", "symfony/serializer": "^6.1", @@ -29,10 +30,13 @@ }, "require-dev": { "phpspec/prophecy-phpunit": "^2.0", + "api-platform/symfony": "*@dev || ^3.1", + "api-platform/validator": "*@dev || ^3.1", + "twig/twig": "^3.7", + "symfony/mercure-bundle": "*", "symfony/phpunit-bridge": "^6.1", "symfony/routing": "^6.1", - "symfony/validator": "^6.1", - "symfony/mercure-bundle": "*" + "symfony/validator": "^6.1" }, "autoload": { "psr-4": { @@ -68,6 +72,18 @@ { "type": "path", "url": "../State" + }, + { + "type": "path", + "url": "../Serializer" + }, + { + "type": "path", + "url": "../Symfony" + }, + { + "type": "path", + "url": "../Validator" } ] } diff --git a/src/Metadata/IdentifiersExtractor.php b/src/Metadata/IdentifiersExtractor.php index 5ea5a629ab7..c36dc4a3ea2 100644 --- a/src/Metadata/IdentifiersExtractor.php +++ b/src/Metadata/IdentifiersExtractor.php @@ -13,14 +13,11 @@ namespace ApiPlatform\Metadata; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; diff --git a/src/Metadata/IdentifiersExtractorInterface.php b/src/Metadata/IdentifiersExtractorInterface.php index 9fef96732f8..5ef91fde783 100644 --- a/src/Metadata/IdentifiersExtractorInterface.php +++ b/src/Metadata/IdentifiersExtractorInterface.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Metadata; use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\Metadata\Operation; /** * Extracts identifiers for a given Resource according to the retrieved Metadata. diff --git a/src/Metadata/Tests/Fixtures/ApiResource/DummyWithEnumIdentifier.php b/src/Metadata/Tests/Fixtures/ApiResource/DummyWithEnumIdentifier.php new file mode 100644 index 00000000000..8c4e3d633ae --- /dev/null +++ b/src/Metadata/Tests/Fixtures/ApiResource/DummyWithEnumIdentifier.php @@ -0,0 +1,37 @@ + + * + * 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\Metadata\Tests\Fixtures\ApiResource; + +class DummyWithEnumIdentifier +{ + public $id; + + public function __construct( + public StringEnumAsIdentifier $stringEnumAsIdentifier = StringEnumAsIdentifier::FOO, + public IntEnumAsIdentifier $intEnumAsIdentifier = IntEnumAsIdentifier::FOO, + ) { + } +} + +enum IntEnumAsIdentifier: int +{ + case FOO = 1; + case BAR = 2; +} + +enum StringEnumAsIdentifier: string +{ + case FOO = 'foo'; + case BAR = 'bar'; +} diff --git a/src/Metadata/Tests/Fixtures/ApiResource/RelationMultiple.php b/src/Metadata/Tests/Fixtures/ApiResource/RelationMultiple.php new file mode 100644 index 00000000000..69488222e9a --- /dev/null +++ b/src/Metadata/Tests/Fixtures/ApiResource/RelationMultiple.php @@ -0,0 +1,64 @@ + + * + * 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\Metadata\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Tests\Fixtures\TestBundle\State\RelationMultipleProvider; + +#[ApiResource( + mercure: true, + operations: [ + new Post(), + new Get( + uriTemplate: '/dummy/{firstId}/relations/{secondId}', + uriVariables: [ + 'firstId' => new Link( + fromClass: Dummy::class, + toProperty: 'first', + identifiers: ['id'], + ), + 'secondId' => new Link( + fromClass: Dummy::class, + toProperty: 'second', + identifiers: ['id'], + ), + ], + provider: RelationMultipleProvider::class, + ), + new GetCollection( + uriTemplate : '/dummy/{firstId}/relations', + uriVariables: [ + 'firstId' => new Link( + fromClass: Dummy::class, + toProperty: 'first', + identifiers: ['id'], + ), + ], + provider: RelationMultipleProvider::class, + ), + ] +)] + +class RelationMultiple +{ + #[ApiProperty(identifier: true)] + public ?int $id = null; + public ?Dummy $first = null; + public ?Dummy $second = null; +} diff --git a/src/Metadata/Tests/Fixtures/State/RelationMultipleProvider.php b/src/Metadata/Tests/Fixtures/State/RelationMultipleProvider.php new file mode 100644 index 00000000000..5f8ae004747 --- /dev/null +++ b/src/Metadata/Tests/Fixtures/State/RelationMultipleProvider.php @@ -0,0 +1,53 @@ + + * + * 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\Metadata\Tests\Fixtures\State; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationMultiple; + +class RelationMultipleProvider implements ProviderInterface +{ + /** + * {@inheritDoc} + */ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object|null + { + $firstDummy = new Dummy(); + $firstDummy->setId($uriVariables['firstId']); + $secondDummy = new Dummy(); + $relationMultiple = new RelationMultiple(); + $relationMultiple->id = 1; + $relationMultiple->first = $firstDummy; + $relationMultiple->second = $secondDummy; + + if ($operation instanceof GetCollection) { + $secondDummy->setId(2); + $thirdDummy = new Dummy(); + $thirdDummy->setId(3); + $relationMultiple2 = new RelationMultiple(); + $relationMultiple2->id = 2; + $relationMultiple2->first = $firstDummy; + $relationMultiple2->second = $thirdDummy; + + return [$relationMultiple, $relationMultiple2]; + } + + $relationMultiple->second->setId($uriVariables['secondId']); + + return $relationMultiple; + } +} diff --git a/tests/Api/IdentifiersExtractorTest.php b/src/Metadata/Tests/IdentifiersExtractorTest.php similarity index 95% rename from tests/Api/IdentifiersExtractorTest.php rename to src/Metadata/Tests/IdentifiersExtractorTest.php index db7cd50e06c..88de9d6fa32 100644 --- a/tests/Api/IdentifiersExtractorTest.php +++ b/src/Metadata/Tests/IdentifiersExtractorTest.php @@ -11,20 +11,20 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Api; +namespace ApiPlatform\Metadata\Tests; -use ApiPlatform\Api\IdentifiersExtractor; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractor; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyWithEnumIdentifier; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationMultiple; -use ApiPlatform\Tests\Fixtures\TestBundle\State\RelationMultipleProvider; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\Dummy; +use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\DummyWithEnumIdentifier; +use ApiPlatform\Metadata\Tests\Fixtures\ApiResource\RelationMultiple; +use ApiPlatform\Metadata\Tests\Fixtures\State\RelationMultipleProvider; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; diff --git a/src/Metadata/UriVariablesConverter.php b/src/Metadata/UriVariablesConverter.php new file mode 100644 index 00000000000..db838be38e6 --- /dev/null +++ b/src/Metadata/UriVariablesConverter.php @@ -0,0 +1,90 @@ + + * + * 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\Metadata; + +use ApiPlatform\Metadata\Exception\InvalidUriVariableException; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * UriVariables converter that chains uri variables transformers. + * + * @author Antoine Bluchet + */ +final class UriVariablesConverter implements UriVariablesConverterInterface +{ + /** + * @param iterable $uriVariableTransformers + */ + public function __construct(private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly iterable $uriVariableTransformers) + { + } + + /** + * {@inheritdoc} + * + * To handle the composite identifiers type correctly, use an `uri_variables_map` that maps uriVariables to their uriVariablesDefinition. + * Indeed, a composite identifier will already be parsed, and their corresponding properties will be the parameterName and not the defined + * identifiers. + */ + public function convert(array $uriVariables, string $class, array $context = []): array + { + $operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation(); + $context += ['operation' => $operation]; + $uriVariablesDefinitions = $operation->getUriVariables() ?? []; + + foreach ($uriVariables as $parameterName => $value) { + $uriVariableDefinition = $context['uri_variables_map'][$parameterName] ?? $uriVariablesDefinitions[$parameterName] ?? $uriVariablesDefinitions['id'] ?? new Link(); + + // When a composite identifier is used, we assume that the parameterName is the property to find our type + $properties = $uriVariableDefinition->getIdentifiers() ?? [$parameterName]; + if ($uriVariableDefinition->getCompositeIdentifier()) { + $properties = [$parameterName]; + } + + if (!$types = $this->getIdentifierTypes($uriVariableDefinition->getFromClass() ?? $class, $properties)) { + continue; + } + + foreach ($this->uriVariableTransformers as $uriVariableTransformer) { + if (!$uriVariableTransformer->supportsTransformation($value, $types, $context)) { + continue; + } + + try { + $uriVariables[$parameterName] = $uriVariableTransformer->transform($value, $types, $context); + break; + } catch (InvalidUriVariableException $e) { + throw new InvalidUriVariableException(sprintf('Identifier "%s" could not be transformed.', $parameterName), $e->getCode(), $e); + } + } + } + + return $uriVariables; + } + + private function getIdentifierTypes(string $resourceClass, array $properties): array + { + $types = []; + foreach ($properties as $property) { + $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); + foreach ($propertyMetadata->getBuiltinTypes() as $type) { + $types[] = Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType; + } + } + + return $types; + } +} diff --git a/src/Metadata/UriVariablesConverterInterface.php b/src/Metadata/UriVariablesConverterInterface.php new file mode 100644 index 00000000000..22ba1e7214e --- /dev/null +++ b/src/Metadata/UriVariablesConverterInterface.php @@ -0,0 +1,36 @@ + + * + * 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\Metadata; + +use ApiPlatform\Metadata\Exception\InvalidIdentifierException; + +/** + * Identifier converter. + * + * @author Antoine Bluchet + */ +interface UriVariablesConverterInterface +{ + /** + * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. + * + * @param array $data URI variables to convert to PHP values + * @param string $class The class to which the URI variables belong to + * + * @throws InvalidIdentifierException + * + * @return array Array indexed by identifiers properties with their values denormalized + */ + public function convert(array $data, string $class, array $context = []): array; +} diff --git a/src/Metadata/Util/ResourceClassInfoTrait.php b/src/Metadata/Util/ResourceClassInfoTrait.php index 7a3a7a44996..a4b5b42af4f 100644 --- a/src/Metadata/Util/ResourceClassInfoTrait.php +++ b/src/Metadata/Util/ResourceClassInfoTrait.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Metadata\Util; +use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; /** * Retrieves information about a resource class. @@ -29,7 +29,7 @@ trait ResourceClassInfoTrait /** * @var LegacyResourceClassResolverInterface|ResourceClassResolverInterface */ - private $resourceClassResolver = null; + private $resourceClassResolver; private ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null; /** diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index c1037d584a6..3ae3bb90f6d 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -13,19 +13,19 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use ApiPlatform\Metadata\Util\CloneTrait; +use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index 1006ed0732f..fca2bfc893c 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -13,14 +13,14 @@ namespace ApiPlatform\Serializer; +use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 9137c14b87e..8bfea62e752 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -13,12 +13,12 @@ namespace ApiPlatform\Serializer\Tests; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DtoWithNullValue; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\Dummy; diff --git a/src/Serializer/Tests/Fixtures/ApiResource/DtoWithNullValue.php b/src/Serializer/Tests/Fixtures/ApiResource/DtoWithNullValue.php index 9efc52cc402..1183c93e489 100644 --- a/src/Serializer/Tests/Fixtures/ApiResource/DtoWithNullValue.php +++ b/src/Serializer/Tests/Fixtures/ApiResource/DtoWithNullValue.php @@ -17,7 +17,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; /** - * Issue #5584 + * Issue #5584. */ #[ApiResource(denormalizationContext: [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true])] final class DtoWithNullValue diff --git a/src/Serializer/Tests/Fixtures/ApiResource/RelatedDummy.php b/src/Serializer/Tests/Fixtures/ApiResource/RelatedDummy.php index ada315167fa..374aff14bbb 100644 --- a/src/Serializer/Tests/Fixtures/ApiResource/RelatedDummy.php +++ b/src/Serializer/Tests/Fixtures/ApiResource/RelatedDummy.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Serializer\Tests\Fixtures\ApiResource; -use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; diff --git a/src/Serializer/Tests/Fixtures/ApiResource/SecuredDummy.php b/src/Serializer/Tests/Fixtures/ApiResource/SecuredDummy.php index 4363808add3..538de12cf34 100644 --- a/src/Serializer/Tests/Fixtures/ApiResource/SecuredDummy.php +++ b/src/Serializer/Tests/Fixtures/ApiResource/SecuredDummy.php @@ -22,7 +22,6 @@ use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Symfony\Component\Validator\Constraints as Assert; diff --git a/src/Serializer/Tests/ItemNormalizerTest.php b/src/Serializer/Tests/ItemNormalizerTest.php index d04cb6e086a..b33f833b99c 100644 --- a/src/Serializer/Tests/ItemNormalizerTest.php +++ b/src/Serializer/Tests/ItemNormalizerTest.php @@ -13,13 +13,11 @@ namespace ApiPlatform\Serializer\Tests; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; @@ -27,6 +25,8 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\ItemNormalizer; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\Dummy; use PHPUnit\Framework\TestCase; diff --git a/src/Serializer/Tests/SerializerContextBuilderTest.php b/src/Serializer/Tests/SerializerContextBuilderTest.php index 308b3db57e6..dacc366a9fb 100644 --- a/src/Serializer/Tests/SerializerContextBuilderTest.php +++ b/src/Serializer/Tests/SerializerContextBuilderTest.php @@ -13,8 +13,8 @@ namespace ApiPlatform\Serializer\Tests; -use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Patch; diff --git a/src/Serializer/Tests/SerializerFilterContextBuilderTest.php b/src/Serializer/Tests/SerializerFilterContextBuilderTest.php index 5427afc6823..8a4c497d876 100644 --- a/src/Serializer/Tests/SerializerFilterContextBuilderTest.php +++ b/src/Serializer/Tests/SerializerFilterContextBuilderTest.php @@ -14,8 +14,8 @@ namespace ApiPlatform\Serializer\Tests; use ApiPlatform\Api\FilterInterface; -use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index 91a580c3aee..5eb981b9834 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -30,11 +30,11 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\AttributesExtractor; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; use ApiPlatform\State\ProviderInterface; use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\Util\AttributesExtractor; use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; use Symfony\Component\Routing\RouterInterface; diff --git a/src/Symfony/Util/RequestAttributesExtractor.php b/src/Symfony/Util/RequestAttributesExtractor.php index 22306af5e59..74d549e505b 100644 --- a/src/Symfony/Util/RequestAttributesExtractor.php +++ b/src/Symfony/Util/RequestAttributesExtractor.php @@ -20,6 +20,7 @@ * Extracts data used by the library form a Request instance. * * @internal + * * @author Kévin Dunglas */ final class RequestAttributesExtractor diff --git a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php b/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php index 1eafea9c9a1..0d3dc6dd7e8 100644 --- a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php +++ b/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Symfony\Validator\Exception; -use ApiPlatform\Exception\ExceptionInterface; +use ApiPlatform\Metadata\Exception\ExceptionInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; /** diff --git a/src/Util/RequestAttributesExtractor.php b/src/Util/RequestAttributesExtractor.php index 7aefcf503d6..2d180f6742e 100644 --- a/src/Util/RequestAttributesExtractor.php +++ b/src/Util/RequestAttributesExtractor.php @@ -19,6 +19,7 @@ * Extracts data used by the library form a Request instance. * * @deprecated use \ApiPlatform\Symfony\Util\RequestAttributesExtractor although it should've been internal + * * @author Kévin Dunglas */ final class RequestAttributesExtractor