From 0fda38cd35e071c1b623f29e4611312b2237b09a Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 17 Nov 2021 20:45:13 +0100 Subject: [PATCH] feat: rector --- .github/workflows/ci.yml | 2 +- features/doctrine/eager_loading.feature | 4 +- features/jsonld/input_output.feature | 2 +- features/jsonld/non_resource.feature | 1 + rector.patch | 20 +++ .../Doctrine/Orm/State/CollectionProvider.php | 3 +- .../Doctrine/Orm/State/LinksHandlerTrait.php | 23 +-- .../Bridge/Doctrine/Orm/ItemDataProvider.php | 11 +- ...egacyApiResourceToApiResourceAttribute.php | 1 - .../Rector/Service/SubresourceTransformer.php | 12 +- .../CollectionFiltersNormalizer.php | 35 +---- .../Serializer/DocumentationNormalizer.php | 3 +- ...iResourceToLegacyResourceMetadataTrait.php | 4 +- ...plateResourceMetadataCollectionFactory.php | 11 +- src/OpenApi/Factory/OpenApiFactory.php | 3 +- src/Symfony/EventListener/WriteListener.php | 2 +- src/Symfony/Messenger/Processor.php | 8 +- .../Doctrine/Orm/State/ItemProviderTest.php | 143 ++++++------------ .../Doctrine/Orm/ItemDataProviderTest.php | 16 +- .../ApiPlatformExtensionTest.php | 1 + .../Twig/ApiPlatformProfilerPanelTest.php | 2 +- .../TestBundle/Entity/AttributeResource.php | 2 - .../TestBundle/Entity/AttributeResources.php | 4 +- tests/Fixtures/TestBundle/Entity/Company.php | 6 + tests/Fixtures/TestBundle/Entity/Employee.php | 1 - .../State/ContainNonResourceProvider.php | 54 +++++++ .../State/DummyDtoNoOutputProcessor.php | 77 ++++++++++ ...esourceInterfaceImplementationProvider.php | 51 +++++++ .../TestBundle/State/SerializableProvider.php | 49 ++++++ tests/Fixtures/app/config/config_common.yml | 18 +++ ...kResourceMetadataCollectionFactoryTest.php | 6 - 31 files changed, 392 insertions(+), 183 deletions(-) create mode 100644 rector.patch create mode 100644 tests/Fixtures/TestBundle/State/ContainNonResourceProvider.php create mode 100644 tests/Fixtures/TestBundle/State/DummyDtoNoOutputProcessor.php create mode 100644 tests/Fixtures/TestBundle/State/ResourceInterfaceImplementationProvider.php create mode 100644 tests/Fixtures/TestBundle/State/SerializableProvider.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac0df5ec089..a0682c139f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -841,7 +841,7 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Require Symfony components and Rector dependencies - run: composer require symfony/intl symfony/uid rector/rector --dev --no-interaction --no-progress --ansi + run: git apply rector.patch && composer require symfony/intl symfony/uid rector/rector --dev --no-interaction --no-progress --ansi - name: Install PHPUnit run: vendor/bin/simple-phpunit --version - name: Clear test app cache diff --git a/features/doctrine/eager_loading.feature b/features/doctrine/eager_loading.feature index 27ab9804abe..539330a1c54 100644 --- a/features/doctrine/eager_loading.feature +++ b/features/doctrine/eager_loading.feature @@ -17,7 +17,7 @@ Feature: Eager Loading LEFT JOIN thirdLevel_a1.fourthLevel fourthLevel_a2 LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a3 LEFT JOIN relatedToDummyFriend_a3.dummyFriend dummyFriend_a4 - WHERE o.id = :id_id + WHERE o.id = :id_p1 """ Scenario: Eager loading for the search filter @@ -74,7 +74,7 @@ Feature: Eager Loading FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel o LEFT JOIN o.car car_a1 LEFT JOIN o.passenger passenger_a2 - WHERE o.id = :id_id + WHERE o.id = :id_p1 """ Scenario: Eager loading for a relation with complex sub-query filter diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 361be332ff5..e4eb99e087c 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -282,7 +282,7 @@ Feature: JSON-LD DTO input and output @!mongodb Scenario: Use messenger with an input where the handler gives a synchronous result - And I send a "POST" request to "/messenger_with_inputs" with body: + When I send a "POST" request to "/messenger_with_inputs" with body: """ { "var": "test" diff --git a/features/jsonld/non_resource.feature b/features/jsonld/non_resource.feature index 446ff64d6fb..654d90d35bf 100644 --- a/features/jsonld/non_resource.feature +++ b/features/jsonld/non_resource.feature @@ -7,6 +7,7 @@ Feature: JSON-LD non-resource handling Given I add "Accept" header equal to "application/ld+json" And I add "Content-Type" header equal to "application/ld+json" + @createSchema Scenario: Get a resource containing a raw object When I send a "GET" request to "/contain_non_resources/1" Then the response status code should be 200 diff --git a/rector.patch b/rector.patch new file mode 100644 index 00000000000..46fca3dcf3b --- /dev/null +++ b/rector.patch @@ -0,0 +1,20 @@ +diff --git a/composer.json b/composer.json +index d5beb2afc..fca961b12 100644 +--- a/composer.json ++++ b/composer.json +@@ -47,10 +47,10 @@ + "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.1", + "phpdocumentor/type-resolver": "^0.3 || ^0.4 || ^1.4", + "phpstan/extension-installer": "^1.0", +- "phpstan/phpstan": "^0.12.65", +- "phpstan/phpstan-doctrine": "^0.12.7", +- "phpstan/phpstan-phpunit": "^0.12.4", +- "phpstan/phpstan-symfony": "^0.12.4", ++ "phpstan/phpstan": "*", ++ "phpstan/phpstan-doctrine": "*", ++ "phpstan/phpstan-phpunit": "*", ++ "phpstan/phpstan-symfony": "*", + "psr/log": "^1.0", + "ramsey/uuid": "^3.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4", + diff --git a/src/Bridge/Doctrine/Orm/State/CollectionProvider.php b/src/Bridge/Doctrine/Orm/State/CollectionProvider.php index e3f18fe07a8..fac972a49d5 100644 --- a/src/Bridge/Doctrine/Orm/State/CollectionProvider.php +++ b/src/Bridge/Doctrine/Orm/State/CollectionProvider.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ManagerRegistry; /** @@ -51,6 +52,7 @@ public function provide(string $resourceClass, array $identifiers = [], ?string /** @var EntityManagerInterface $manager */ $manager = $this->managerRegistry->getManagerForClass($resourceClass); + /** @var EntityRepository */ $repository = $manager->getRepository($resourceClass); if (!method_exists($repository, 'createQueryBuilder')) { throw new RuntimeException('The repository class must have a "createQueryBuilder" method.'); @@ -61,7 +63,6 @@ public function provide(string $resourceClass, array $identifiers = [], ?string $this->handleLinks($queryBuilder, $identifiers, $queryNameGenerator, $context, $resourceClass, $operationName); - // dd($queryBuilder->getQuery()); foreach ($this->collectionExtensions as $extension) { $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); diff --git a/src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php b/src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php index 82d156f031a..da1ae882ff2 100644 --- a/src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php +++ b/src/Bridge/Doctrine/Orm/State/LinksHandlerTrait.php @@ -64,6 +64,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $previousAlias = $alias; $previousIdentifiers = end($links)->getIdentifiers(); + $previousJoinProperty = $doctrineClassMetadata->getIdentifier()[0]; $expressions = []; $identifiers = array_reverse($identifiers); @@ -73,14 +74,10 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que } $identifierProperties = $link->getIdentifiers(); - $currentAlias = $queryNameGenerator->generateJoinAlias($alias); - - if ($link->getFromClass() === $resourceClass) { - $currentAlias = $alias; - } if (!$link->getFromProperty() && !$link->getToProperty()) { $doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass()); + $currentAlias = $link->getFromClass() === $resourceClass ? $alias : $queryNameGenerator->generateJoinAlias($alias); foreach ($identifierProperties as $identifierProperty) { $placeholder = $queryNameGenerator->generateParameterName($identifierProperty); @@ -90,22 +87,24 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $previousAlias = $currentAlias; $previousIdentifiers = $identifierProperties; + $previousJoinProperty = $doctrineClassMetadata->getIdentifier()[0]; continue; } if (1 < \count($previousIdentifiers) || 1 < \count($identifierProperties)) { - throw new RuntimeException('Composite identifiers on a relation can not be handled automatically, implement your own query.'); + throw new RuntimeException('Multiple identifiers on a relation can not be handled automatically, implement your own query.'); } $previousIdentifier = $previousIdentifiers[0]; $identifierProperty = $identifierProperties[0]; + $joinProperty = $doctrineClassMetadata->getIdentifier()[0]; $placeholder = $queryNameGenerator->generateParameterName($identifierProperty); if ($link->getFromProperty() && !$link->getToProperty()) { $doctrineClassMetadata = $manager->getClassMetadata($link->getFromClass()); $joinAlias = $queryNameGenerator->generateJoinAlias('m'); - $assocationMapping = $doctrineClassMetadata->getAssociationMappings()[$link->getFromProperty()]; - $relationType = $assocationMapping['type']; + $associationMapping = $doctrineClassMetadata->getAssociationMapping($link->getFromProperty()); + $relationType = $associationMapping['type']; if ($relationType & ClassMetadataInfo::TO_MANY) { $nextAlias = $queryNameGenerator->generateJoinAlias($alias); @@ -116,14 +115,14 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que } // A single-valued association path expression to an inverse side is not supported in DQL queries. - if ($relationType & ClassMetadataInfo::TO_ONE && !$assocationMapping['isOwningSide']) { - $queryBuilder->innerJoin("$previousAlias.".$assocationMapping['mappedBy'], $joinAlias); + if ($relationType & ClassMetadataInfo::TO_ONE && !($associationMapping['isOwningSide'] ?? true)) { + $queryBuilder->innerJoin("$previousAlias.".$associationMapping['mappedBy'], $joinAlias); } else { $queryBuilder->join( $link->getFromClass(), $joinAlias, 'with', - "{$previousAlias}.{$previousIdentifier} = $joinAlias.{$link->getFromProperty()}" + "{$previousAlias}.{$previousJoinProperty} = $joinAlias.{$link->getFromProperty()}" ); } @@ -131,6 +130,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty)); $previousAlias = $joinAlias; $previousIdentifier = $identifierProperty; + $previousJoinProperty = $joinProperty; continue; } @@ -140,6 +140,7 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que $queryBuilder->setParameter($placeholder, array_shift($identifiers), $doctrineClassMetadata->getTypeOfField($identifierProperty)); $previousAlias = $joinAlias; $previousIdentifier = $identifierProperty; + $previousJoinProperty = $joinProperty; } if ($expressions) { diff --git a/src/Core/Bridge/Doctrine/Orm/ItemDataProvider.php b/src/Core/Bridge/Doctrine/Orm/ItemDataProvider.php index 5d6db54a914..e02f3bbc631 100644 --- a/src/Core/Bridge/Doctrine/Orm/ItemDataProvider.php +++ b/src/Core/Bridge/Doctrine/Orm/ItemDataProvider.php @@ -101,7 +101,7 @@ public function getItem(string $resourceClass, $id, string $operationName = null $queryNameGenerator = new QueryNameGenerator(); $doctrineClassMetadata = $manager->getClassMetadata($resourceClass); - $this->addWhereForIdentifiers($identifiers, $queryBuilder, $doctrineClassMetadata); + $this->addWhereForIdentifiers($identifiers, $queryBuilder, $doctrineClassMetadata, $queryNameGenerator); foreach ($this->itemExtensions as $extension) { $extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context); @@ -116,19 +116,20 @@ public function getItem(string $resourceClass, $id, string $operationName = null /** * Add WHERE conditions to the query for one or more identifiers (simple or composite). + * + * @param mixed $queryNameGenerator */ - private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryBuilder, ClassMetadata $classMetadata) + private function addWhereForIdentifiers(array $identifiers, QueryBuilder $queryBuilder, ClassMetadata $classMetadata, $queryNameGenerator) { $alias = $queryBuilder->getRootAliases()[0]; foreach ($identifiers as $identifier => $value) { - $placeholder = ':id_'.$identifier; + $placeholder = $queryNameGenerator->generateParameterName($identifier); $expression = $queryBuilder->expr()->eq( "{$alias}.{$identifier}", - $placeholder + ':'.$placeholder ); $queryBuilder->andWhere($expression); - $queryBuilder->setParameter($placeholder, $value, $classMetadata->getTypeOfField($identifier)); } } diff --git a/src/Core/Bridge/Rector/Rules/AbstractLegacyApiResourceToApiResourceAttribute.php b/src/Core/Bridge/Rector/Rules/AbstractLegacyApiResourceToApiResourceAttribute.php index 1ab5478e678..d2ade0c1a5e 100644 --- a/src/Core/Bridge/Rector/Rules/AbstractLegacyApiResourceToApiResourceAttribute.php +++ b/src/Core/Bridge/Rector/Rules/AbstractLegacyApiResourceToApiResourceAttribute.php @@ -38,7 +38,6 @@ abstract class AbstractLegacyApiResourceToApiResourceAttribute extends AbstractR 'itemOperations' => [ 'get', 'put', - 'patch', 'delete', ], 'collectionOperations' => [ diff --git a/src/Core/Bridge/Rector/Service/SubresourceTransformer.php b/src/Core/Bridge/Rector/Service/SubresourceTransformer.php index b66fb70f780..ccabf8e6d64 100644 --- a/src/Core/Bridge/Rector/Service/SubresourceTransformer.php +++ b/src/Core/Bridge/Rector/Service/SubresourceTransformer.php @@ -30,8 +30,8 @@ final class SubresourceTransformer public function __construct() { - $this->ormMetadataFactory = new AnnotationDriver(new AnnotationReader()); - $this->odmMetadataFactory = new ODMAnnotationDriver(new AnnotationReader()); + $this->ormMetadataFactory = class_exists(AnnotationDriver::class) ? new AnnotationDriver(new AnnotationReader()) : null; + $this->odmMetadataFactory = class_exists(ODMAnnotationDriver::class) ? new ODMAnnotationDriver(new AnnotationReader()) : null; } public function toUriVariables(array $subresourceMetadata): array @@ -81,12 +81,16 @@ private function getDoctrineMetadata(string $class): ClassMetadata $metadata->initializeReflection(new RuntimeReflectionService()); try { - $this->ormMetadataFactory->loadMetadataForClass($class, $metadata); + if ($this->ormMetadataFactory) { + $this->ormMetadataFactory->loadMetadataForClass($class, $metadata); + } } catch (MappingException $e) { } try { - $this->odmMetadataFactory->loadMetadataForClass($class, $metadata); + if ($this->odmMetadataFactory) { + $this->odmMetadataFactory->loadMetadataForClass($class, $metadata); + } } catch (ODMMappingException $e) { } diff --git a/src/Core/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Core/Hydra/Serializer/CollectionFiltersNormalizer.php index 67af32ea151..23327a107f6 100644 --- a/src/Core/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Core/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -13,10 +13,11 @@ namespace ApiPlatform\Core\Hydra\Serializer; +use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Api\FilterCollection; use ApiPlatform\Core\Api\FilterInterface; use ApiPlatform\Core\Api\FilterLocatorTrait; -use ApiPlatform\Core\Api\ResourceClassResolverInterface; +use ApiPlatform\Core\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use Psr\Container\ContainerInterface; @@ -33,23 +34,21 @@ final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface { use FilterLocatorTrait; - private $collectionNormalizer; private $resourceMetadataFactory; private $resourceClassResolver; /** - * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection - * @param mixed $resourceMetadataFactory + * @param ContainerInterface|FilterCollection $filterLocator The new filter locator or the deprecated filter collection + * @param mixed $resourceMetadataFactory + * @param ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver */ - public function __construct(NormalizerInterface $collectionNormalizer, $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, $filterLocator) + public function __construct(NormalizerInterface $collectionNormalizer, $resourceMetadataFactory, $resourceClassResolver, $filterLocator) { $this->setFilterLocator($filterLocator); - $this->collectionNormalizer = $collectionNormalizer; $this->resourceMetadataFactory = $resourceMetadataFactory; $this->resourceClassResolver = $resourceClassResolver; - if (!$resourceMetadataFactory instanceof ResourceMetadataCollectionFactoryInterface) { trigger_deprecation('api-platform/core', '2.7', sprintf('Use "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, ResourceMetadataFactoryInterface::class)); } @@ -80,17 +79,13 @@ public function normalize($object, $format = null, array $context = []): array if (!\is_array($data)) { throw new UnexpectedValueException('Expected data to be an array'); } - if (!isset($context['resource_class']) || isset($context['api_sub_level'])) { return $data; } - $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']); $resourceFilters = null; - if ($this->resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - $operationName = $context['collection_operation_name'] ?? null; if (null === $operationName) { $resourceFilters = $resourceMetadata->getAttribute('filters', []); @@ -101,23 +96,19 @@ public function normalize($object, $format = null, array $context = []): array $operation = $context['operation'] ?? $this->resourceMetadataFactory->create($resourceClass)->getOperation($context['operation_name'] ?? null); $resourceFilters = $operation->getFilters(); } - if (!$resourceFilters) { return $data; } - $requestParts = parse_url($context['request_uri'] ?? ''); if (!\is_array($requestParts)) { return $data; } - $currentFilters = []; foreach ($resourceFilters as $filterId) { if ($filter = $this->getFilter($filterId)) { $currentFilters[] = $filter; } } - if ($currentFilters) { $data['hydra:search'] = $this->getSearch($resourceClass, $requestParts, $currentFilters); } @@ -147,20 +138,10 @@ private function getSearch(string $resourceClass, array $parts, array $filters): foreach ($filters as $filter) { foreach ($filter->getDescription($resourceClass) as $variable => $data) { $variables[] = $variable; - $mapping[] = [ - '@type' => 'IriTemplateMapping', - 'variable' => $variable, - 'property' => $data['property'], - 'required' => $data['required'], - ]; + $mapping[] = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $data['property'], 'required' => $data['required']]; } } - return [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => $mapping, - ]; + return ['@type' => 'hydra:IriTemplate', 'hydra:template' => sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), 'hydra:variableRepresentation' => 'BasicRepresentation', 'hydra:mapping' => $mapping]; } } diff --git a/src/Core/Hydra/Serializer/DocumentationNormalizer.php b/src/Core/Hydra/Serializer/DocumentationNormalizer.php index cc5d3580def..848512c5f46 100644 --- a/src/Core/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Core/Hydra/Serializer/DocumentationNormalizer.php @@ -29,6 +29,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use Symfony\Component\PropertyInfo\Type; @@ -342,7 +343,7 @@ private function getHydraOperations(string $resourceClass, $resourceMetadata, st $hydraOperations = []; foreach ($resourceMetadataCollection as $resourceMetadata) { foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - if (($operation->isCollection() ?? false) !== $collection) { + if (($operation instanceof Post || ($operation->isCollection() ?? false)) !== $collection) { continue; } diff --git a/src/Core/Metadata/Resource/ApiResourceToLegacyResourceMetadataTrait.php b/src/Core/Metadata/Resource/ApiResourceToLegacyResourceMetadataTrait.php index 6b638a5f1e1..bad95e5ba4d 100644 --- a/src/Core/Metadata/Resource/ApiResourceToLegacyResourceMetadataTrait.php +++ b/src/Core/Metadata/Resource/ApiResourceToLegacyResourceMetadataTrait.php @@ -96,8 +96,8 @@ private function transformUriVariablesToIdentifiers(array $arrayOperation): arra $arrayOperation['identifiers'] = []; foreach ($arrayOperation['uri_variables'] as $parameterName => $identifiedBy) { - if (1 === \count($identifiedBy->getIdentifiers())) { - $arrayOperation['identifiers'][$parameterName] = [$identifiedBy->getFromClass(), $identifiedBy->getIdentifiers()[0]]; + if (1 === \count($identifiedBy->getIdentifiers() ?? ['id'])) { + $arrayOperation['identifiers'][$parameterName] = [$identifiedBy->getFromClass(), $identifiedBy->getIdentifiers()[0] ?? ['id']]; continue; } diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 2a503592709..52bfa18928a 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -111,16 +111,21 @@ private function generateUriTemplate(Operation $operation): string private function configureUriVariables($operation) { // We will generate the collection route, don't initialize variables here - if ($operation instanceof Operation && $operation->isCollection() && !$operation->getUriTemplate() || ($operation instanceof Post && !$operation->getUriVariables())) { + if ($operation instanceof Operation && $operation->isCollection() && !$operation->getUriTemplate()) { return $operation; } if (!$operation->getUriVariables()) { $operation = $operation->withUriVariables($this->transformLinksToUriVariables($this->linkFactory->createLinksFromIdentifiers($operation))); } + $operation = $this->normalizeUriVariables($operation); if (!($uriTemplate = $operation->getUriTemplate())) { + if ($operation instanceof Post) { + return $operation->withUriVariables([]); + } + return $operation; } @@ -134,7 +139,7 @@ private function configureUriVariables($operation) return '_format' !== $v; }); - if (\count($variables) < \count($uriVariables)) { + if (\count($variables) !== \count($uriVariables)) { $newUriVariables = []; foreach ($variables as $variable) { if (isset($uriVariables[$variable])) { @@ -142,7 +147,7 @@ private function configureUriVariables($operation) continue; } - $newUriVariables[$variable] = (new Link())->withFromClass($operation->getClass())->withIdentifiers([$variable])->withParameterName($variable); + $newUriVariables[$variable] = (new Link())->withFromClass($operation->getClass())->withIdentifiers(['id'])->withParameterName($variable); } return $operation->withUriVariables($newUriVariables); diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index a093ac36a5a..988208da741 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -29,6 +29,7 @@ use ApiPlatform\Core\PathResolver\OperationPathResolverInterface; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -230,7 +231,7 @@ private function collectPaths(ApiResource $resource, ResourceMetadataCollection break; } - if (!$operation->isCollection()) { + if (!$operation->isCollection() && !$operation instanceof Post) { $responses['404'] = new Model\Response('Resource not found'); } diff --git a/src/Symfony/EventListener/WriteListener.php b/src/Symfony/EventListener/WriteListener.php index a152bf44a42..4236b5379de 100644 --- a/src/Symfony/EventListener/WriteListener.php +++ b/src/Symfony/EventListener/WriteListener.php @@ -75,7 +75,7 @@ public function onKernelView(ViewEvent $event): void return; } - $context = ['operation' => $operation]; + $context = ['operation' => $operation, 'resource_class' => $attributes['resource_class']]; try { $identifiers = $this->getOperationIdentifiers($operation, $request->attributes->all(), $attributes['resource_class']); } catch (InvalidIdentifierException $e) { diff --git a/src/Symfony/Messenger/Processor.php b/src/Symfony/Messenger/Processor.php index 903122e0c7f..11802e3388c 100644 --- a/src/Symfony/Messenger/Processor.php +++ b/src/Symfony/Messenger/Processor.php @@ -88,8 +88,12 @@ public function process($data, array $identifiers = [], ?string $operationName = public function supports($data, array $identifiers = [], ?string $operationName = null, array $context = []): bool { try { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($context['resource_class'] ?? $this->getObjectClass($data)); - $operation = $resourceMetadataCollection->getOperation($operationName ?? null); + if (isset($context['operation'])) { + $operation = $context['operation']; + } else { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($context['resource_class'] ?? $this->getObjectClass($data)); + $operation = $resourceMetadataCollection->getOperation($operationName); + } return false !== ($operation->getMessenger() ?? false); } catch (OperationNotFoundException $e) { diff --git a/tests/Bridge/Doctrine/Orm/State/ItemProviderTest.php b/tests/Bridge/Doctrine/Orm/State/ItemProviderTest.php index e9003284da5..1d656c340f9 100644 --- a/tests/Bridge/Doctrine/Orm/State/ItemProviderTest.php +++ b/tests/Bridge/Doctrine/Orm/State/ItemProviderTest.php @@ -44,8 +44,8 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Query\Expr; -use Doctrine\ORM\Query\Expr\Comparison; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ObjectRepository; @@ -67,22 +67,15 @@ public function testGetItemSingleIdentifier() $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $comparisonProphecy = $this->prophesize(Comparison::class); - $comparison = $comparisonProphecy->reveal(); - - $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.identifier', ':id_identifier')->willReturn($comparisonProphecy)->shouldBeCalled(); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); + $queryBuilderProphecy->andWhere('o.identifier = :identifier_p1')->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_identifier', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('identifier_p1', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); - $managerRegistry = $this->getManagerRegistry(OperationResource::class, [ + $managerRegistryProphecy = $this->getManagerRegistry(OperationResource::class, [ 'identifier' => [ 'type' => Types::INTEGER, ], @@ -98,7 +91,7 @@ public function testGetItemSingleIdentifier() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), OperationResource::class, ['identifier' => 1], 'get', $context)->shouldBeCalled(); - $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistry, [$extensionProphecy->reveal()]); + $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->provide(OperationResource::class, ['identifier' => 1], 'get', $context)); } @@ -113,17 +106,14 @@ public function testGetItemDoubleIdentifier() $queryProphecy = $this->prophesize(AbstractQuery::class); $queryProphecy->getOneOrNullResult()->willReturn([])->shouldBeCalled(); - $comparisonProphecy = $this->prophesize(Comparison::class); - $comparison = $comparisonProphecy->reveal(); - - $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.ida', ':id_ida')->willReturn($comparisonProphecy)->shouldBeCalled(); - $exprProphecy->eq('o.idb', ':id_idb')->willReturn($comparisonProphecy)->shouldBeCalled(); + // $exprProphecy = $this->prophesize(Expr::class); + // $exprProphecy->eq('o.ida', ':id_ida')->willReturn($comparisonProphecy)->shouldBeCalled(); + // $exprProphecy->eq('o.idb', ':id_idb')->willReturn($comparisonProphecy)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); + $queryBuilderProphecy->andWhere('o.idb = :idb_p1')->shouldBeCalled(); + $queryBuilderProphecy->andWhere('o.ida = :ida_p2')->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); $resourceMetadataFactoryProphecy->create(OperationResource::class)->willReturn(new ResourceMetadataCollection(OperationResource::class, [(new ApiResource())->withOperations(new Operations(['get' => (new Get())->withUriVariables([ @@ -137,12 +127,12 @@ public function testGetItemDoubleIdentifier() ]), ])]))])); - $queryBuilderProphecy->setParameter(':id_ida', 1, Types::INTEGER)->shouldBeCalled(); - $queryBuilderProphecy->setParameter(':id_idb', 2, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('idb_p1', 2, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('ida_p2', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); - $managerRegistry = $this->getManagerRegistry(OperationResource::class, [ + $managerRegistryProphecy = $this->getManagerRegistry(OperationResource::class, [ 'ida' => [ 'type' => Types::INTEGER, ], @@ -155,7 +145,7 @@ public function testGetItemDoubleIdentifier() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), OperationResource::class, ['ida' => 1, 'idb' => 2], 'get', $context)->shouldBeCalled(); - $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistry, [$extensionProphecy->reveal()]); + $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->provide(OperationResource::class, ['ida' => 1, 'idb' => 2], 'get', $context)); } @@ -167,21 +157,14 @@ public function testQueryResultExtension() { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $comparisonProphecy = $this->prophesize(Comparison::class); - $comparison = $comparisonProphecy->reveal(); - - $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.identifier', ':id_identifier')->willReturn($comparisonProphecy)->shouldBeCalled(); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); + $queryBuilderProphecy->andWhere('o.identifier = :identifier_p1')->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_identifier', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('identifier_p1', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); - $managerRegistry = $this->getManagerRegistry(OperationResource::class, [ + $managerRegistryProphecy = $this->getManagerRegistry(OperationResource::class, [ 'identifier' => [ 'type' => Types::INTEGER, ], @@ -199,7 +182,7 @@ public function testQueryResultExtension() ]), ])]))])); - $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistry, [$extensionProphecy->reveal()]); + $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->provide(OperationResource::class, ['identifier' => 1], 'get', $context)); } @@ -324,8 +307,10 @@ private function getMetadataFactories(string $resourceClass, array $identifiers) /** * Gets a mocked manager registry. + * + * @param mixed $classMetadatas */ - private function getManagerRegistry(string $resourceClass, array $identifierFields, QueryBuilder $queryBuilder): ManagerRegistry + private function getManagerRegistry(string $resourceClass, array $identifierFields, QueryBuilder $queryBuilder, $classMetadatas = []) { $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->getIdentifier()->willReturn(array_keys($identifierFields)); @@ -347,46 +332,51 @@ private function getManagerRegistry(string $resourceClass, array $identifierFiel $managerProphecy->getConnection()->willReturn($connectionProphecy); $managerProphecy->getRepository($resourceClass)->willReturn($repositoryProphecy->reveal()); + foreach ($classMetadatas as $class => $classMetadata) { + $managerProphecy->getClassMetadata($class)->willReturn($classMetadata); + } + $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass($resourceClass)->willReturn($managerProphecy->reveal()); - return $managerRegistryProphecy->reveal(); + return $managerRegistryProphecy; } /** * @requires PHP 8.0 */ - public function testGetSubResourceFromProperty() + public function testGetSubresourceFromProperty() { $queryProphecy = $this->prophesize(AbstractQuery::class); $queryProphecy->getOneOrNullResult()->willReturn([])->shouldBeCalled(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $comparisonProphecy = $this->prophesize(Comparison::class); - $comparison = $comparisonProphecy->reveal(); - - $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('company_a1.id', ':id_employeeId')->willReturn($comparisonProphecy)->shouldBeCalled(); - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->join(Employee::class, 'company_a1', 'with', 'o.id = company_a1.company')->shouldBeCalled(); + $queryBuilderProphecy->join(Employee::class, 'm_a1', 'with', 'o.id = m_a1.company')->shouldBeCalled(); $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_employeeId', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->andWhere('m_a1.id = :id_p1')->shouldBeCalled(); + $queryBuilderProphecy->setParameter('id_p1', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); - $managerRegistry = $this->getManagerRegistry(Company::class, [ - 'employeeId' => [ + $employeeClassMetadataProphecy = $this->prophesize(ClassMetadata::class); + $employeeClassMetadataProphecy->getAssociationMapping('company')->willReturn([ + 'type' => ClassMetadataInfo::TO_ONE, + ]); + $employeeClassMetadataProphecy->getTypeOfField('id')->willReturn(Types::INTEGER); + + $managerRegistryProphecy = $this->getManagerRegistry(Company::class, [ + 'id' => [ 'type' => Types::INTEGER, ], - ], $queryBuilder); + ], $queryBuilder, [ + Employee::class => $employeeClassMetadataProphecy->reveal(), + ]); $resourceMetadataFactoryProphecy->create(Company::class)->willReturn(new ResourceMetadataCollection(Company::class, [(new ApiResource())->withOperations(new Operations(['getCompany' => (new Get())->withUriVariables([ - 'employeeId' => (new Link())->withFromClass("ApiPlatform\Tests\Fixtures\TestBundle\Entity\Employee") + 'employeeId' => (new Link())->withFromClass(Employee::class) ->withIdentifiers([ 0 => 'id', ])->withFromProperty('company'), @@ -395,55 +385,8 @@ public function testGetSubResourceFromProperty() $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Company::class, ['employeeId' => 1], 'getCompany', [])->shouldBeCalled(); - $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistry, [$extensionProphecy->reveal()]); + $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistryProphecy->reveal(), [$extensionProphecy->reveal()]); $this->assertEquals([], $dataProvider->provide(Company::class, ['employeeId' => 1], 'getCompany')); } - - /** - * @requires PHP 8.0 - */ - public function testGetSubResourceProperty() - { - $queryProphecy = $this->prophesize(AbstractQuery::class); - $queryProphecy->getOneOrNullResult()->willReturn([])->shouldBeCalled(); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - - $comparisonProphecy = $this->prophesize(Comparison::class); - $comparison = $comparisonProphecy->reveal(); - - $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('company_a1.id', ':id_companyId')->willReturn($comparisonProphecy)->shouldBeCalled(); - - $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); - $queryBuilderProphecy->join('o.company', 'company_a1')->shouldBeCalled(); - $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); - $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); - $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_companyId', 1, Types::INTEGER)->shouldBeCalled(); - - $queryBuilder = $queryBuilderProphecy->reveal(); - - $managerRegistry = $this->getManagerRegistry(Employee::class, [ - 'companyId' => [ - 'type' => Types::INTEGER, - ], - ], $queryBuilder); - - $resourceMetadataFactoryProphecy->create(Employee::class)->willReturn(new ResourceMetadataCollection(Company::class, [(new ApiResource())->withOperations(new Operations(['getEmployees' => (new GetCollection())->withUriVariables([ - 'companyId' => (new Link())->withFromClass("ApiPlatform\Tests\Fixtures\TestBundle\Entity\Company") - ->withIdentifiers([ - 0 => 'id', - ])->withToProperty('company'), - ])]))])); - - $extensionProphecy = $this->prophesize(QueryItemExtensionInterface::class); - $extensionProphecy->applyToItem($queryBuilder, Argument::type(QueryNameGeneratorInterface::class), Employee::class, ['companyId' => 1], 'getEmployees', [])->shouldBeCalled(); - - $dataProvider = new ItemProvider($resourceMetadataFactoryProphecy->reveal(), $managerRegistry, [$extensionProphecy->reveal()]); - - $this->assertEquals([], $dataProvider->provide(Employee::class, ['companyId' => 1], 'getEmployees')); - } } diff --git a/tests/Core/Bridge/Doctrine/Orm/ItemDataProviderTest.php b/tests/Core/Bridge/Doctrine/Orm/ItemDataProviderTest.php index 6e0383d349a..694ee343f2d 100644 --- a/tests/Core/Bridge/Doctrine/Orm/ItemDataProviderTest.php +++ b/tests/Core/Bridge/Doctrine/Orm/ItemDataProviderTest.php @@ -61,14 +61,14 @@ public function testGetItemSingleIdentifier() $comparison = $comparisonProphecy->reveal(); $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.id', ':id_id')->willReturn($comparisonProphecy)->shouldBeCalled(); + $exprProphecy->eq('o.id', ':id_p1')->willReturn($comparisonProphecy)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_id', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('id_p1', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); @@ -98,8 +98,8 @@ public function testGetItemDoubleIdentifier() $comparison = $comparisonProphecy->reveal(); $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.ida', ':id_ida')->willReturn($comparisonProphecy)->shouldBeCalled(); - $exprProphecy->eq('o.idb', ':id_idb')->willReturn($comparisonProphecy)->shouldBeCalled(); + $exprProphecy->eq('o.ida', ':ida_p1')->willReturn($comparisonProphecy)->shouldBeCalled(); + $exprProphecy->eq('o.idb', ':idb_p2')->willReturn($comparisonProphecy)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $queryBuilderProphecy->getQuery()->willReturn($queryProphecy->reveal())->shouldBeCalled(); @@ -107,8 +107,8 @@ public function testGetItemDoubleIdentifier() $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_ida', 1, Types::INTEGER)->shouldBeCalled(); - $queryBuilderProphecy->setParameter(':id_idb', 2, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('ida_p1', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('idb_p2', 2, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); @@ -164,13 +164,13 @@ public function testQueryResultExtension() $comparison = $comparisonProphecy->reveal(); $exprProphecy = $this->prophesize(Expr::class); - $exprProphecy->eq('o.id', ':id_id')->willReturn($comparisonProphecy)->shouldBeCalled(); + $exprProphecy->eq('o.id', ':id_p1')->willReturn($comparisonProphecy)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); $queryBuilderProphecy->expr()->willReturn($exprProphecy->reveal())->shouldBeCalled(); $queryBuilderProphecy->andWhere($comparison)->shouldBeCalled(); $queryBuilderProphecy->getRootAliases()->shouldBeCalled()->willReturn(['o']); - $queryBuilderProphecy->setParameter(':id_id', 1, Types::INTEGER)->shouldBeCalled(); + $queryBuilderProphecy->setParameter('id_p1', 1, Types::INTEGER)->shouldBeCalled(); $queryBuilder = $queryBuilderProphecy->reveal(); diff --git a/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 7aaa980d3fc..89f39c05af0 100644 --- a/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Core/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -1532,6 +1532,7 @@ private function getBaseContainerBuilderProphecyWithoutDefaultMetadataLoading(ar 'api_platform.problem.normalizer.constraint_violation_list', 'api_platform.problem.normalizer.error', 'api_platform.rector.command', + 'api_platform.rector.subresource_transformer', 'api_platform.swagger.action.ui', 'api_platform.swagger.listener.ui', 'api_platform.validator', diff --git a/tests/Core/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Core/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php index a15aa0126cf..417dfd0fdfd 100644 --- a/tests/Core/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php +++ b/tests/Core/Bridge/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php @@ -230,7 +230,7 @@ public function testPostCollectionProcessorProfiler() // Metadata tab $tabContent = $crawler->filter('.metadata-tab-content'); - $this->assertSame('_api_/processor_entities.{_format}_post_collection', $tabContent->filter('th.status-success')->html(), 'The actual operation should be highlighted.'); + $this->assertSame('_api_/processor_entities.{_format}_post', $tabContent->filter('th.status-success')->html(), 'The actual operation should be highlighted.'); // Data provider tab $tabContent = $crawler->filter('.data-provider-tab-content'); diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index f86e1e7b83c..e5580704f17 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -17,7 +17,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Put; @@ -36,7 +35,6 @@ final class AttributeResource /** * @var ?Dummy */ - #[Link('dummyId')] public $dummy = null; /** diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index 32868184e43..97bd7119513 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -13,12 +13,12 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; -use Traversable; -use ArrayIterator; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; +use ArrayIterator; use IteratorAggregate; +use Traversable; #[ApiResource('/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true])] #[GetCollection] diff --git a/tests/Fixtures/TestBundle/Entity/Company.php b/tests/Fixtures/TestBundle/Entity/Company.php index 29155326a71..5d1d259c348 100644 --- a/tests/Fixtures/TestBundle/Entity/Company.php +++ b/tests/Fixtures/TestBundle/Entity/Company.php @@ -32,6 +32,12 @@ ], )] #[Get] +#[ApiResource( + uriTemplate: '/employees/{employeeId}/company', + uriVariables: [ + 'employeeId' => ['from_class' => Employee::class, 'from_property' => 'company'], + ], +)] class Company { /** diff --git a/tests/Fixtures/TestBundle/Entity/Employee.php b/tests/Fixtures/TestBundle/Entity/Employee.php index c2cce8191d6..48cddc69ff5 100644 --- a/tests/Fixtures/TestBundle/Entity/Employee.php +++ b/tests/Fixtures/TestBundle/Entity/Employee.php @@ -16,7 +16,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Post; use Doctrine\ORM\Mapping as ORM; diff --git a/tests/Fixtures/TestBundle/State/ContainNonResourceProvider.php b/tests/Fixtures/TestBundle/State/ContainNonResourceProvider.php new file mode 100644 index 00000000000..376fb4bb1f2 --- /dev/null +++ b/tests/Fixtures/TestBundle/State/ContainNonResourceProvider.php @@ -0,0 +1,54 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\NotAResource; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\ContainNonResource as ContainNonResourceDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ContainNonResource; + +/** + * @author Kévin Dunglas + */ +class ContainNonResourceProvider implements ProviderInterface +{ + /** + * {@inheritDoc} + */ + public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []) + { + $id = $identifiers['id'] ?? null; + if (!is_scalar($id)) { + throw new \InvalidArgumentException('The id must be a scalar.'); + } + + // Retrieve the blog post item from somewhere + $cnr = new $resourceClass(); + $cnr->id = $id; + $cnr->notAResource = new NotAResource('f1', 'b1'); + $cnr->nested = new $resourceClass(); + $cnr->nested->id = "$id-nested"; + $cnr->nested->notAResource = new NotAResource('f2', 'b2'); + + return $cnr; + } + + /** + * {@inheritDoc} + */ + public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool + { + return \in_array($resourceClass, [ContainNonResource::class, ContainNonResourceDocument::class], true); + } +} diff --git a/tests/Fixtures/TestBundle/State/DummyDtoNoOutputProcessor.php b/tests/Fixtures/TestBundle/State/DummyDtoNoOutputProcessor.php new file mode 100644 index 00000000000..83cc86e6d17 --- /dev/null +++ b/tests/Fixtures/TestBundle/State/DummyDtoNoOutputProcessor.php @@ -0,0 +1,77 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Dto\Document\InputDto as InputDtoDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Dto\InputDto; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput; +use Doctrine\Persistence\ManagerRegistry; + +class DummyDtoNoOutputProcessor implements ProcessorInterface +{ + private $registry; + + public function __construct(ManagerRegistry $registry) + { + $this->registry = $registry; + } + + /** + * {@inheritdoc} + */ + public function supports($data): bool + { + return $data instanceof InputDto || $data instanceof InputDtoDocument; + } + + /** + * {@inheritdoc} + */ + public function remove($data) + { + throw new \RuntimeException('Not implemented'); + } + + /** + * {@inheritDoc} + */ + public function resumable(?string $operationName = null, array $context = []): bool + { + return false; + } + + /** + * {@inheritDoc} + */ + public function process($data, array $identifiers = [], ?string $operationName = null, array $context = []) + { + $isOrm = true; + $em = $this->registry->getManagerForClass(DummyDtoNoOutput::class); + if (null === $em) { + $em = $this->registry->getManagerForClass(DummyDtoNoOutputDocument::class); + $isOrm = false; + } + + $output = $isOrm ? new DummyDtoNoOutput() : new DummyDtoNoOutputDocument(); + $output->lorem = $data->foo; + $output->ipsum = (string) $data->bar; + + $em->persist($output); + $em->flush(); + + return $output; + } +} diff --git a/tests/Fixtures/TestBundle/State/ResourceInterfaceImplementationProvider.php b/tests/Fixtures/TestBundle/State/ResourceInterfaceImplementationProvider.php new file mode 100644 index 00000000000..3c8088535ee --- /dev/null +++ b/tests/Fixtures/TestBundle/State/ResourceInterfaceImplementationProvider.php @@ -0,0 +1,51 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\ResourceInterfaceImplementation; + +final class ResourceInterfaceImplementationProvider implements ProviderInterface +{ + /** + * {@inheritDoc} + */ + public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []) + { + /** @var Operation */ + $operation = $context['operation']; + if ($operation->isCollection()) { + yield (new ResourceInterfaceImplementation())->setFoo('item1'); + yield (new ResourceInterfaceImplementation())->setFoo('item2'); + + return; + } + + if ('some-id' === $identifiers['id']) { + return (new ResourceInterfaceImplementation())->setFoo('single item'); + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool + { + return ResourceInterface::class === $resourceClass; + } +} diff --git a/tests/Fixtures/TestBundle/State/SerializableProvider.php b/tests/Fixtures/TestBundle/State/SerializableProvider.php new file mode 100644 index 00000000000..add710c7b23 --- /dev/null +++ b/tests/Fixtures/TestBundle/State/SerializableProvider.php @@ -0,0 +1,49 @@ + + * + * 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\Tests\Fixtures\TestBundle\State; + +use ApiPlatform\Core\DataProvider\SerializerAwareDataProviderTrait; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Model\SerializableResource; + +/** + * @author Vincent Chalamon + */ +class SerializableProvider implements ProviderInterface +{ + use SerializerAwareDataProviderTrait; + + /** + * {@inheritDoc} + */ + public function provide(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []) + { + return $this->getSerializer()->deserialize(<<<'JSON' +{ + "id": 1, + "foo": "Lorem", + "bar": "Ipsum" +} +JSON + , $resourceClass, 'json'); + } + + /** + * {@inheritDoc} + */ + public function supports(string $resourceClass, array $identifiers = [], ?string $operationName = null, array $context = []): bool + { + return SerializableResource::class === $resourceClass; + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index a6db970211b..121e88e4a67 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -117,6 +117,24 @@ services: tags: - { name: 'api_platform.state_processor' } + contain_non_resource.state_provider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\ContainNonResourceProvider' + public: false + tags: + - { name: 'api_platform.state_provider' } + + serializable.state_provider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\SerializableProvider' + public: false + tags: + - { name: 'api_platform.state_provider' } + + resource_interface.state_provider: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\State\ResourceInterfaceImplementationProvider' + public: false + tags: + - { name: 'api_platform.state_provider' } + contain_non_resource.item_data_provider: class: 'ApiPlatform\Tests\Fixtures\TestBundle\DataProvider\ContainNonResourceItemDataProvider' public: false diff --git a/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php index b506ae5992b..916bc0bd3a7 100644 --- a/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactoryTest.php @@ -14,23 +14,17 @@ namespace ApiPlatform\Tests\Metadata\Resource\Factory; use ApiPlatform\Core\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\Operation\PathSegmentNameGeneratorInterface; use ApiPlatform\Core\Tests\ProphecyTrait; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Delete; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\LinkFactory; use ApiPlatform\Metadata\Resource\Factory\LinkResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;