From 41bbad94e93df49eb4ade0fe1307b20d9cd07102 Mon Sep 17 00:00:00 2001 From: helyakin Date: Tue, 18 Oct 2022 11:12:57 +0200 Subject: [PATCH 01/19] fix: update yaml extractor test file coding standard (#5068) --- tests/Metadata/Extractor/YamlExtractorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index 843df41f723..27e469614e9 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -471,7 +471,7 @@ public function testInvalidYaml(string $path, string $error): void public function getInvalidPaths(): array { return [ - [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml'.'".'], + [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml".'], ]; } } From 541b738e942156b711665952b50fbd4f060fcdea Mon Sep 17 00:00:00 2001 From: ArnoudThibaut Date: Fri, 21 Oct 2022 14:16:58 +0200 Subject: [PATCH 02/19] fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) --- CHANGELOG.md | 4 +++ src/GraphQl/Action/EntrypointAction.php | 6 ++-- .../ApiPlatformExtension.php | 17 +++++++-- .../Bundle/Resources/config/graphql.xml | 4 +-- .../ApiPlatformExtensionTest.php | 35 +++++++++++++++++++ 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be83b8dcb3..2e65c47102a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.0.3 + +* Graphql: add a clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) + ## 3.0.2 * Metadata: generate skolem IRI by default, use `genId: false` to disable **BC** diff --git a/src/GraphQl/Action/EntrypointAction.php b/src/GraphQl/Action/EntrypointAction.php index f4c9af93e68..dcdd4a1dfd8 100644 --- a/src/GraphQl/Action/EntrypointAction.php +++ b/src/GraphQl/Action/EntrypointAction.php @@ -34,7 +34,7 @@ final class EntrypointAction { private int $debug; - public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly GraphiQlAction $graphiQlAction, private readonly GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) + public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly ?GraphiQlAction $graphiQlAction, private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) { $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; } @@ -43,11 +43,11 @@ public function __invoke(Request $request): Response { try { if ($request->isMethod('GET') && 'html' === $request->getRequestFormat()) { - if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled) { + if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) { return ($this->graphiQlAction)($request); } - if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled) { + if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) { return ($this->graphQlPlaygroundAction)($request); } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index fcc023abb68..3b1c8faeb5a 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -53,6 +53,7 @@ use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; +use Twig\Environment; /** * The extension of this bundle. @@ -462,9 +463,12 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array { $enabled = $this->isConfigEnabled($container, $config['graphql']); + $graphiqlEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql']); + $graphqlPlayGroundEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground']); + $container->setParameter('api_platform.graphql.enabled', $enabled); - $container->setParameter('api_platform.graphql.graphiql.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql'])); - $container->setParameter('api_platform.graphql.graphql_playground.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground'])); + $container->setParameter('api_platform.graphql.graphiql.enabled', $graphiqlEnabled); + $container->setParameter('api_platform.graphql.graphql_playground.enabled', $graphqlPlayGroundEnabled); $container->setParameter('api_platform.graphql.collection.pagination', $config['graphql']['collection']['pagination']); if (!$enabled) { @@ -476,6 +480,15 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array $loader->load('graphql.xml'); + // @phpstan-ignore-next-line because PHPStan uses the container of the test env cache and in test the parameter kernel.bundles always contains the key TwigBundle + if (!class_exists(Environment::class) || !isset($container->getParameter('kernel.bundles')['TwigBundle'])) { + if ($graphiqlEnabled || $graphqlPlayGroundEnabled) { + throw new RuntimeException(sprintf('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the %s environnement or disable GraphiQL and GraphQL Playground.', $container->getParameter('kernel.environment'))); + } + $container->removeDefinition('api_platform.graphql.action.graphiql'); + $container->removeDefinition('api_platform.graphql.action.graphql_playground'); + } + $container->registerForAutoconfiguration(QueryItemResolverInterface::class) ->addTag('api_platform.graphql.query_resolver'); $container->registerForAutoconfiguration(QueryCollectionResolverInterface::class) diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index debaefb508c..b5a8e4e80e5 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -49,8 +49,8 @@ - - + + %kernel.debug% diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index cf89456b6af..006ad7e788d 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -70,7 +70,9 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Uid\AbstractUid; @@ -163,6 +165,7 @@ protected function setUp(): void 'kernel.bundles' => [ 'DoctrineBundle' => DoctrineBundle::class, 'SecurityBundle' => SecurityBundle::class, + 'TwigBundle' => TwigBundle::class, ], 'kernel.bundles_metadata' => [ 'TestBundle' => [ @@ -173,6 +176,7 @@ protected function setUp(): void ], 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', 'kernel.debug' => false, + 'kernel.environment' => 'test', ]); $this->container = new ContainerBuilder($containerParameterBag); @@ -693,6 +697,37 @@ public function testGraphQlConfiguration(): void $this->assertServiceHasTags('api_platform.graphql.normalizer.runtime_exception', ['serializer.normalizer']); } + public function testRuntimeExceptionIsThrownIfTwigIsNotEnabledButGraphqlClientsAre(): void + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['graphql']['enabled'] = true; + $this->container->getParameterBag()->set('kernel.bundles', [ + 'DoctrineBundle' => DoctrineBundle::class, + 'SecurityBundle' => SecurityBundle::class, + ]); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the test environnement or disable GraphiQL and GraphQL Playground.'); + + (new ApiPlatformExtension())->load($config, $this->container); + } + + public function testGraphqlClientsDefinitionsAreRemovedIfDisabled(): void + { + $config = self::DEFAULT_CONFIG; + $config['api_platform']['graphql']['enabled'] = true; + $config['api_platform']['graphql']['graphiql']['enabled'] = false; + $config['api_platform']['graphql']['graphql_playground']['enabled'] = false; + $this->container->getParameterBag()->set('kernel.bundles', [ + 'DoctrineBundle' => DoctrineBundle::class, + 'SecurityBundle' => SecurityBundle::class, + ]); + + (new ApiPlatformExtension())->load($config, $this->container); + + $this->assertNotContainerHasService('api_platform.graphql.action.graphiql'); + $this->assertNotContainerHasService('api_platform.graphql.action.graphql_playground'); + } + public function testDoctrineOrmConfiguration(): void { $config = self::DEFAULT_CONFIG; From 9c19fa17110aac7dd39bff827091c00b42a80d4f Mon Sep 17 00:00:00 2001 From: helyakin Date: Fri, 21 Oct 2022 17:49:02 +0200 Subject: [PATCH 03/19] fix(metadata): add class key in payload argument resolver (#5067) * fix: add class key in payload argument resolver * add null if everything else goes wrong --- .../Bundle/ArgumentResolver/PayloadArgumentResolver.php | 2 +- .../Bundle/ArgumentResolver/PayloadArgumentResolverTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index 83f0aa495c6..c8b000d1165 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -73,7 +73,7 @@ private function getExpectedInputClass(Request $request): ?string $context = $this->serializationContextBuilder->createFromRequest($request, false, RequestAttributesExtractor::extractAttributes($request)); - return $context['input'] ?? $context['resource_class']; + return $context['input']['class'] ?? $context['resource_class'] ?? null; } } diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index 0b099d79146..ed2ea20bfeb 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -234,7 +234,7 @@ private function createArgumentResolver(): PayloadArgumentResolver (new ApiResource())->withShortName('ResourceImplementation')->withOperations(new Operations([ 'update' => new Put(), 'update_no_deserialize' => (new Put())->withDeserialize(false), - 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class]), + 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class, 'name' => 'NotResource']), 'create' => new Post(), ])), ])); @@ -255,7 +255,7 @@ private function createArgumentResolver(): PayloadArgumentResolver ]; if ('update_with_dto' === $request->attributes->get('_api_operation_name')) { - $context['input'] = NotResource::class; + $context['input'] = ['class' => NotResource::class, 'name' => 'NotResource']; } else { $context['input'] = null; } From 1b64ebf6a438222ae091ec3690063d0fb1b61977 Mon Sep 17 00:00:00 2001 From: davy-beauzil <38990335+davy-beauzil@users.noreply.github.com> Date: Mon, 24 Oct 2022 11:07:15 +0200 Subject: [PATCH 04/19] fix: upgrade command remove ApiSubresource attribute (#5049) Fixes #5038 --- .../Upgrade/UpgradeApiResourceVisitor.php | 19 ++- .../Command/UpgradeApiResourceCommandTest.php | 159 +++++++++++------- .../Entity/DummyToUpgradeProduct.php | 49 ++++++ .../DummyToUpgradeWithOnlyAnnotation.php | 61 +++++++ .../DummyToUpgradeWithOnlyAttribute.php | 49 ++++++ 5 files changed, 273 insertions(+), 64 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php create mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php diff --git a/src/Core/Upgrade/UpgradeApiResourceVisitor.php b/src/Core/Upgrade/UpgradeApiResourceVisitor.php index e36d85e0916..ead9a207f01 100644 --- a/src/Core/Upgrade/UpgradeApiResourceVisitor.php +++ b/src/Core/Upgrade/UpgradeApiResourceVisitor.php @@ -125,11 +125,8 @@ public function enterNode(Node $node) } if ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Interface_) { - if ($this->isAnnotation) { - $this->removeAnnotation($node); - } else { - $this->removeAttribute($node); - } + $this->removeAnnotation($node); + $this->removeAttribute($node); $arguments = []; $operations = null === $this->resourceAnnotation->itemOperations && null === $this->resourceAnnotation->collectionOperations ? null : array_merge( @@ -361,13 +358,23 @@ private function removeAttribute(Node\Stmt\Class_|Node\Stmt\Interface_ $node) } } } + foreach ($node->stmts as $k => $stmts) { + foreach ($stmts->attrGroups as $i => $attrGroups) { + foreach ($attrGroups->attrs as $j => $attrs) { + if (str_ends_with(implode('\\', $attrs->name->parts), 'ApiSubresource')) { + unset($node->stmts[$k]->attrGroups[$i]); + break; + } + } + } + } } private function removeAnnotation(Node\Stmt\Class_|Node\Stmt\Interface_ $node) { $comment = $node->getDocComment(); - if (preg_match('/@ApiResource/', $comment->getText())) { + if ($comment && preg_match('/@ApiResource/', $comment->getText())) { $node->setDocComment($this->removeAnnotationByTag($comment, 'ApiResource')); } } diff --git a/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php index baf9c3e103f..6e54aa91755 100644 --- a/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php +++ b/tests/Core/Bridge/Symfony/Bundle/Command/UpgradeApiResourceCommandTest.php @@ -22,7 +22,8 @@ use ApiPlatform\Core\Upgrade\SubresourceTransformer; use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceNameCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAnnotation; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyToUpgradeWithOnlyAttribute; use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; @@ -49,75 +50,117 @@ private function getCommandTester(ResourceNameCollectionFactoryInterface $resour /** * @requires PHP 8.1 + * + * @dataProvider debugResourceProvider */ - public function testDebugResource() + public function testDebugResource(string $entityClass, array $subresourceOperationFactoryReturn, array $expectedStrings) { $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); - $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([RelatedDummy::class])); + $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection([$entityClass])); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadata()); + $resourceMetadataFactoryProphecy->create($entityClass)->willReturn(new ResourceMetadata()); $subresourceOperationFactoryProphecy = $this->prophesize(SubresourceOperationFactoryInterface::class); - $subresourceOperationFactoryProphecy->create(RelatedDummy::class)->willReturn([[ - 'property' => 'id', - 'collection' => false, - 'resource_class' => RelatedDummy::class, - 'shortNames' => [ - 'RelatedDummy', - ], - 'legacy_filters' => [ - 'related_dummy.friends', - 'related_dummy.complex_sub_query', - ], - 'legacy_normalization_context' => [ - 'groups' => [ - 'friends', - ], - ], - 'legacy_type' => 'https://schema.org/Product', - 'identifiers' => [ - 'id' => [ - RelatedDummy::class, - 'id', - true, - ], - ], - 'operation_name' => 'id_get_subresource', - 'route_name' => 'api_related_dummies_id_get_subresource', - 'path' => '/related_dummies/{id}/id.{_format}', - ]]); + $subresourceOperationFactoryProphecy->create($entityClass)->willReturn($subresourceOperationFactoryReturn); $commandTester = $this->getCommandTester($resourceNameCollectionFactoryProphecy->reveal(), $resourceMetadataFactoryProphecy->reveal(), $subresourceOperationFactoryProphecy->reveal()); $commandTester->execute([]); - $expectedStrings = [ - '-use ApiPlatform\\Core\\Annotation\\ApiSubresource', - '-use ApiPlatform\\Core\\Annotation\\ApiProperty', - '-use ApiPlatform\\Core\\Annotation\\ApiResource', - '-use ApiPlatform\\Core\\Annotation\\ApiFilter', - '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;', - '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;', - '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;', - '+use ApiPlatform\\Metadata\\ApiProperty', - '+use ApiPlatform\\Metadata\\ApiResource', - '+use ApiPlatform\\Metadata\\ApiFilter', - '+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter', - '+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter', - '+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter', - '+use ApiPlatform\\Metadata\\Get', - "+#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])]", - "#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]", - "+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]", - '+ #[ApiFilter(filterClass: SearchFilter::class)]', - '+ #[ApiFilter(filterClass: ExistsFilter::class)]', - '+ #[ApiFilter(filterClass: DateFilter::class)]', - '+ #[ApiProperty(writable: false)]', - "+ #[ApiProperty(iris: ['RelatedDummy.name'])]", - "+ #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')]", - ]; - $display = $commandTester->getDisplay(); foreach ($expectedStrings as $expectedString) { $this->assertStringContainsString($expectedString, $display); } } + + public function debugResourceProvider(): array + { + $entityClasses = [ + 'only_annotation' => DummyToUpgradeWithOnlyAnnotation::class, + 'only_attribute' => DummyToUpgradeWithOnlyAttribute::class, + ]; + + return array_map(function ($key, $entityClass) { + $expectedStrings = [ + '+#[ApiResource]', + '-use ApiPlatform\\Core\\Annotation\\ApiSubresource', + '-use ApiPlatform\\Core\\Annotation\\ApiProperty', + '-use ApiPlatform\\Core\\Annotation\\ApiResource', + '+use ApiPlatform\\Metadata\\ApiProperty', + '+use ApiPlatform\\Metadata\\ApiResource', + '+use ApiPlatform\\Metadata\\ApiFilter', + '+use ApiPlatform\\Metadata\\Get', + sprintf("#[ApiResource(uriTemplate: '/%s/{id}/name.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]", $key), + ]; + + if (DummyToUpgradeWithOnlyAnnotation::class === $entityClass) { + array_push($expectedStrings, + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\SearchFilter', + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\ExistsFilter', + '+use ApiPlatform\\Doctrine\\Orm\\Filter\\DateFilter', + '-use ApiPlatform\\Core\\Annotation\\ApiFilter', + '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;', + '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\ExistsFilter;', + '-use ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\DateFilter;', + '- * @ApiResource', + '- * @ApiFilter(SearchFilter::class, properties={"id"})', + "+#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])]", + '- * @ApiProperty(writable=false)', + '+ #[ApiProperty(writable: false)]', + '- * @ApiSubresource', + '- * @ApiFilter(DateFilter::class)', + '- * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct")', + "+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct'])]", + '- * @ApiFilter(SearchFilter::class)', + '- * @ApiFilter(ExistsFilter::class)', + '+ #[ApiFilter(filterClass: SearchFilter::class)]', + '+ #[ApiFilter(filterClass: ExistsFilter::class)]', + '+ #[ApiFilter(filterClass: DateFilter::class)]' + ); + } + + if (DummyToUpgradeWithOnlyAttribute::class === $entityClass) { + array_push($expectedStrings, + '-#[ApiResource()]', + "+#[ApiResource(uriTemplate: '/only_attribute/{id}/name.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])]", + '- #[ApiSubresource]', + "- #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')]", + "+ #[ApiProperty(iris: ['DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct'])]" + ); + } + + return [ + $entityClass, + [ + [ + 'property' => 'id', + 'collection' => false, + 'resource_class' => $entityClass, + 'shortNames' => [ + substr($entityClass, (\strlen($entityClass) - strrpos($entityClass, '\\') - 1) * (-1)), + ], + 'legacy_filters' => [ + 'related_dummy.friends', + 'related_dummy.complex_sub_query', + ], + 'legacy_normalization_context' => [ + 'groups' => [ + 'friends', + ], + ], + 'legacy_type' => 'https://schema.org/Product', + 'identifiers' => [ + 'id' => [ + $entityClass, + 'id', + true, + ], + ], + 'operation_name' => 'name_get_subresource', + 'route_name' => sprintf('api_%s_name_get_subresource', $key), + 'path' => sprintf('/%s/{id}/name.{_format}', $key), + ], + ], + array_merge($expectedStrings), + ]; + }, array_keys($entityClasses), array_values($entityClasses)); + } } diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php new file mode 100644 index 00000000000..cabc7a4352c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.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\Entity; + +use ApiPlatform\Core\Annotation\ApiResource; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + * + * @ApiResource + */ +class DummyToUpgradeProduct +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class) + */ + private $dummysToUpgradeWithOnlyAnnotation; + + /** + * @var Collection + * + * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class) + */ + private $dummysToUpgradeWithOnlyAttribute; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php new file mode 100644 index 00000000000..576ca2a55f0 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php @@ -0,0 +1,61 @@ + + * + * 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\Entity; + +use ApiPlatform\Core\Annotation\ApiFilter; +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + * + * @ApiResource + * + * @ApiFilter(SearchFilter::class, properties={"id"}) + */ +class DummyToUpgradeWithOnlyAnnotation +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * @Groups({"chicago", "friends"}) + * @ApiProperty(writable=false) + * @ApiFilter(DateFilter::class) + */ + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation") + * @ORM\JoinColumn(nullable=false) + * @Groups({"barcelona", "chicago", "friends"}) + * + * @ApiSubresource + * + * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct") + * @ApiFilter(SearchFilter::class) + * @ApiFilter(ExistsFilter::class) + */ + private $dummyToUpgradeProduct; +} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php new file mode 100644 index 00000000000..f9a1fd1f592 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.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\Entity; + +use ApiPlatform\Core\Annotation\ApiProperty; +use ApiPlatform\Core\Annotation\ApiResource; +use ApiPlatform\Core\Annotation\ApiSubresource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + */ +#[ApiResource()] +class DummyToUpgradeWithOnlyAttribute +{ + /** + * @var int + * + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + */ + #[Groups(['chicago', 'friends'])] + #[ApiProperty(writable: false)] + private $id; + + /** + * @var DummyToUpgradeProduct + * + * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute") + * @ORM\JoinColumn(nullable=false) + */ + #[Groups(['barcelona', 'chicago', 'friends'])] + #[ApiSubresource] + #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')] + private $dummyToUpgradeProduct; +} From 7044c5a1b2895e72f0579d1e788740606f94dece Mon Sep 17 00:00:00 2001 From: Baptiste Leduc Date: Mon, 24 Oct 2022 11:09:52 +0200 Subject: [PATCH 05/19] fix(doctrine): use abitrary index instead of value (#5079) --- src/Doctrine/Orm/Filter/SearchFilter.php | 6 ++++-- .../Common/Filter/SearchFilterTestTrait.php | 12 ++++++++++++ tests/Doctrine/Odm/Filter/SearchFilterTest.php | 15 +++++++++++++++ tests/Doctrine/Orm/Filter/SearchFilterTest.php | 8 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 3f35706b3b5..8401ebb842a 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -179,7 +179,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild $parameters = []; foreach ($values as $key => $value) { $keyValueParameter = sprintf('%s_%s', $valueParameter, $key); - $parameters[$caseSensitive ? $value : strtolower($value)] = $keyValueParameter; + $parameters[] = [$caseSensitive ? $value : strtolower($value), $keyValueParameter]; $ors[] = match ($strategy) { self::STRATEGY_PARTIAL => $queryBuilder->expr()->like( @@ -209,7 +209,9 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild } $queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors)); - array_walk($parameters, $queryBuilder->setParameter(...)); + foreach ($parameters as $parameter) { + $queryBuilder->setParameter($parameter[1], $parameter[0]); + } } /** diff --git a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php index a7fc3780999..51b8811918d 100644 --- a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php @@ -297,6 +297,18 @@ private function provideApplyTestArguments(): array ], ], ], + 'partial (multiple almost same values; case insensitive)' => [ + [ + 'id' => null, + 'name' => 'ipartial', + ], + [ + 'name' => [ + 'blue car', + 'Blue Car', + ], + ], + ], 'start' => [ [ 'id' => null, diff --git a/tests/Doctrine/Odm/Filter/SearchFilterTest.php b/tests/Doctrine/Odm/Filter/SearchFilterTest.php index 28b721f3475..5dd394ce965 100644 --- a/tests/Doctrine/Odm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Odm/Filter/SearchFilterTest.php @@ -426,6 +426,21 @@ public function provideApplyTestData(): array ], $filterFactory, ], + 'partial (multiple almost same values; case insensitive)' => [ + [ + [ + '$match' => [ + 'name' => [ + '$in' => [ + new Regex('blue car', 'i'), + new Regex('Blue Car', 'i'), + ], + ], + ], + ], + ], + $filterFactory, + ], 'start' => [ [ [ diff --git a/tests/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Doctrine/Orm/Filter/SearchFilterTest.php index 2d2f2a79e1f..7ad972534a7 100644 --- a/tests/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Orm/Filter/SearchFilterTest.php @@ -352,6 +352,14 @@ public function provideApplyTestData(): array ], $filterFactory, ], + 'partial (multiple almost same values; case insensitive)' => [ + sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_0, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_1, \'%%\'))', $this->alias, Dummy::class), + [ + 'name_p1_0' => 'blue car', + 'name_p1_1' => 'blue car', + ], + $filterFactory, + ], 'start' => [ sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1_0, \'%%\')', $this->alias, Dummy::class), ['name_p1_0' => 'partial'], From a4cd12b2a73bc0f726c5724de790f885884e6113 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Mon, 24 Oct 2022 16:11:25 +0200 Subject: [PATCH 06/19] fix: uri template should respect rfc 6570 (#5080) --- ...plateResourceMetadataCollectionFactory.php | 8 ++++++-- src/OpenApi/Factory/OpenApiFactory.php | 3 ++- .../NormalizeOperationNameTrait.php | 3 +-- src/Symfony/Routing/ApiLoader.php | 5 +++++ .../TestBundle/Document/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Document/Answer.php | 4 ++-- tests/Fixtures/TestBundle/Document/Book.php | 2 +- tests/Fixtures/TestBundle/Document/Dummy.php | 4 ++-- .../Document/DummyAggregateOffer.php | 4 ++-- .../TestBundle/Document/DummyOffer.php | 6 +++--- .../TestBundle/Document/DummyProduct.php | 2 +- .../TestBundle/Document/DummyValidation.php | 2 +- .../TestBundle/Document/FourthLevel.php | 12 +++++------ .../Fixtures/TestBundle/Document/Greeting.php | 2 +- .../TestBundle/Document/NetworkPathDummy.php | 2 +- .../Fixtures/TestBundle/Document/Question.php | 4 ++-- .../TestBundle/Document/RelatedDummy.php | 14 ++++++------- .../Document/RelatedToDummyFriend.php | 10 +++++----- .../TestBundle/Document/SlugChildDummy.php | 4 ++-- .../TestBundle/Document/SlugParentDummy.php | 4 ++-- .../TestBundle/Document/ThirdLevel.php | 10 +++++----- .../TestBundle/Entity/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Answer.php | 4 ++-- .../TestBundle/Entity/AttributeResource.php | 2 +- .../TestBundle/Entity/AttributeResources.php | 2 +- tests/Fixtures/TestBundle/Entity/Book.php | 2 +- tests/Fixtures/TestBundle/Entity/Dummy.php | 4 ++-- .../TestBundle/Entity/DummyAggregateOffer.php | 4 ++-- .../Fixtures/TestBundle/Entity/DummyOffer.php | 6 +++--- .../TestBundle/Entity/DummyProduct.php | 2 +- .../TestBundle/Entity/DummyValidation.php | 2 +- .../TestBundle/Entity/FourthLevel.php | 12 +++++------ tests/Fixtures/TestBundle/Entity/Greeting.php | 2 +- .../TestBundle/Entity/NetworkPathDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Question.php | 4 ++-- .../TestBundle/Entity/RelatedDummy.php | 14 ++++++------- .../Entity/RelatedToDummyFriend.php | 10 +++++----- .../TestBundle/Entity/SlugChildDummy.php | 4 ++-- .../TestBundle/Entity/SlugParentDummy.php | 4 ++-- .../Fixtures/TestBundle/Entity/ThirdLevel.php | 10 +++++----- .../Command/JsonSchemaGenerateCommandTest.php | 6 +++--- .../ResourceMetadataCompatibilityTest.php | 4 ++-- tests/Metadata/Extractor/XmlExtractorTest.php | 6 +++--- .../Metadata/Extractor/YamlExtractorTest.php | 8 ++++---- tests/Metadata/Extractor/xml/valid.xml | 4 ++-- tests/Metadata/Extractor/yaml/valid.yaml | 4 ++-- ...sResourceMetadataCollectionFactoryTest.php | 20 +++++++++---------- ...eResourceMetadataCollectionFactoryTest.php | 8 ++++---- .../PayloadArgumentResolverTest.php | 4 ++-- 49 files changed, 136 insertions(+), 127 deletions(-) diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 1ceacfc5636..7b4a5ca7c70 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -105,7 +105,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return sprintf('%s.{_format}', $uriTemplate); + return sprintf('%s{._format}', $uriTemplate); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation @@ -144,8 +144,12 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap } $operation = $operation->withUriVariables($uriVariables); + if (str_ends_with($uriTemplate, '{._format}')) { + $uriTemplate = substr($uriTemplate, 0, -10); + } + $route = (new Route($uriTemplate))->compile(); - $variables = array_filter($route->getPathVariables(), fn ($v): bool => '_format' !== $v); + $variables = $route->getPathVariables(); if (\count($variables) !== \count($uriVariables)) { $newUriVariables = []; diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 34dc68f2cfe..75f1d2ee8bb 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -333,7 +333,8 @@ private function flattenMimeTypes(array $responseFormats): array */ private function getPath(string $path): string { - if (str_ends_with($path, '.{_format}')) { + // Handle either API Platform's URI Template (rfc6570) or Symfony's route + if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php index 53bb0b22a10..2d37d93b8f1 100644 --- a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php +++ b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php @@ -22,7 +22,6 @@ trait NormalizeOperationNameTrait { private function normalizeOperationName(string $operationName): string { - // .{_format} is related to the symfony router - return preg_replace('/^_/', '', str_replace(['/', '.{_format}', '{', '}'], ['', '', '_', ''], $operationName)); + return preg_replace('/^_/', '', str_replace(['/', '{._format}', '{', '}'], ['', '', '_', ''], $operationName)); } } diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index 923a9b50110..a9b636e063c 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -75,6 +75,11 @@ public function load(mixed $data, string $type = null): RouteCollection $path = str_replace(sprintf('{%s}', $parameterName), $expandedValue, $path); } + // Within Symfony .{_format} is a special parameter but the rfc6570 specifies label expansion with a dot operator + if (str_ends_with($path, '{._format}')) { + $path = str_replace('{._format}', '.{_format}', $path); + } + if (($controller = $operation->getController()) && !$this->container->has($controller)) { throw new RuntimeException(sprintf('There is no builtin action for the "%s" operation. You need to define the controller yourself.', $operationName)); } diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php index 1209f780a9a..23b8bea0b1d 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ODM\Document] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Document/Answer.php b/tests/Fixtures/TestBundle/Document/Answer.php index 03023a4c892..2b45253e55d 100644 --- a/tests/Fixtures/TestBundle/Document/Answer.php +++ b/tests/Fixtures/TestBundle/Document/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ODM\Document] class Answer { diff --git a/tests/Fixtures/TestBundle/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php index e95179ce536..c74a8a30328 100644 --- a/tests/Fixtures/TestBundle/Document/Book.php +++ b/tests/Fixtures/TestBundle/Document/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ODM\Document] class Book { diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index 4a8b56c991c..cfa585544e0 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -29,8 +29,8 @@ * @author Alexandre Delplace */ #[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ODM\Document] class Dummy { diff --git a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php index ebec8ed9c59..8ccf3657399 100644 --- a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyOffer.php b/tests/Fixtures/TestBundle/Document/DummyOffer.php index 80bb1aecaa8..518829e632b 100644 --- a/tests/Fixtures/TestBundle/Document/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyProduct.php b/tests/Fixtures/TestBundle/Document/DummyProduct.php index 7ea04a0447a..065956e5ee7 100644 --- a/tests/Fixtures/TestBundle/Document/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Document/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Document/DummyValidation.php b/tests/Fixtures/TestBundle/Document/DummyValidation.php index 587fd26b7be..a0fa6b10033 100644 --- a/tests/Fixtures/TestBundle/Document/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Document/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation.{_format}'), + new Post(uriTemplate: 'dummy_validation{._format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Document/FourthLevel.php b/tests/Fixtures/TestBundle/Document/FourthLevel.php index 46a9cd1d615..bf653bfbc37 100644 --- a/tests/Fixtures/TestBundle/Document/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Document/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Document/Greeting.php b/tests/Fixtures/TestBundle/Document/Greeting.php index 086b22e84b1..bb6ec15928a 100644 --- a/tests/Fixtures/TestBundle/Document/Greeting.php +++ b/tests/Fixtures/TestBundle/Document/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Greeting { diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php index 41b63270bf8..bd2794e67a5 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ODM\Document] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Document/Question.php b/tests/Fixtures/TestBundle/Document/Question.php index 6375515e66c..417f78864b9 100644 --- a/tests/Fixtures/TestBundle/Document/Question.php +++ b/tests/Fixtures/TestBundle/Document/Question.php @@ -19,8 +19,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Question { diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index 0ca2c57dc04..a400aa06450 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -37,13 +37,13 @@ * @author Alexandre Delplace */ #[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.mongodb.friends'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ODM\Document] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php index 254fd5da549..8955436b628 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ODM\Document] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php index 4d210239e04..890cf391652 100644 --- a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php index 4ff555b8764..ed34ea94bd6 100644 --- a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ODM\Document] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index e4b6fe19cfa..046b89afb16 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -26,11 +26,11 @@ * @author Alexandre Delplace */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php index ff2fb260aca..dfb1f919c21 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ORM\Entity] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Answer.php b/tests/Fixtures/TestBundle/Entity/Answer.php index 7a6fb033669..945fd5e3b6d 100644 --- a/tests/Fixtures/TestBundle/Entity/Answer.php +++ b/tests/Fixtures/TestBundle/Entity/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ORM\Entity] class Answer { diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index 560251f7c69..0c4649e27e7 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -31,7 +31,7 @@ #[Put] #[Delete] #[ApiResource( - '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', inputFormats: ['json' => ['application/merge-patch+json']], status: 301, provider: AttributeResourceProvider::class, diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index 6685647c810..342579ca658 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -22,7 +22,7 @@ use Traversable; #[ApiResource( - '/attribute_resources.{_format}', + '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], provider: AttributeResourceProvider::class )] diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php index 45a30eb6807..f892b81cb3f 100644 --- a/tests/Fixtures/TestBundle/Entity/Book.php +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ORM\Entity] class Book { diff --git a/tests/Fixtures/TestBundle/Entity/Dummy.php b/tests/Fixtures/TestBundle/Entity/Dummy.php index 33fc651dbd7..69e9384db63 100644 --- a/tests/Fixtures/TestBundle/Entity/Dummy.php +++ b/tests/Fixtures/TestBundle/Entity/Dummy.php @@ -28,8 +28,8 @@ * @author Kévin Dunglas */ #[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] class Dummy { diff --git a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php index 33d5def4a21..7089cdf2a70 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyOffer.php b/tests/Fixtures/TestBundle/Entity/DummyOffer.php index 8448b8bf21c..5e9d52dad81 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php index 6901e719705..f2a5475d46a 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidation.php b/tests/Fixtures/TestBundle/Entity/DummyValidation.php index e06a4f02d29..8500aa1556b 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Entity/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation.{_format}'), + new Post(uriTemplate: 'dummy_validation{._format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Entity/FourthLevel.php b/tests/Fixtures/TestBundle/Entity/FourthLevel.php index 85161c78705..b62eaed1b27 100644 --- a/tests/Fixtures/TestBundle/Entity/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Entity/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Entity/Greeting.php b/tests/Fixtures/TestBundle/Entity/Greeting.php index ee0bcd5602d..d74e8217b55 100644 --- a/tests/Fixtures/TestBundle/Entity/Greeting.php +++ b/tests/Fixtures/TestBundle/Entity/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Greeting { diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php index 30070090fda..46530b96c5e 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ORM\Entity] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Question.php b/tests/Fixtures/TestBundle/Entity/Question.php index 0ffc8bfd2cb..78b1afd5026 100644 --- a/tests/Fixtures/TestBundle/Entity/Question.php +++ b/tests/Fixtures/TestBundle/Entity/Question.php @@ -19,8 +19,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Question { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index bbb36c24aca..d3f0126dccb 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -36,13 +36,13 @@ * @author Kévin Dunglas */ #[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ORM\Entity] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index b7823a78d86..c584c30596b 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ORM\Entity] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php index bab3c01e521..c1d8c5b21e4 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php index c6412d78229..104ca2cc97e 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ORM\Entity] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 1d18d362d73..1ca2e4c6f43 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -25,11 +25,11 @@ * @author Kévin Dunglas */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class ThirdLevel { diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index fb6040a5de1..ae69822be21 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -49,21 +49,21 @@ public function testExecuteWithoutOption(): void public function testExecuteWithItemOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}.{_format}_get', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}{._format}_get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithCollectionOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_get_collection', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_get_collection', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithJsonldFormatOption(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_post', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('@id', $result); diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 20342ad36e6..1cd97c28814 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -224,7 +224,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase [ 'name' => 'custom_operation_name', 'method' => 'GET', - 'uriTemplate' => '/users/{userId}/comments.{_format}', + 'uriTemplate' => '/users/{userId}/comments{._format}', 'shortName' => self::SHORT_NAME, 'description' => 'A list of Comments', 'types' => ['Comment'], @@ -330,7 +330,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], ], [ - 'uriTemplate' => '/users/{userId}/comments/{commentId}.{_format}', + 'uriTemplate' => '/users/{userId}/comments/{commentId}{._format}', 'class' => Get::class, 'uriVariables' => [ 'userId' => [ diff --git a/tests/Metadata/Extractor/XmlExtractorTest.php b/tests/Metadata/Extractor/XmlExtractorTest.php index dc1f9b45e8f..b229f52f1b6 100644 --- a/tests/Metadata/Extractor/XmlExtractorTest.php +++ b/tests/Metadata/Extractor/XmlExtractorTest.php @@ -99,7 +99,7 @@ public function testValidXML(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/comments.{_format}', + 'uriTemplate' => '/users/{author}/comments{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -177,7 +177,7 @@ public function testValidXML(): void [ 'name' => 'custom_operation_name', 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/comments.{_format}', + 'uriTemplate' => '/users/{author}/comments{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -268,7 +268,7 @@ public function testValidXML(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/comments/{id}.{_format}', + 'uriTemplate' => '/users/{userId}/comments/{id}{._format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index 843df41f723..cede4108ebc 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -166,7 +166,7 @@ public function testValidYaml(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/programs.{_format}', + 'uriTemplate' => '/users/{author}/programs{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -230,7 +230,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/programs.{_format}', + 'uriTemplate' => '/users/{author}/programs{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -305,7 +305,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/programs/{id}.{_format}', + 'uriTemplate' => '/users/{userId}/programs/{id}{._format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -471,7 +471,7 @@ public function testInvalidYaml(string $path, string $error): void public function getInvalidPaths(): array { return [ - [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml'.'".'], + [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml".'], ]; } } diff --git a/tests/Metadata/Extractor/xml/valid.xml b/tests/Metadata/Extractor/xml/valid.xml index 3269a7ba104..cfcccc202d3 100644 --- a/tests/Metadata/Extractor/xml/valid.xml +++ b/tests/Metadata/Extractor/xml/valid.xml @@ -7,7 +7,7 @@ @@ -96,7 +96,7 @@ - + diff --git a/tests/Metadata/Extractor/yaml/valid.yaml b/tests/Metadata/Extractor/yaml/valid.yaml index d001ba20899..c3a80a5acac 100644 --- a/tests/Metadata/Extractor/yaml/valid.yaml +++ b/tests/Metadata/Extractor/yaml/valid.yaml @@ -3,7 +3,7 @@ resources: ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program: - ~ - - uriTemplate: /users/{author}/programs.{_format} + - uriTemplate: /users/{author}/programs{._format} uriVariables: ['author'] types: ['someirischema'] description: User programs @@ -13,7 +13,7 @@ resources: operations: ApiPlatform\Metadata\GetCollection: ~ ApiPlatform\Metadata\Get: - uriTemplate: /users/{userId}/programs/{id}.{_format} + uriTemplate: /users/{userId}/programs/{id}{._format} types: ['anotheririschema'] uriVariables: userId: [ApiPlatform\Tests\Fixtures\TestBundle\Entity\User, 'author'] diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 0420c8d3be8..a7b78bd9c8a 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -81,11 +81,11 @@ class: AttributeResource::class, new ApiResource( shortName: 'AttributeResource', class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', operations: [ - '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_get' => new Get( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_get' => new Get( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 4, @@ -94,9 +94,9 @@ class: AttributeResource::class, // @noRector \Rector\Php81\Rector\Array_\FirstClassCallableRector processor: [AttributeResourceProcessor::class, 'process'] ), - '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_patch' => new Patch( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_patch' => new Patch( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 5, @@ -119,17 +119,17 @@ class: AttributeResource::class, $this->assertEquals( new ResourceMetadataCollection(AttributeResources::class, [ new ApiResource( - uriTemplate: '/attribute_resources.{_format}', + uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResources', normalizationContext: ['skip_null_values' => true], class: AttributeResources::class, provider: AttributeResourceProvider::class, operations: [ - '_api_/attribute_resources.{_format}_get_collection' => new GetCollection( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, + '_api_/attribute_resources{._format}_get_collection' => new GetCollection( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, ), - '_api_/attribute_resources.{_format}_post' => new Post( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, + '_api_/attribute_resources{._format}_post' => new Post( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, ), ], graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResources', AttributeResources::class, AttributeResourceProvider::class) diff --git a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index d60638d3f71..7603509b3a9 100644 --- a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -115,10 +115,10 @@ class: AttributeResource::class, shortName: 'AttributeResource', class: AttributeResource::class, operations: [ - '_api_/attribute_resources/{id}.{_format}_get' => new Get(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_get'), - '_api_/attribute_resources/{id}.{_format}_put' => new Put(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_put'), - '_api_/attribute_resources/{id}.{_format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_delete'), - '_api_/attribute_resources.{_format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources.{_format}_get_collection'), + '_api_/attribute_resources/{id}{._format}_get' => new Get(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_get'), + '_api_/attribute_resources/{id}{._format}_put' => new Put(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_put'), + '_api_/attribute_resources/{id}{._format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_delete'), + '_api_/attribute_resources{._format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources{._format}_get_collection'), ] ), new ApiResource( diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index ad0f265a015..c96d73b6746 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -205,7 +205,7 @@ public function provideIntegrationCases(): iterable yield 'simple' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations.{_format}_put', + '_api_operation_name' => '_api_/resource_implementations{._format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload): void {}, @@ -215,7 +215,7 @@ static function (ResourceImplementation $payload): void {}, yield 'with another argument named $data' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations.{_format}_put', + '_api_operation_name' => '_api_/resource_implementations{._format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload, $data): void {}, From 310363d56129c94cf4d51977f85486729e582fbc Mon Sep 17 00:00:00 2001 From: Xavier Laviron Date: Mon, 24 Oct 2022 17:39:31 +0200 Subject: [PATCH 07/19] fix: remove @internal annotation for Operations (#5089) See #5084 --- src/Metadata/Operations.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index 4ea95109c70..b11f81ba161 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -15,9 +15,6 @@ use RuntimeException; -/** - * @internal - */ final class Operations implements \IteratorAggregate, \Countable { private $operations; From 8250d41a38913a17364d617875bb5a90f434ec48 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 25 Oct 2022 09:32:25 +0200 Subject: [PATCH 08/19] fix(metadata): define a name on a single operation (#5090) fixes #5082 --- ...butesResourceMetadataCollectionFactory.php | 4 ++-- .../Entity/AttributeOnlyOperation.php | 21 +++++++++++++++++++ ...sResourceMetadataCollectionFactoryTest.php | 17 +++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 55a58ccff75..f830d775af6 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -215,8 +215,8 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper } // Check for name conflict - if ($operation->getName()) { - if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { + if ($operation->getName() && null !== ($operations = $resource->getOperations())) { + if (!$operations->has($operation->getName())) { return [$operation->getName(), $operation]; } diff --git a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php new file mode 100644 index 00000000000..e13550bb603 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php @@ -0,0 +1,21 @@ + + * + * 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\Entity; + +use ApiPlatform\Metadata\Get; + +#[Get(name: 'my own name')] +final class AttributeOnlyOperation +{ +} diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 7c53b693c1f..85d4e228b9a 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -29,6 +29,7 @@ use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeDefaultOperations; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeOnlyOperation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResources; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExtraPropertiesResource; @@ -206,4 +207,20 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } + + public function testOverrideNameWithoutOperations(): void + { + $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); + + $operation = new HttpOperation(shortName: 'AttributeOnlyOperation', class: AttributeOnlyOperation::class); + $this->assertEquals(new ResourceMetadataCollection(AttributeOnlyOperation::class, [ + new ApiResource( + shortName: 'AttributeOnlyOperation', + class: AttributeOnlyOperation::class, + operations: [ + 'my own name' => (new Get(name: 'my own name', priority: 1))->withOperation($operation), + ] + ), + ]), $attributeResourceMetadataCollectionFactory->create(AttributeOnlyOperation::class)); + } } From 27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 25 Oct 2022 11:18:00 +0200 Subject: [PATCH 09/19] fix(metadata): deprecate when user decorates in legacy mode (#5091) fixes #5078 --- .../Metadata/Property/Factory/CachedPropertyMetadataFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Metadata/Property/Factory/CachedPropertyMetadataFactory.php b/src/Core/Metadata/Property/Factory/CachedPropertyMetadataFactory.php index ae3ff9c4f77..452ce9cbcb2 100644 --- a/src/Core/Metadata/Property/Factory/CachedPropertyMetadataFactory.php +++ b/src/Core/Metadata/Property/Factory/CachedPropertyMetadataFactory.php @@ -44,7 +44,7 @@ public function create(string $resourceClass, string $property, array $options = $cacheKey = self::CACHE_KEY_PREFIX.md5(serialize([$resourceClass, $property, $options])); return $this->getCached($cacheKey, function () use ($resourceClass, $property, $options) { - return $this->decorated->create($resourceClass, $property, $options); + return $this->decorated->create($resourceClass, $property, $options + ['deprecate' => false]); }); } } From 44337ddb3908d7b05ed75b75325b7941581f575b Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Wed, 2 Nov 2022 10:28:25 +0100 Subject: [PATCH 10/19] fix(graphql): use right nested operation (#5102) --- features/graphql/collection.feature | 46 ++++++ features/main/default_order.feature | 36 ++++- src/GraphQl/Type/FieldsBuilder.php | 75 +++++----- src/GraphQl/Type/TypeBuilder.php | 2 +- src/GraphQl/Type/TypeBuilderInterface.php | 2 +- .../Extractor/DynamicResourceExtractor.php | 50 +++++++ .../DynamicResourceExtractorInterface.php | 24 +++ ...butesResourceMetadataCollectionFactory.php | 4 + ...actorResourceMetadataCollectionFactory.php | 27 +++- ...ltersResourceMetadataCollectionFactory.php | 4 + ...hpDocResourceMetadataCollectionFactory.php | 4 + .../Resource/ResourceMetadataCollection.php | 7 + .../Bundle/Resources/config/graphql.xml | 1 + .../Resources/config/metadata/resource.xml | 8 + tests/Behat/DoctrineContext.php | 17 ++- .../Fixtures/TestBundle/Document/FooDummy.php | 13 ++ tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 ++ tests/Fixtures/TestBundle/Entity/SoMany.php | 3 + tests/GraphQl/Type/FieldsBuilderTest.php | 141 +++++++++++------- tests/GraphQl/Type/TypeBuilderTest.php | 4 +- .../DynamicResourceExtractorTest.php | 37 +++++ .../ResourceMetadataCompatibilityTest.php | 16 +- ...sResourceMetadataCollectionFactoryTest.php | 10 ++ ...cResourceMetadataCollectionFactoryTest.php | 15 ++ .../ApiPlatformExtensionTest.php | 2 + 25 files changed, 446 insertions(+), 115 deletions(-) create mode 100644 src/Metadata/Extractor/DynamicResourceExtractor.php create mode 100644 src/Metadata/Extractor/DynamicResourceExtractorInterface.php create mode 100644 tests/Metadata/Extractor/DynamicResourceExtractorTest.php diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 1540549f7d8..9767505171a 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,3 +910,49 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element + + @createSchema + Scenario: Retrieve paginated collections using mixed pagination + Given there are 5 fooDummy objects with fake names + When I send the following GraphQL request: + """ + { + fooDummies(page: 1) { + collection { + id + name + soManies(first: 2) { + edges { + node { + content + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + paginationInfo { + itemsPerPage + lastPage + totalCount + } + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the JSON node "data.fooDummies.collection" should have 3 elements + And the JSON node "data.fooDummies.collection[2].id" should exist + And the JSON node "data.fooDummies.collection[2].name" should exist + And the JSON node "data.fooDummies.collection[2].soManies" should exist + And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements + And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" + And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" + And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 + And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 + And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 91211014315..7458c2456e9 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,35 +79,61 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5" + "dummy": "/dummies/5", + "soManies": [ + "/so_manies/13", + "/so_manies/14", + "/so_manies/15" + ] + }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3" + "dummy": "/dummies/3", + "soManies": [ + "/so_manies/7", + "/so_manies/8", + "/so_manies/9" + ] }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2" + "dummy": "/dummies/2", + "soManies": [ + "/so_manies/4", + "/so_manies/5", + "/so_manies/6" + ] }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1" + "dummy": "/dummies/1", + "soManies": [ + "/so_manies/1", + "/so_manies/2", + "/so_manies/3" + ] }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4" + "dummy": "/dummies/4", + "soManies": [ + "/so_manies/10", + "/so_manies/11", + "/so_manies/12" + ] } ], "hydra:totalItems": 5, diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 8817899c8ef..5d1f86e3569 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -17,12 +17,10 @@ use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; +use ApiPlatform\Metadata\Extractor\DynamicResourceExtractorInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -47,7 +45,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly DynamicResourceExtractorInterface $dynamicResourceExtractor, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { } @@ -256,7 +254,25 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $resourceOperation = $rootOperation; + if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); + } catch (OperationNotFoundException) { + // If there is no query operation for a nested resource, use a dynamic resource to get one. + $dynamicResourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($this->dynamicResourceExtractor->addResource($resourceClass)); + + $resourceOperation = $dynamicResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query') + ->withResource($resourceMetadataCollection[0]); + } + } + + if (!$resourceOperation instanceof Operation) { + throw new \LogicException('The resource operation should be a GraphQL operation.'); + } + + $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -271,43 +287,22 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - $resolverOperation = $rootOperation; - - if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); - - if (!$resolverOperation instanceof Operation) { - $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); - } - } - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($rootOperation)) { - $args = $this->getGraphQlPaginationArgs($rootOperation); - } - - // Find the collection operation to get filters, there might be a smarter way to do this - $operation = null; - if (!empty($resourceClass)) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $operation = $resourceMetadataCollection->getOperation(null, true); - } catch (OperationNotFoundException) { - } + if ($this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); } return [ @@ -368,13 +363,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array { - if (null === $operation || null === $resourceClass) { + if (null === $resourceClass) { return $args; } - foreach ($operation->getFilters() ?? [] as $filterId) { + foreach ($resourceOperation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -382,7 +377,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -399,14 +394,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -470,7 +465,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -487,7 +482,7 @@ private function convertType(Type $type, bool $input, Operation $rootOperation, } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index acc2d9429ee..447897114be 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -210,7 +210,7 @@ public function getNodeInterface(): InterfaceType /** * {@inheritdoc} */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType + public function getResourcePaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType { $shortName = $resourceType->name; $paginationType = $this->pagination->getGraphQlPaginationType($operation); diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php index 50bb0077893..386c437495d 100644 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ b/src/GraphQl/Type/TypeBuilderInterface.php @@ -43,7 +43,7 @@ public function getNodeInterface(): InterfaceType; /** * Gets the type of a paginated collection of the given resource type. */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; + public function getResourcePaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; /** * Returns true if a type is a collection. diff --git a/src/Metadata/Extractor/DynamicResourceExtractor.php b/src/Metadata/Extractor/DynamicResourceExtractor.php new file mode 100644 index 00000000000..dd5c909c1b7 --- /dev/null +++ b/src/Metadata/Extractor/DynamicResourceExtractor.php @@ -0,0 +1,50 @@ + + * + * 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\Extractor; + +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; + +/** + * Extracts a dynamic resource (used by GraphQL for nested resources). + * + * @author Alan Poulain + */ +final class DynamicResourceExtractor implements DynamicResourceExtractorInterface +{ + private array $dynamicResources = []; + + /** + * {@inheritdoc} + */ + public function getResources(): array + { + return $this->dynamicResources; + } + + public function addResource(string $resourceClass, array $config = []): string + { + $dynamicResourceName = $this->getDynamicResourceName($resourceClass); + + $this->dynamicResources[$dynamicResourceName] = [ + array_merge(['class' => $resourceClass], $config), + ]; + + return $dynamicResourceName; + } + + private function getDynamicResourceName(string $resourceClass): string + { + return ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.$resourceClass; + } +} diff --git a/src/Metadata/Extractor/DynamicResourceExtractorInterface.php b/src/Metadata/Extractor/DynamicResourceExtractorInterface.php new file mode 100644 index 00000000000..c14e589fc63 --- /dev/null +++ b/src/Metadata/Extractor/DynamicResourceExtractorInterface.php @@ -0,0 +1,24 @@ + + * + * 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\Extractor; + +/** + * Extracts a dynamic resource (used by GraphQL for nested resources). + * + * @author Alan Poulain + */ +interface DynamicResourceExtractorInterface extends ResourceExtractorInterface +{ + public function addResource(string $resourceClass, array $config = []): string; +} diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9255c236fa8..ad77154eb3d 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -64,6 +64,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } + if ($resourceMetadataCollection->isDynamic()) { + return $resourceMetadataCollection; + } + try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index c44358897ca..711d70348da 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,7 +19,12 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -51,7 +56,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } - if (!(class_exists($resourceClass) || interface_exists($resourceClass)) || !$resources = $this->extractor->getResources()[$resourceClass] ?? false) { + if (!($resourceMetadataCollection->isDynamic() || class_exists($resourceClass) || interface_exists($resourceClass)) || !$resources = $this->extractor->getResources()[$resourceClass] ?? false) { return $resourceMetadataCollection; } @@ -88,9 +93,7 @@ private function buildResources(array $nodes, string $resourceClass): array } } - if (isset($node['graphQlOperations'])) { - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); - } + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -148,6 +151,20 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; + if (null === $data) { + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { + $operation = $this->getOperationWithDefaults($resource, $operation); + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + $operations[$operation->getName()] = $operation; + } + + return $operations; + } + foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -175,7 +192,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index bb68448c4c2..ef72c3b1799 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -41,6 +41,10 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } + if ($resourceMetadataCollection->isDynamic()) { + return $resourceMetadataCollection; + } + try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { diff --git a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php index b94e6bffdf1..57b862f76b7 100644 --- a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php @@ -42,6 +42,10 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); + if ($resourceMetadataCollection->isDynamic()) { + return $resourceMetadataCollection; + } + foreach ($resourceMetadataCollection as $key => $resourceMetadata) { if (null !== $resourceMetadata->getDescription()) { continue; diff --git a/src/Metadata/Resource/ResourceMetadataCollection.php b/src/Metadata/Resource/ResourceMetadataCollection.php index 2921635706b..28eff63c2f0 100644 --- a/src/Metadata/Resource/ResourceMetadataCollection.php +++ b/src/Metadata/Resource/ResourceMetadataCollection.php @@ -24,6 +24,8 @@ */ final class ResourceMetadataCollection extends \ArrayObject { + public const DYNAMIC_RESOURCE_CLASS_PREFIX = 'Dynamic#'; + private array $operationCache = []; public function __construct(private readonly string $resourceClass, array $input = []) @@ -86,6 +88,11 @@ public function getOperation(?string $operationName = null, bool $forceCollectio $this->handleNotFound($operationName, $metadata); } + public function isDynamic(): bool + { + return str_starts_with($this->resourceClass, self::DYNAMIC_RESOURCE_CLASS_PREFIX); + } + /** * @throws OperationNotFoundException */ diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index b5a8e4e80e5..154dd90783a 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -121,6 +121,7 @@ + diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index 91d5d1a669a..31bb563593b 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -23,6 +23,14 @@ %api_platform.defaults% + + + + + + %api_platform.defaults% + + diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 5e02353fcfb..37aaa67205b 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); - $dummy->content = 'Many #'.$i; + $soMany = $this->buildSoMany(); + $soMany->content = 'Many #'.$i; - $this->manager->persist($dummy); + $this->manager->persist($soMany); } $this->manager->flush(); @@ -340,6 +340,12 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); + for ($j = 0; $j < 3; ++$j) { + $soMany = $this->buildSoMany(); + $soMany->content = "So many $j"; + $soMany->fooDummy = $foo; + $foo->soManies->add($soMany); + } $this->manager->persist($foo); } @@ -2200,6 +2206,11 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } + private function buildSoMany(): SoMany|SoManyDocument + { + return $this->isOrm() ? new SoMany() : new SoManyDocument(); + } + private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index cc9fe959f25..8736509e855 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -42,6 +44,17 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index b744c0c2a55..92596dad682 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -44,6 +46,17 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index 65a2ad93790..e3770b8007d 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,4 +31,7 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; + + #[ORM\ManyToOne] + public ?FooDummy $fooDummy; } diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index c7bb3ba897a..ab08c118efa 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -22,6 +22,7 @@ use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Extractor\DynamicResourceExtractorInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; @@ -56,29 +57,18 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; - private ObjectProphecy $propertyMetadataFactoryProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - + private ObjectProphecy $dynamicResourceExtractorProphecy; private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $typeBuilderProphecy; - private ObjectProphecy $typeConverterProphecy; - private ObjectProphecy $itemResolverFactoryProphecy; - private ObjectProphecy $collectionResolverFactoryProphecy; - private ObjectProphecy $itemMutationResolverFactoryProphecy; - private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; - private ObjectProphecy $filterLocatorProphecy; - private ObjectProphecy $resourceClassResolverProphecy; - private FieldsBuilder $fieldsBuilder; /** @@ -89,6 +79,7 @@ protected function setUp(): void $this->propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $this->dynamicResourceExtractorProphecy = $this->prophesize(DynamicResourceExtractorInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); @@ -103,7 +94,7 @@ protected function setUp(): void private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->dynamicResourceExtractorProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -139,7 +130,6 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -150,8 +140,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -164,7 +154,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -179,7 +169,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -192,7 +182,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -219,8 +209,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -244,8 +233,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -274,7 +263,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -308,7 +297,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -322,7 +311,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -338,7 +327,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -371,8 +360,6 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -383,7 +370,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -403,7 +390,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -435,7 +422,6 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -447,9 +433,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -469,7 +455,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -498,6 +484,8 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -505,20 +493,33 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyNestedResource' === $propertyName) { + $nestedResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + }); + } + if ('propertyNestedResourceNoQuery' === $propertyName) { + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); + $this->dynamicResourceExtractorProphecy->addResource('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn('Dynamic#nestedResourceNoQueryClass'); + $dynamicResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('Dynamic#nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $dynamicResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $dynamicResourceQueryOperation->withDescription('A description.'))->willReturn(static function (): void { + }); + } } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); - $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -535,7 +536,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', new Query(), + 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -563,7 +564,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', new Query(), + 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -582,7 +583,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', new Query(), + 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -601,7 +602,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', new Query(), + 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -621,12 +622,40 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), + [ + 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), + 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), + ], + false, 0, null, + [ + 'id' => [ + 'type' => GraphQLType::nonNull(GraphQLType::id()), + ], + 'propertyNestedResource' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + 'propertyNestedResourceNoQuery' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + ], + ], + 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -650,7 +679,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -686,7 +715,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -705,7 +734,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -717,7 +746,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -733,7 +762,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -752,7 +781,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', new Subscription(), + 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -772,7 +801,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', new Subscription(), + 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -787,13 +816,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', new Query(), + 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', new Query(), + 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -802,7 +831,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', new Query(), + 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 66b0e7cd855..40712d7a05d 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/Metadata/Extractor/DynamicResourceExtractorTest.php b/tests/Metadata/Extractor/DynamicResourceExtractorTest.php new file mode 100644 index 00000000000..088318d6743 --- /dev/null +++ b/tests/Metadata/Extractor/DynamicResourceExtractorTest.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\Tests\Metadata\Extractor; + +use ApiPlatform\Metadata\Extractor\DynamicResourceExtractor; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use PHPUnit\Framework\TestCase; + +final class DynamicResourceExtractorTest extends TestCase +{ + public function testAddResource(): void + { + $dynamicResourceExtractor = new DynamicResourceExtractor(); + + $dynamicResourceName = $dynamicResourceExtractor->addResource(Dummy::class, ['description' => 'A description.']); + + self::assertSame(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.Dummy::class, $dynamicResourceName); + self::assertSame([ + $dynamicResourceName => [[ + 'class' => Dummy::class, + 'description' => 'A description.', + ]], + ], $dynamicResourceExtractor->getResources()); + } +} diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 1cd97c28814..3c59cc9c12c 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,10 +20,13 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -453,7 +456,16 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resources[] = $resource->withOperations(new Operations($operations)); + $resource = $resource->withOperations(new Operations($operations)); + + // Build default GraphQL operations + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { + $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; + $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); + } + + $resources[] = $resource->withGraphQlOperations($graphQlOperations); continue; } @@ -591,7 +603,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index a7b78bd9c8a..aa0b74b1471 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -209,4 +209,14 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } + + public function testDynamic(): void + { + $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); + + self::assertEquals( + new ResourceMetadataCollection(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.AttributeResource::class), + $attributeResourceMetadataCollectionFactory->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.AttributeResource::class) + ); + } } diff --git a/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php index dd92aa2fe80..c297b0bbd05 100644 --- a/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php @@ -58,4 +58,19 @@ public function testExtractDescription(): void $factory = new PhpDocResourceMetadataCollectionFactory($decorated); $this->assertSame('My dummy entity.', $factory->create(DummyEntity::class)[0]->getDescription()); } + + public function testDynamic(): void + { + $resourceCollection = new ResourceMetadataCollection(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class, [new ApiResource()]); + $decoratedProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $decoratedProphecy->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class)->willReturn($resourceCollection)->shouldBeCalled(); + $decorated = $decoratedProphecy->reveal(); + + $factory = new PhpDocResourceMetadataCollectionFactory($decorated); + + self::assertSame( + $resourceCollection, + $factory->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class) + ); + } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 006ad7e788d..dc997b0bee9 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -367,6 +367,8 @@ public function testMetadataConfiguration(): void // metadata/resource.xml 'api_platform.metadata.resource.metadata_collection_factory.attributes', 'api_platform.metadata.resource.metadata_collection_factory.xml', + 'api_platform.metadata.resource_extractor.dynamic', + 'api_platform.metadata.resource.metadata_collection_factory.dynamic', 'api_platform.metadata.resource.metadata_collection_factory.uri_template', 'api_platform.metadata.resource.metadata_collection_factory.link', 'api_platform.metadata.resource.metadata_collection_factory.operation_name', From c1cb3cd2ff32c8b1ee694b0989efeb133fbd8438 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 08:54:24 +0100 Subject: [PATCH 11/19] Revert "fix(graphql): use right nested operation (#5102)" (#5111) This reverts commit 44337ddb3908d7b05ed75b75325b7941581f575b. --- features/graphql/collection.feature | 46 ------ features/main/default_order.feature | 36 +---- src/GraphQl/Type/FieldsBuilder.php | 75 +++++----- src/GraphQl/Type/TypeBuilder.php | 2 +- src/GraphQl/Type/TypeBuilderInterface.php | 2 +- .../Extractor/DynamicResourceExtractor.php | 50 ------- .../DynamicResourceExtractorInterface.php | 24 --- ...butesResourceMetadataCollectionFactory.php | 4 - ...actorResourceMetadataCollectionFactory.php | 27 +--- ...ltersResourceMetadataCollectionFactory.php | 4 - ...hpDocResourceMetadataCollectionFactory.php | 4 - .../Resource/ResourceMetadataCollection.php | 7 - .../Bundle/Resources/config/graphql.xml | 1 - .../Resources/config/metadata/resource.xml | 8 - tests/Behat/DoctrineContext.php | 17 +-- .../Fixtures/TestBundle/Document/FooDummy.php | 13 -- tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 -- tests/Fixtures/TestBundle/Entity/SoMany.php | 3 - tests/GraphQl/Type/FieldsBuilderTest.php | 141 +++++++----------- tests/GraphQl/Type/TypeBuilderTest.php | 4 +- .../DynamicResourceExtractorTest.php | 37 ----- .../ResourceMetadataCompatibilityTest.php | 16 +- ...sResourceMetadataCollectionFactoryTest.php | 10 -- ...cResourceMetadataCollectionFactoryTest.php | 15 -- .../ApiPlatformExtensionTest.php | 2 - 25 files changed, 115 insertions(+), 446 deletions(-) delete mode 100644 src/Metadata/Extractor/DynamicResourceExtractor.php delete mode 100644 src/Metadata/Extractor/DynamicResourceExtractorInterface.php delete mode 100644 tests/Metadata/Extractor/DynamicResourceExtractorTest.php diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 9767505171a..1540549f7d8 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,49 +910,3 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element - - @createSchema - Scenario: Retrieve paginated collections using mixed pagination - Given there are 5 fooDummy objects with fake names - When I send the following GraphQL request: - """ - { - fooDummies(page: 1) { - collection { - id - name - soManies(first: 2) { - edges { - node { - content - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - } - } - paginationInfo { - itemsPerPage - lastPage - totalCount - } - } - } - """ - Then the response status code should be 200 - And the response should be in JSON - And the JSON node "data.fooDummies.collection" should have 3 elements - And the JSON node "data.fooDummies.collection[2].id" should exist - And the JSON node "data.fooDummies.collection[2].name" should exist - And the JSON node "data.fooDummies.collection[2].soManies" should exist - And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements - And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" - And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" - And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 - And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 - And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 7458c2456e9..91211014315 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,61 +79,35 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5", - "soManies": [ - "/so_manies/13", - "/so_manies/14", - "/so_manies/15" - ] - + "dummy": "/dummies/5" }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3", - "soManies": [ - "/so_manies/7", - "/so_manies/8", - "/so_manies/9" - ] + "dummy": "/dummies/3" }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2", - "soManies": [ - "/so_manies/4", - "/so_manies/5", - "/so_manies/6" - ] + "dummy": "/dummies/2" }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1", - "soManies": [ - "/so_manies/1", - "/so_manies/2", - "/so_manies/3" - ] + "dummy": "/dummies/1" }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4", - "soManies": [ - "/so_manies/10", - "/so_manies/11", - "/so_manies/12" - ] + "dummy": "/dummies/4" } ], "hydra:totalItems": 5, diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 5d1f86e3569..8817899c8ef 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -17,10 +17,12 @@ use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; -use ApiPlatform\Metadata\Extractor\DynamicResourceExtractorInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -45,7 +47,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly DynamicResourceExtractorInterface $dynamicResourceExtractor, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { } @@ -254,25 +256,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $resourceOperation = $rootOperation; - if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); - } catch (OperationNotFoundException) { - // If there is no query operation for a nested resource, use a dynamic resource to get one. - $dynamicResourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($this->dynamicResourceExtractor->addResource($resourceClass)); - - $resourceOperation = $dynamicResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query') - ->withResource($resourceMetadataCollection[0]); - } - } - - if (!$resourceOperation instanceof Operation) { - throw new \LogicException('The resource operation should be a GraphQL operation.'); - } - - $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -287,22 +271,43 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; + $resolverOperation = $rootOperation; + + if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); + + if (!$resolverOperation instanceof Operation) { + $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); + } + } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); + if ($this->pagination->isGraphQlEnabled($rootOperation)) { + $args = $this->getGraphQlPaginationArgs($rootOperation); + } + + // Find the collection operation to get filters, there might be a smarter way to do this + $operation = null; + if (!empty($resourceClass)) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $operation = $resourceMetadataCollection->getOperation(null, true); + } catch (OperationNotFoundException) { + } } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); } return [ @@ -363,13 +368,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array { - if (null === $resourceClass) { + if (null === $operation || null === $resourceClass) { return $args; } - foreach ($resourceOperation->getFilters() ?? [] as $filterId) { + foreach ($operation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -377,7 +382,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -394,14 +399,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -465,7 +470,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -482,7 +487,7 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 447897114be..acc2d9429ee 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -210,7 +210,7 @@ public function getNodeInterface(): InterfaceType /** * {@inheritdoc} */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType + public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType { $shortName = $resourceType->name; $paginationType = $this->pagination->getGraphQlPaginationType($operation); diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php index 386c437495d..50bb0077893 100644 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ b/src/GraphQl/Type/TypeBuilderInterface.php @@ -43,7 +43,7 @@ public function getNodeInterface(): InterfaceType; /** * Gets the type of a paginated collection of the given resource type. */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; + public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; /** * Returns true if a type is a collection. diff --git a/src/Metadata/Extractor/DynamicResourceExtractor.php b/src/Metadata/Extractor/DynamicResourceExtractor.php deleted file mode 100644 index dd5c909c1b7..00000000000 --- a/src/Metadata/Extractor/DynamicResourceExtractor.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * 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\Extractor; - -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; - -/** - * Extracts a dynamic resource (used by GraphQL for nested resources). - * - * @author Alan Poulain - */ -final class DynamicResourceExtractor implements DynamicResourceExtractorInterface -{ - private array $dynamicResources = []; - - /** - * {@inheritdoc} - */ - public function getResources(): array - { - return $this->dynamicResources; - } - - public function addResource(string $resourceClass, array $config = []): string - { - $dynamicResourceName = $this->getDynamicResourceName($resourceClass); - - $this->dynamicResources[$dynamicResourceName] = [ - array_merge(['class' => $resourceClass], $config), - ]; - - return $dynamicResourceName; - } - - private function getDynamicResourceName(string $resourceClass): string - { - return ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.$resourceClass; - } -} diff --git a/src/Metadata/Extractor/DynamicResourceExtractorInterface.php b/src/Metadata/Extractor/DynamicResourceExtractorInterface.php deleted file mode 100644 index c14e589fc63..00000000000 --- a/src/Metadata/Extractor/DynamicResourceExtractorInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * 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\Extractor; - -/** - * Extracts a dynamic resource (used by GraphQL for nested resources). - * - * @author Alan Poulain - */ -interface DynamicResourceExtractorInterface extends ResourceExtractorInterface -{ - public function addResource(string $resourceClass, array $config = []): string; -} diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index ad77154eb3d..9255c236fa8 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -64,10 +64,6 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } - if ($resourceMetadataCollection->isDynamic()) { - return $resourceMetadataCollection; - } - try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index 711d70348da..c44358897ca 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,12 +19,7 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -56,7 +51,7 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } - if (!($resourceMetadataCollection->isDynamic() || class_exists($resourceClass) || interface_exists($resourceClass)) || !$resources = $this->extractor->getResources()[$resourceClass] ?? false) { + if (!(class_exists($resourceClass) || interface_exists($resourceClass)) || !$resources = $this->extractor->getResources()[$resourceClass] ?? false) { return $resourceMetadataCollection; } @@ -93,7 +88,9 @@ private function buildResources(array $nodes, string $resourceClass): array } } - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); + if (isset($node['graphQlOperations'])) { + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); + } $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -151,20 +148,6 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; - if (null === $data) { - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { - $operation = $this->getOperationWithDefaults($resource, $operation); - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - $operations[$operation->getName()] = $operation; - } - - return $operations; - } - foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -192,7 +175,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index ef72c3b1799..bb68448c4c2 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -41,10 +41,6 @@ public function create(string $resourceClass): ResourceMetadataCollection $resourceMetadataCollection = $this->decorated->create($resourceClass); } - if ($resourceMetadataCollection->isDynamic()) { - return $resourceMetadataCollection; - } - try { $reflectionClass = new \ReflectionClass($resourceClass); } catch (\ReflectionException) { diff --git a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php index 57b862f76b7..b94e6bffdf1 100644 --- a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php @@ -42,10 +42,6 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - if ($resourceMetadataCollection->isDynamic()) { - return $resourceMetadataCollection; - } - foreach ($resourceMetadataCollection as $key => $resourceMetadata) { if (null !== $resourceMetadata->getDescription()) { continue; diff --git a/src/Metadata/Resource/ResourceMetadataCollection.php b/src/Metadata/Resource/ResourceMetadataCollection.php index 28eff63c2f0..2921635706b 100644 --- a/src/Metadata/Resource/ResourceMetadataCollection.php +++ b/src/Metadata/Resource/ResourceMetadataCollection.php @@ -24,8 +24,6 @@ */ final class ResourceMetadataCollection extends \ArrayObject { - public const DYNAMIC_RESOURCE_CLASS_PREFIX = 'Dynamic#'; - private array $operationCache = []; public function __construct(private readonly string $resourceClass, array $input = []) @@ -88,11 +86,6 @@ public function getOperation(?string $operationName = null, bool $forceCollectio $this->handleNotFound($operationName, $metadata); } - public function isDynamic(): bool - { - return str_starts_with($this->resourceClass, self::DYNAMIC_RESOURCE_CLASS_PREFIX); - } - /** * @throws OperationNotFoundException */ diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 154dd90783a..b5a8e4e80e5 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -121,7 +121,6 @@ - diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index 31bb563593b..91d5d1a669a 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -23,14 +23,6 @@ %api_platform.defaults% - - - - - - %api_platform.defaults% - - diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 37aaa67205b..5e02353fcfb 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $soMany = $this->buildSoMany(); - $soMany->content = 'Many #'.$i; + $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); + $dummy->content = 'Many #'.$i; - $this->manager->persist($soMany); + $this->manager->persist($dummy); } $this->manager->flush(); @@ -340,12 +340,6 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); - for ($j = 0; $j < 3; ++$j) { - $soMany = $this->buildSoMany(); - $soMany->content = "So many $j"; - $soMany->fooDummy = $foo; - $foo->soManies->add($soMany); - } $this->manager->persist($foo); } @@ -2206,11 +2200,6 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } - private function buildSoMany(): SoMany|SoManyDocument - { - return $this->isOrm() ? new SoMany() : new SoManyDocument(); - } - private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index 8736509e855..cc9fe959f25 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -44,17 +42,6 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index 92596dad682..b744c0c2a55 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -46,17 +44,6 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index e3770b8007d..65a2ad93790 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,7 +31,4 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; - - #[ORM\ManyToOne] - public ?FooDummy $fooDummy; } diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index ab08c118efa..c7bb3ba897a 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -22,7 +22,6 @@ use ApiPlatform\GraphQl\Type\TypesContainerInterface; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Extractor\DynamicResourceExtractorInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; @@ -57,18 +56,29 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; + private ObjectProphecy $propertyMetadataFactoryProphecy; + private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $dynamicResourceExtractorProphecy; + private ObjectProphecy $typesContainerProphecy; + private ObjectProphecy $typeBuilderProphecy; + private ObjectProphecy $typeConverterProphecy; + private ObjectProphecy $itemResolverFactoryProphecy; + private ObjectProphecy $collectionResolverFactoryProphecy; + private ObjectProphecy $itemMutationResolverFactoryProphecy; + private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; + private ObjectProphecy $filterLocatorProphecy; + private ObjectProphecy $resourceClassResolverProphecy; + private FieldsBuilder $fieldsBuilder; /** @@ -79,7 +89,6 @@ protected function setUp(): void $this->propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $this->propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $this->resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->dynamicResourceExtractorProphecy = $this->prophesize(DynamicResourceExtractorInterface::class); $this->typesContainerProphecy = $this->prophesize(TypesContainerInterface::class); $this->typeBuilderProphecy = $this->prophesize(TypeBuilderInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); @@ -94,7 +103,7 @@ protected function setUp(): void private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->dynamicResourceExtractorProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -130,6 +139,7 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -140,8 +150,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -154,7 +164,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -169,7 +179,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -182,7 +192,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -209,7 +219,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -233,8 +244,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -263,7 +274,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -297,7 +308,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -311,7 +322,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -327,7 +338,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -360,6 +371,8 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -370,7 +383,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -390,7 +403,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -422,6 +435,7 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -433,9 +447,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -455,7 +469,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -484,8 +498,6 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -493,33 +505,20 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); - if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - if ('propertyNestedResource' === $propertyName) { - $nestedResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { - }); - } - if ('propertyNestedResourceNoQuery' === $propertyName) { - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); - $this->dynamicResourceExtractorProphecy->addResource('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn('Dynamic#nestedResourceNoQueryClass'); - $dynamicResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('Dynamic#nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $dynamicResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $dynamicResourceQueryOperation->withDescription('A description.'))->willReturn(static function (): void { - }); - } + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -536,7 +535,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -564,7 +563,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with advanced name converter' => ['resourceClass', new Query(), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -583,7 +582,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query input' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -602,7 +601,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with simple non-null string array property' => ['resourceClass', new Query(), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -622,40 +621,12 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), - [ - 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), - 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), - ], - false, 0, null, - [ - 'id' => [ - 'type' => GraphQLType::nonNull(GraphQLType::id()), - ], - 'propertyNestedResource' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - 'propertyNestedResourceNoQuery' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - ], - ], - 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -679,7 +650,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -715,7 +686,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -734,7 +705,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -746,7 +717,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -762,7 +733,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -781,7 +752,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription non input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -801,7 +772,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -816,13 +787,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata non input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -831,7 +802,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'invalid types' => ['resourceClass', new Query(), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 40712d7a05d..66b0e7cd855 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/Metadata/Extractor/DynamicResourceExtractorTest.php b/tests/Metadata/Extractor/DynamicResourceExtractorTest.php deleted file mode 100644 index 088318d6743..00000000000 --- a/tests/Metadata/Extractor/DynamicResourceExtractorTest.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * 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\Metadata\Extractor; - -use ApiPlatform\Metadata\Extractor\DynamicResourceExtractor; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; -use PHPUnit\Framework\TestCase; - -final class DynamicResourceExtractorTest extends TestCase -{ - public function testAddResource(): void - { - $dynamicResourceExtractor = new DynamicResourceExtractor(); - - $dynamicResourceName = $dynamicResourceExtractor->addResource(Dummy::class, ['description' => 'A description.']); - - self::assertSame(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.Dummy::class, $dynamicResourceName); - self::assertSame([ - $dynamicResourceName => [[ - 'class' => Dummy::class, - 'description' => 'A description.', - ]], - ], $dynamicResourceExtractor->getResources()); - } -} diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 3c59cc9c12c..1cd97c28814 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,13 +20,10 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -456,16 +453,7 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resource = $resource->withOperations(new Operations($operations)); - - // Build default GraphQL operations - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { - $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; - $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); - } - - $resources[] = $resource->withGraphQlOperations($graphQlOperations); + $resources[] = $resource->withOperations(new Operations($operations)); continue; } @@ -603,7 +591,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index aa0b74b1471..a7b78bd9c8a 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -209,14 +209,4 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } - - public function testDynamic(): void - { - $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); - - self::assertEquals( - new ResourceMetadataCollection(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.AttributeResource::class), - $attributeResourceMetadataCollectionFactory->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.AttributeResource::class) - ); - } } diff --git a/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php index c297b0bbd05..dd92aa2fe80 100644 --- a/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactoryTest.php @@ -58,19 +58,4 @@ public function testExtractDescription(): void $factory = new PhpDocResourceMetadataCollectionFactory($decorated); $this->assertSame('My dummy entity.', $factory->create(DummyEntity::class)[0]->getDescription()); } - - public function testDynamic(): void - { - $resourceCollection = new ResourceMetadataCollection(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class, [new ApiResource()]); - $decoratedProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $decoratedProphecy->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class)->willReturn($resourceCollection)->shouldBeCalled(); - $decorated = $decoratedProphecy->reveal(); - - $factory = new PhpDocResourceMetadataCollectionFactory($decorated); - - self::assertSame( - $resourceCollection, - $factory->create(ResourceMetadataCollection::DYNAMIC_RESOURCE_CLASS_PREFIX.DummyEntity::class) - ); - } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index dc997b0bee9..006ad7e788d 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -367,8 +367,6 @@ public function testMetadataConfiguration(): void // metadata/resource.xml 'api_platform.metadata.resource.metadata_collection_factory.attributes', 'api_platform.metadata.resource.metadata_collection_factory.xml', - 'api_platform.metadata.resource_extractor.dynamic', - 'api_platform.metadata.resource.metadata_collection_factory.dynamic', 'api_platform.metadata.resource.metadata_collection_factory.uri_template', 'api_platform.metadata.resource.metadata_collection_factory.link', 'api_platform.metadata.resource.metadata_collection_factory.operation_name', From bbeaf7082bba4a019206c3862425cf849d55addd Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 16:58:42 +0100 Subject: [PATCH 12/19] fix(graphql): always allow to query nested resources (#5112) * fix(graphql): always allow to query nested resources * review Co-authored-by: Alan Poulain --- features/graphql/collection.feature | 46 +++++ features/main/default_order.feature | 36 +++- ...NestedOperationResourceMetadataFactory.php | 67 ++++++++ src/GraphQl/Type/FieldsBuilder.php | 71 ++++---- ...butesResourceMetadataCollectionFactory.php | 139 +-------------- ...actorResourceMetadataCollectionFactory.php | 25 ++- ...ltersResourceMetadataCollectionFactory.php | 10 +- .../Factory/OperationDefaultsTrait.php | 160 ++++++++++++++++++ .../Resources/config/doctrine_mongodb_odm.xml | 5 + .../Bundle/Resources/config/doctrine_orm.xml | 6 + .../Bundle/Resources/config/graphql.xml | 9 + tests/Behat/DoctrineContext.php | 17 +- .../Fixtures/TestBundle/Document/FooDummy.php | 13 ++ tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 ++ .../TestBundle/Entity/RelatedDummy.php | 10 +- tests/Fixtures/TestBundle/Entity/SoMany.php | 3 + ...edOperationResourceMetadataFactoryTest.php | 44 +++++ tests/GraphQl/Type/FieldsBuilderTest.php | 137 +++++++++------ tests/GraphQl/Type/TypeBuilderTest.php | 4 +- .../ResourceMetadataCompatibilityTest.php | 16 +- .../ApiPlatformExtensionTest.php | 2 + 21 files changed, 583 insertions(+), 250 deletions(-) create mode 100644 src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php create mode 100644 src/Metadata/Resource/Factory/OperationDefaultsTrait.php create mode 100644 tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 1540549f7d8..9767505171a 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,3 +910,49 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element + + @createSchema + Scenario: Retrieve paginated collections using mixed pagination + Given there are 5 fooDummy objects with fake names + When I send the following GraphQL request: + """ + { + fooDummies(page: 1) { + collection { + id + name + soManies(first: 2) { + edges { + node { + content + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + } + } + paginationInfo { + itemsPerPage + lastPage + totalCount + } + } + } + """ + Then the response status code should be 200 + And the response should be in JSON + And the JSON node "data.fooDummies.collection" should have 3 elements + And the JSON node "data.fooDummies.collection[2].id" should exist + And the JSON node "data.fooDummies.collection[2].name" should exist + And the JSON node "data.fooDummies.collection[2].soManies" should exist + And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements + And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" + And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" + And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 + And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 + And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 91211014315..7458c2456e9 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,35 +79,61 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5" + "dummy": "/dummies/5", + "soManies": [ + "/so_manies/13", + "/so_manies/14", + "/so_manies/15" + ] + }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3" + "dummy": "/dummies/3", + "soManies": [ + "/so_manies/7", + "/so_manies/8", + "/so_manies/9" + ] }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2" + "dummy": "/dummies/2", + "soManies": [ + "/so_manies/4", + "/so_manies/5", + "/so_manies/6" + ] }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1" + "dummy": "/dummies/1", + "soManies": [ + "/so_manies/1", + "/so_manies/2", + "/so_manies/3" + ] }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4" + "dummy": "/dummies/4", + "soManies": [ + "/so_manies/10", + "/so_manies/11", + "/so_manies/12" + ] } ], "hydra:totalItems": 5, diff --git a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php new file mode 100644 index 00000000000..05163b0c2b7 --- /dev/null +++ b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php @@ -0,0 +1,67 @@ + + * + * 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\GraphQl\Metadata\Factory; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +final class GraphQlNestedOperationResourceMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + use OperationDefaultsTrait; + + public function __construct(array $defaults, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null) + { + $this->defaults = $defaults; + $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); + $this->logger = $logger ?? new NullLogger(); + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); + + if ($this->decorated) { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + } + + if (0 < \count($resourceMetadataCollection)) { + return $resourceMetadataCollection; + } + + $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; + + $apiResource = new ApiResource( + class: $resourceClass, + shortName: $shortName + ); + + if (class_exists($resourceClass)) { + $refl = new \ReflectionClass($resourceClass); + $attribute = $refl->getAttributes(ApiResource::class)[0] ?? null; + $attributeInstance = $attribute?->newInstance(); + if ($filters = $attributeInstance?->getFilters()) { + $apiResource = $apiResource->withFilters($filters); + } + } + + $resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource); + + return $resourceMetadataCollection; + } +} diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 8817899c8ef..d64f1cb800b 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -20,9 +20,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -47,7 +45,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null) { } @@ -256,7 +254,23 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $resourceOperation = $rootOperation; + if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); + } catch (OperationNotFoundException) { + // If there is no query operation for a nested resource we force one to exist + $nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass); + $resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); + } + } + + if (!$resourceOperation instanceof Operation) { + throw new \LogicException('The resource operation should be a GraphQL operation.'); + } + + $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -271,43 +285,22 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - $resolverOperation = $rootOperation; - - if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); - - if (!$resolverOperation instanceof Operation) { - $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); - } - } - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($rootOperation)) { - $args = $this->getGraphQlPaginationArgs($rootOperation); - } - - // Find the collection operation to get filters, there might be a smarter way to do this - $operation = null; - if (!empty($resourceClass)) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $operation = $resourceMetadataCollection->getOperation(null, true); - } catch (OperationNotFoundException) { - } + if ($this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); } return [ @@ -368,13 +361,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array { - if (null === $operation || null === $resourceClass) { + if (null === $resourceClass) { return $args; } - foreach ($operation->getFilters() ?? [] as $filterId) { + foreach ($resourceOperation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -382,7 +375,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -399,14 +392,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -470,7 +463,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -487,7 +480,7 @@ private function convertType(Type $type, bool $input, Operation $rootOperation, } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9255c236fa8..9b125aed5c3 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -14,20 +14,12 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -45,12 +37,12 @@ */ final class AttributesResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - private readonly LoggerInterface $logger; - private readonly CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; + use OperationDefaultsTrait; - public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, private readonly array $defaults = [], private readonly bool $graphQlEnabled = false) + public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false) { $this->logger = $logger ?? new NullLogger(); + $this->defaults = $defaults; $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); } @@ -164,115 +156,6 @@ private function buildResourceOperations(array $attributes, string $resourceClas return $resources; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array - { - // Inherit from resource defaults - foreach (get_class_methods($resource) as $methodName) { - if (!str_starts_with($methodName, 'get')) { - continue; - } - - if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { - continue; - } - - if (null === ($value = $resource->{$methodName}())) { - continue; - } - - $operation = $operation->{'with'.substr($methodName, 3)}($value); - } - - $operation = $operation->withExtraProperties(array_merge( - $resource->getExtraProperties(), - $operation->getExtraProperties(), - $generated ? ['generated_operation' => true] : [] - )); - - // Add global defaults attributes to the operation - $operation = $this->addGlobalDefaults($operation); - - if ($operation instanceof GraphQlOperation) { - if (!$operation->getName()) { - throw new RuntimeException('No GraphQL operation name.'); - } - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - return [$operation->getName(), $operation]; - } - - if (!$operation instanceof HttpOperation) { - throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); - } - - if ($operation->getRouteName()) { - /** @var HttpOperation $operation */ - $operation = $operation->withName($operation->getRouteName()); - } - - // Check for name conflict - if ($operation->getName()) { - if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { - return [$operation->getName(), $operation]; - } - - $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); - /** @var HttpOperation $operation */ - $operation = $operation->withName(''); - } - - return [ - sprintf( - '_api_%s_%s%s', - $operation->getUriTemplate() ?: $operation->getShortName(), - strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), - $operation instanceof CollectionOperationInterface ? '_collection' : '', - ), - $operation, - ]; - } - - private function addGlobalDefaults(ApiResource|HttpOperation|GraphQlOperation $operation): ApiResource|HttpOperation|GraphQlOperation - { - $extraProperties = []; - foreach ($this->defaults as $key => $value) { - $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); - $getter = 'get'.$upperKey; - - if (!method_exists($operation, $getter)) { - if (!isset($extraProperties[$key])) { - $extraProperties[$key] = $value; - } - } else { - $currentValue = $operation->{$getter}(); - - if (\is_array($currentValue) && $currentValue) { - $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); - } - - if (null !== $currentValue) { - continue; - } - - $operation = $operation->{'with'.$upperKey}($value); - } - } - - return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); - } - - private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource - { - $resource = $resource - ->withShortName($resource->getShortName() ?? $shortName) - ->withClass($resourceClass); - - return $this->addGlobalDefaults($resource); - } - private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool { foreach ($reflectionClass->getAttributes() as $attribute) { @@ -303,22 +186,6 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, return false; } - private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource - { - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { - [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); - $graphQlOperations[$key] = $operation; - } - - if ($resource->getMercure()) { - [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); - $graphQlOperations[$key] = $operation; - } - - return $resource->withGraphQlOperations($graphQlOperations); - } - private function getDefaultHttpOperations($resource): iterable { $post = new Post(); diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index c44358897ca..86c154f7bc4 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,7 +19,12 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -88,9 +93,7 @@ private function buildResources(array $nodes, string $resourceClass): array } } - if (isset($node['graphQlOperations'])) { - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); - } + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -148,6 +151,20 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; + if (null === $data) { + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { + $operation = $this->getOperationWithDefaults($resource, $operation); + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + $operations[$operation->getName()] = $operation; + } + + return $operations; + } + foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -175,7 +192,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index bb68448c4c2..10a711fe71a 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -50,17 +50,21 @@ public function create(string $resourceClass): ResourceMetadataCollection $filters = array_keys($this->readFilterAttributes($reflectionClass)); foreach ($resourceMetadataCollection as $i => $resource) { - foreach ($operations = $resource->getOperations() as $operationName => $operation) { + foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { $operations->add($operationName, $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters)))); } - $resourceMetadataCollection[$i] = $resource->withOperations($operations); + if ($operations) { + $resourceMetadataCollection[$i] = $resource->withOperations($operations); + } foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { $graphQlOperations[$operationName] = $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters))); } - $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + if ($graphQlOperations) { + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + } } return $resourceMetadataCollection; diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php new file mode 100644 index 00000000000..01f3bdc346f --- /dev/null +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -0,0 +1,160 @@ + + * + * 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\Resource\Factory; + +use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +trait OperationDefaultsTrait +{ + private CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; + private array $defaults = []; + private LoggerInterface $logger; + + private function addGlobalDefaults(ApiResource|Operation $operation): ApiResource|Operation + { + $extraProperties = []; + foreach ($this->defaults as $key => $value) { + $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); + $getter = 'get'.$upperKey; + + if (!method_exists($operation, $getter)) { + if (!isset($extraProperties[$key])) { + $extraProperties[$key] = $value; + } + } else { + $currentValue = $operation->{$getter}(); + + if (\is_array($currentValue) && $currentValue) { + $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); + } + + if (null !== $currentValue) { + continue; + } + + $operation = $operation->{'with'.$upperKey}($value); + } + } + + return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); + } + + private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource + { + $resource = $resource + ->withShortName($resource->getShortName() ?? $shortName) + ->withClass($resourceClass); + + return $this->addGlobalDefaults($resource); + } + + private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource + { + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $graphQlOperations[$key] = $operation; + } + + if ($resource->getMercure()) { + [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); + $graphQlOperations[$key] = $operation; + } + + return $resource->withGraphQlOperations($graphQlOperations); + } + + private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array + { + // Inherit from resource defaults + foreach (get_class_methods($resource) as $methodName) { + if (!str_starts_with($methodName, 'get')) { + continue; + } + + if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { + continue; + } + + if (null === ($value = $resource->{$methodName}())) { + continue; + } + + $operation = $operation->{'with'.substr($methodName, 3)}($value); + } + + $operation = $operation->withExtraProperties(array_merge( + $resource->getExtraProperties(), + $operation->getExtraProperties(), + $generated ? ['generated_operation' => true] : [] + )); + + // Add global defaults attributes to the operation + $operation = $this->addGlobalDefaults($operation); + + if ($operation instanceof GraphQlOperation) { + if (!$operation->getName()) { + throw new RuntimeException('No GraphQL operation name.'); + } + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + return [$operation->getName(), $operation]; + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); + } + + if ($operation->getRouteName()) { + /** @var HttpOperation $operation */ + $operation = $operation->withName($operation->getRouteName()); + } + + // Check for name conflict + if ($operation->getName()) { + if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { + return [$operation->getName(), $operation]; + } + + $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); + /** @var HttpOperation $operation */ + $operation = $operation->withName(''); + } + + return [ + sprintf( + '_api_%s_%s%s', + $operation->getUriTemplate() ?: $operation->getShortName(), + strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), + $operation instanceof CollectionOperationInterface ? '_collection' : '', + ), + $operation, + ]; + } +} diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 4cecc9d339a..29f9a7a761f 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -156,6 +156,11 @@ + + + + + diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index a46540ab0a9..0b6949bb9b8 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -166,6 +166,12 @@ + + + + + + diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index b5a8e4e80e5..0f713b273b9 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -133,6 +133,7 @@ %api_platform.graphql.nesting_separator% + @@ -274,6 +275,14 @@ + + + %api_platform.defaults% + + + + + diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 5e02353fcfb..37aaa67205b 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); - $dummy->content = 'Many #'.$i; + $soMany = $this->buildSoMany(); + $soMany->content = 'Many #'.$i; - $this->manager->persist($dummy); + $this->manager->persist($soMany); } $this->manager->flush(); @@ -340,6 +340,12 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); + for ($j = 0; $j < 3; ++$j) { + $soMany = $this->buildSoMany(); + $soMany->content = "So many $j"; + $soMany->fooDummy = $foo; + $foo->soManies->add($soMany); + } $this->manager->persist($foo); } @@ -2200,6 +2206,11 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } + private function buildSoMany(): SoMany|SoManyDocument + { + return $this->isOrm() ? new SoMany() : new SoManyDocument(); + } + private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index cc9fe959f25..8736509e855 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -42,6 +44,17 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index b744c0c2a55..92596dad682 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,6 +15,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -44,6 +46,17 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] + public Collection $soManies; + + public function __construct() + { + $this->soManies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index d3f0126dccb..a6e60f79169 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -35,7 +35,15 @@ * * @author Kévin Dunglas */ -#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])] +#[ApiResource( + graphQlOperations: [ + new Query(name: 'item_query'), + new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), + ], + types: ['https://schema.org/Product'], + normalizationContext: ['groups' => ['friends']], + filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] +)] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index 65a2ad93790..e3770b8007d 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,4 +31,7 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; + + #[ORM\ManyToOne] + public ?FooDummy $fooDummy; } diff --git a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php new file mode 100644 index 00000000000..65c4b65c51f --- /dev/null +++ b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php @@ -0,0 +1,44 @@ + + * + * 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\GraphQl\Metadata\Factory; + +use ApiPlatform\GraphQl\Metadata\Factory\GraphQlNestedOperationResourceMetadataFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +final class GraphQlNestedOperationResourceMetadataFactoryTest extends TestCase +{ + use ProphecyTrait; + + public function testCreate(): void + { + $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $decorated->create('someClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('someClass')); + + $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500], $decorated->reveal()); + $apiResource = $metadataFactory->create('someClass')[0]; + $this->assertCount(5, $apiResource->getGraphQlOperations()); + } + + public function testCreateWithResource(): void + { + $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500]); + $apiResource = $metadataFactory->create(RelatedDummy::class)[0]; + $this->assertNotEmpty($apiResource->getFilters()); + $this->assertEquals('RelatedDummy', $apiResource->getShortName()); + } +} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index c7bb3ba897a..c5b59ad50b7 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -56,29 +56,18 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; - private ObjectProphecy $propertyMetadataFactoryProphecy; - private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - + private ObjectProphecy $graphQlNestedOperationResourceMetadataFactoryProphecy; private ObjectProphecy $typesContainerProphecy; - private ObjectProphecy $typeBuilderProphecy; - private ObjectProphecy $typeConverterProphecy; - private ObjectProphecy $itemResolverFactoryProphecy; - private ObjectProphecy $collectionResolverFactoryProphecy; - private ObjectProphecy $itemMutationResolverFactoryProphecy; - private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; - private ObjectProphecy $filterLocatorProphecy; - private ObjectProphecy $resourceClassResolverProphecy; - private FieldsBuilder $fieldsBuilder; /** @@ -98,12 +87,13 @@ protected function setUp(): void $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $this->graphQlNestedOperationResourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); } private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__', $this->graphQlNestedOperationResourceMetadataFactoryProphecy->reveal()); } public function testGetNodeQueryFields(): void @@ -139,7 +129,6 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -150,8 +139,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -164,7 +153,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -179,7 +168,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -192,7 +181,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -220,7 +209,6 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -244,8 +232,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -274,7 +262,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -308,7 +296,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -322,7 +310,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -338,7 +326,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -371,8 +359,6 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); - $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -383,7 +369,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -403,7 +389,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -435,7 +421,6 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -447,9 +432,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -469,7 +454,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -498,6 +483,8 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); + $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -505,20 +492,32 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + if ('propertyNestedResource' === $propertyName) { + $nestedResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + }); + } + if ('propertyNestedResourceNoQuery' === $propertyName) { + $nestedResourceQueryOperation = new Query(); + $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); + $this->graphQlNestedOperationResourceMetadataFactoryProphecy->create('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); + $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + }); + } } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); - $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -535,7 +534,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', new Query(), + 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -563,7 +562,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', new Query(), + 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -582,7 +581,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', new Query(), + 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -601,7 +600,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', new Query(), + 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -621,12 +620,40 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), + [ + 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), + 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), + ], + false, 0, null, + [ + 'id' => [ + 'type' => GraphQLType::nonNull(GraphQLType::id()), + ], + 'propertyNestedResource' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + 'propertyNestedResourceNoQuery' => [ + 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), + 'description' => null, + 'args' => [], + 'resolve' => static function (): void { + }, + 'deprecationReason' => null, + ], + ], + ], + 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -650,7 +677,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -686,7 +713,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -705,7 +732,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -717,7 +744,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -733,7 +760,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -752,7 +779,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', new Subscription(), + 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -772,7 +799,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', new Subscription(), + 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -787,13 +814,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', new Query(), + 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', new Query(), + 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -802,7 +829,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', new Query(), + 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 66b0e7cd855..77ba4f7e842 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 1cd97c28814..3c59cc9c12c 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,10 +20,13 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -453,7 +456,16 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resources[] = $resource->withOperations(new Operations($operations)); + $resource = $resource->withOperations(new Operations($operations)); + + // Build default GraphQL operations + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { + $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; + $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); + } + + $resources[] = $resource->withGraphQlOperations($graphQlOperations); continue; } @@ -591,7 +603,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation + private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 006ad7e788d..6b4ede1efd7 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -671,6 +671,8 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.validation_exception', 'api_platform.graphql.normalizer.http_exception', 'api_platform.graphql.normalizer.runtime_exception', + 'api_platform.graphql_metadata.resource.metadata_collection_factory', + 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters' ]; $aliases = [ From 0ed8fbc6c32646ac4d9289669fb00bd89ac8b366 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Nov 2022 18:01:02 +0100 Subject: [PATCH 13/19] chore: php-cs-fixer update --- src/Metadata/Operations.php | 4 +- src/Symfony/Routing/SkolemIriConverter.php | 3 +- src/Test/DoctrineMongoDbOdmTestCase.php | 6 +-- tests/Action/ExceptionActionTest.php | 41 +++++++++---------- .../Odm/Extension/OrderExtensionTest.php | 11 +++-- .../TestBundle/Document/VoDummyInspection.php | 9 ++-- .../TestBundle/Entity/AttributeResources.php | 9 ++-- .../TestBundle/Entity/VoDummyInspection.php | 9 ++-- tests/HttpCache/VarnishPurgerTest.php | 9 ++-- tests/HttpCache/VarnishXKeyPurgerTest.php | 9 ++-- .../PropertyMetadataCompatibilityTest.php | 3 +- .../ResourceMetadataCompatibilityTest.php | 3 +- .../Pagination/TraversablePaginatorTest.php | 3 +- .../ApiPlatformExtensionTest.php | 2 +- 14 files changed, 52 insertions(+), 69 deletions(-) diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index cdbd4fe6bf3..1ddfe931bda 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Metadata; -use RuntimeException; - /** * @internal */ @@ -73,7 +71,7 @@ public function remove(string $key): self } } - throw new RuntimeException(sprintf('Could not remove operation "%s".', $key)); + throw new \RuntimeException(sprintf('Could not remove operation "%s".', $key)); } public function has(string $key): bool diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index 051efeccb4e..c11facaa008 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -17,7 +17,6 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; -use SplObjectStorage; use Symfony\Component\Routing\RouterInterface; /** @@ -36,7 +35,7 @@ final class SkolemIriConverter implements IriConverterInterface public function __construct(RouterInterface $router) { $this->router = $router; - $this->objectHashMap = new SplObjectStorage(); + $this->objectHashMap = new \SplObjectStorage(); } /** diff --git a/src/Test/DoctrineMongoDbOdmTestCase.php b/src/Test/DoctrineMongoDbOdmTestCase.php index 5ee6c03ee15..6d63d70b87f 100644 --- a/src/Test/DoctrineMongoDbOdmTestCase.php +++ b/src/Test/DoctrineMongoDbOdmTestCase.php @@ -20,8 +20,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use function sys_get_temp_dir; - /** * Source: https://github.com/doctrine/DoctrineMongoDBBundle/blob/0174003844bc566bb4cb3b7d10c5528d1924d719/Tests/TestCase.php * Test got excluded from vendor in 4.x. @@ -32,8 +30,8 @@ public static function createTestDocumentManager($paths = []): DocumentManager { $config = new Configuration(); $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_FILE_NOT_EXISTS); - $config->setProxyDir(sys_get_temp_dir()); - $config->setHydratorDir(sys_get_temp_dir()); + $config->setProxyDir(\sys_get_temp_dir()); + $config->setHydratorDir(\sys_get_temp_dir()); $config->setProxyNamespace('SymfonyTests\Doctrine'); $config->setHydratorNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AttributeDriver($paths, new AttributeReader())); // @phpstan-ignore-line diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php index 2088fdb7e50..27914032b87 100644 --- a/tests/Action/ExceptionActionTest.php +++ b/tests/Action/ExceptionActionTest.php @@ -21,7 +21,6 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use DomainException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -74,7 +73,7 @@ public function testActionWithOperationExceptionToStatus( ?array $operationExceptionToStatus, int $expectedStatusCode ): void { - $exception = new DomainException(); + $exception = new \DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -135,86 +134,86 @@ public function provideOperationExceptionToStatusCases(): \Generator ]; yield 'on global attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, null, 100, ]; yield 'on global attributes with empty resource and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], [], 100, ]; yield 'on global attributes and resource attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], null, 200, ]; yield 'on global attributes and resource attributes with empty operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], [], 200, ]; yield 'on global attributes and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global attributes and operation attributes with empty resource attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global, resource and operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 100], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on resource attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], null, 200, ]; yield 'on resource attributes with empty operation attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], [], 200, ]; yield 'on resource and operation attributes' => [ [], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes' => [ [], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes with empty resource attributes' => [ [], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; } diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index 5c3f97ba515..ce722cdc0c4 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -22,7 +22,6 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -39,7 +38,7 @@ public function testApplyToCollectionWithValidOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -60,7 +59,7 @@ public function testApplyToCollectionWithWrongOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -81,7 +80,7 @@ public function testApplyToCollectionWithOrderOverridden(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -102,7 +101,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled(); $aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled(); @@ -130,7 +129,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index 4501a935a59..37f4c6311d0 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; -use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -27,9 +26,9 @@ class VoDummyInspection #[ODM\Field(type: 'date')] private \DateTime $performed; - public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') + public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, \DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); } public function isAccepted(): bool @@ -42,12 +41,12 @@ public function getCar(): VoDummyCar return $this->car; } - public function getPerformed(): DateTime + public function getPerformed(): \DateTime { return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index 342579ca658..e2397c8d938 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -17,9 +17,6 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\State\AttributeResourceProvider; -use ArrayIterator; -use IteratorAggregate; -use Traversable; #[ApiResource( '/attribute_resources{._format}', @@ -28,7 +25,7 @@ )] #[GetCollection] #[Post] -final class AttributeResources implements IteratorAggregate +final class AttributeResources implements \IteratorAggregate { /** * @var AttributeResource[] @@ -40,8 +37,8 @@ public function __construct(AttributeResource ...$collection) $this->collection = $collection; } - public function getIterator(): Traversable + public function getIterator(): \Traversable { - return new ArrayIterator($this->collection); + return new \ArrayIterator($this->collection); } } diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index fc3eb0561f8..e382bc1b1b2 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; -use DateTime; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -28,9 +27,9 @@ class VoDummyInspection #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private \DateTime $performed; - public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') + public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, \DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); } public function isAccepted(): bool @@ -43,12 +42,12 @@ public function getCar(): ?VoDummyCar return $this->car; } - public function getPerformed(): DateTime + public function getPerformed(): \DateTime { return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/HttpCache/VarnishPurgerTest.php b/tests/HttpCache/VarnishPurgerTest.php index ffc2a5b53f4..faca342dc91 100644 --- a/tests/HttpCache/VarnishPurgerTest.php +++ b/tests/HttpCache/VarnishPurgerTest.php @@ -17,7 +17,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -79,12 +78,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -96,12 +95,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/HttpCache/VarnishXKeyPurgerTest.php b/tests/HttpCache/VarnishXKeyPurgerTest.php index 33c64da8678..96e380752b9 100644 --- a/tests/HttpCache/VarnishXKeyPurgerTest.php +++ b/tests/HttpCache/VarnishXKeyPurgerTest.php @@ -17,7 +17,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -109,12 +108,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -126,12 +125,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 04279076970..4c34da9000f 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -21,7 +21,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\PropertyAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlPropertyAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlPropertyAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -87,7 +86,7 @@ public function testValidMetadata(string $extractorClass, PropertyAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, self::PROPERTY, $parameters, self::FIXTURES)); $factory = new ExtractorPropertyMetadataFactory($extractor); $property = $factory->create(self::RESOURCE_CLASS, self::PROPERTY); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiProperty::class, 0, $exception); } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index 3c59cc9c12c..e6ce7a3727d 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -37,7 +37,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\ResourceAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -423,7 +422,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES)); $factory = new ExtractorResourceMetadataCollectionFactory($extractor); $collection = $factory->create(self::RESOURCE_CLASS); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception); } diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index ba51143119d..e9138e7571d 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\State\Pagination; use ApiPlatform\State\Pagination\TraversablePaginator; -use ArrayIterator; use PHPUnit\Framework\TestCase; class TraversablePaginatorTest extends TestCase @@ -30,7 +29,7 @@ public function testInitialize( float $lastPage, int $currentItems ): void { - $traversable = new ArrayIterator($results); + $traversable = new \ArrayIterator($results); $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 6b4ede1efd7..5be3cc47aed 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -672,7 +672,7 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.http_exception', 'api_platform.graphql.normalizer.runtime_exception', 'api_platform.graphql_metadata.resource.metadata_collection_factory', - 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters' + 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters', ]; $aliases = [ From 59826bbe9e246cf839bdc0c4d0d470f54e27b453 Mon Sep 17 00:00:00 2001 From: Liviu Cristian Mirea-Ghiban Date: Fri, 4 Nov 2022 09:50:53 +0200 Subject: [PATCH 14/19] fix: only alias if exists for opcache preload Fixes https://github.com/api-platform/api-platform/issues/2284 (#5110) Co-authored-by: Liviu Mirea --- src/deprecation.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/deprecation.php b/src/deprecation.php index b129c5e1eb8..671b8e595cc 100644 --- a/src/deprecation.php +++ b/src/deprecation.php @@ -12,8 +12,12 @@ declare(strict_types=1); // Must be declared first! -class_alias(ApiPlatform\Api\FilterInterface::class, ApiPlatform\Core\Api\FilterInterface::class); -class_alias(ApiPlatform\Api\ResourceClassResolverInterface::class, ApiPlatform\Core\Api\ResourceClassResolverInterface::class); +if (!interface_exists(ApiPlatform\Core\Api\FilterInterface::class)) { + class_alias(ApiPlatform\Api\FilterInterface::class, ApiPlatform\Core\Api\FilterInterface::class); +} +if (!interface_exists(ApiPlatform\Core\Api\ResourceClassResolverInterface::class)) { + class_alias(ApiPlatform\Api\ResourceClassResolverInterface::class, ApiPlatform\Core\Api\ResourceClassResolverInterface::class); +} $deprecatedInterfaces = include 'deprecated_interfaces.php'; foreach ($deprecatedInterfaces as $oldInterfaceName => $interfaceName) { From 5837ceb17d10855e3f63f569590c099a1414b062 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 16:29:31 +0100 Subject: [PATCH 15/19] chore: php-cs-fixer update (#5118) * chore: php-cs-fixer update * chore: php-cs-fixer update --- .../MongoDbOdm/Extension/OrderExtension.php | 3 +- .../Upgrade/UpgradeApiSubresourceVisitor.php | 3 +- src/Doctrine/Odm/Extension/OrderExtension.php | 3 +- .../Extractor/ResourceExtractorTrait.php | 17 ++++---- src/Metadata/Operations.php | 4 +- src/Symfony/Routing/SkolemIriConverter.php | 3 +- src/Test/DoctrineMongoDbOdmTestCase.php | 2 - tests/Action/ExceptionActionTest.php | 43 +++++++++---------- .../Metadata/Extractor/YamlExtractorTest.php | 3 +- .../Odm/Extension/OrderExtensionTest.php | 11 +++-- .../TestBundle/Document/VoDummyInspection.php | 9 ++-- .../TestBundle/Entity/AttributeResources.php | 9 ++-- .../TestBundle/Entity/VoDummyInspection.php | 8 ++-- tests/HttpCache/VarnishPurgerTest.php | 9 ++-- tests/HttpCache/VarnishXKeyPurgerTest.php | 9 ++-- .../PropertyMetadataCompatibilityTest.php | 3 +- .../ResourceMetadataCompatibilityTest.php | 3 +- .../Pagination/TraversablePaginatorTest.php | 3 +- 18 files changed, 62 insertions(+), 83 deletions(-) diff --git a/src/Core/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtension.php b/src/Core/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtension.php index 17f6a32ccbe..578368eeb03 100644 --- a/src/Core/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtension.php +++ b/src/Core/Bridge/Doctrine/MongoDbOdm/Extension/OrderExtension.php @@ -19,7 +19,6 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; /** * Applies selected ordering while querying resource collection. @@ -110,7 +109,7 @@ private function hasSortStage(Builder $aggregationBuilder): bool // If at least one stage is sort, then it has sorting return true; } - } catch (OutOfRangeException $outOfRangeException) { + } catch (\OutOfRangeException $outOfRangeException) { // There is no more stages on the aggregation builder $shouldStop = true; } diff --git a/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php b/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php index 14c94cc6f14..c818b9bd1cf 100644 --- a/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php +++ b/src/Core/Upgrade/UpgradeApiSubresourceVisitor.php @@ -21,7 +21,6 @@ use ApiPlatform\Metadata\Link; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; -use ReflectionClass; final class UpgradeApiSubresourceVisitor extends NodeVisitorAbstract { @@ -175,7 +174,7 @@ public function enterNode(Node $node) ]; if (null !== $this->referenceType) { - $urlGeneratorInterface = new ReflectionClass(UrlGeneratorInterface::class); + $urlGeneratorInterface = new \ReflectionClass(UrlGeneratorInterface::class); $urlGeneratorConstants = array_flip($urlGeneratorInterface->getConstants()); $currentUrlGeneratorConstant = $urlGeneratorConstants[$this->referenceType]; diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index 917d1d39bf9..f04e9cac236 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -19,7 +19,6 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; /** * Applies selected ordering while querying resource collection. @@ -101,7 +100,7 @@ private function hasSortStage(Builder $aggregationBuilder): bool // If at least one stage is sort, then it has sorting return true; } - } catch (OutOfRangeException $outOfRangeException) { + } catch (\OutOfRangeException $outOfRangeException) { // There is no more stages on the aggregation builder $shouldStop = true; } diff --git a/src/Metadata/Extractor/ResourceExtractorTrait.php b/src/Metadata/Extractor/ResourceExtractorTrait.php index 71c8cbfa2d4..2269575d555 100644 --- a/src/Metadata/Extractor/ResourceExtractorTrait.php +++ b/src/Metadata/Extractor/ResourceExtractorTrait.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Metadata\Extractor; use ApiPlatform\Exception\InvalidArgumentException; -use SimpleXMLElement; use Symfony\Component\Config\Util\XmlUtils; /** @@ -25,14 +24,14 @@ trait ResourceExtractorTrait { /** - * @param array|SimpleXMLElement|null $resource - * @param mixed|null $default + * @param array|\SimpleXMLElement|null $resource + * @param mixed|null $default * * @return array|null */ private function buildArrayValue($resource, string $key, $default = null) { - if (\is_object($resource) && $resource instanceof SimpleXMLElement) { + if (\is_object($resource) && $resource instanceof \SimpleXMLElement) { if (!isset($resource->{$key.'s'}->{$key})) { return $default; } @@ -54,8 +53,8 @@ private function buildArrayValue($resource, string $key, $default = null) /** * Transforms an attribute's value in a PHP value. * - * @param array|SimpleXMLElement|null $resource - * @param mixed|null $default + * @param array|\SimpleXMLElement|null $resource + * @param mixed|null $default * * @return string|int|bool|array|null */ @@ -73,7 +72,7 @@ private function phpize($resource, string $key, string $type, $default = null) case 'integer': return (int) $resource[$key]; case 'bool': - if (\is_object($resource) && $resource instanceof SimpleXMLElement) { + if (\is_object($resource) && $resource instanceof \SimpleXMLElement) { return (bool) XmlUtils::phpize($resource[$key]); } @@ -83,7 +82,7 @@ private function phpize($resource, string $key, string $type, $default = null) throw new InvalidArgumentException(sprintf('The property "%s" must be a "%s", "%s" given.', $key, $type, \gettype($resource[$key]))); } - private function buildArgs(SimpleXMLElement $resource): ?array + private function buildArgs(\SimpleXMLElement $resource): ?array { if (!isset($resource->args->arg)) { return null; @@ -97,7 +96,7 @@ private function buildArgs(SimpleXMLElement $resource): ?array return $data; } - private function buildValues(SimpleXMLElement $resource): array + private function buildValues(\SimpleXMLElement $resource): array { $data = []; foreach ($resource->value as $value) { diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index b11f81ba161..daf1904de79 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Metadata; -use RuntimeException; - final class Operations implements \IteratorAggregate, \Countable { private $operations; @@ -71,7 +69,7 @@ public function remove(string $key): self } } - throw new RuntimeException(sprintf('Could not remove operation "%s".', $key)); + throw new \RuntimeException(sprintf('Could not remove operation "%s".', $key)); } public function has(string $key): bool diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index 6cc5b7fcc27..7f9b0b86da8 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -17,7 +17,6 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; -use SplObjectStorage; use Symfony\Component\Routing\RouterInterface; /** @@ -36,7 +35,7 @@ final class SkolemIriConverter implements IriConverterInterface public function __construct(RouterInterface $router) { $this->router = $router; - $this->objectHashMap = new SplObjectStorage(); + $this->objectHashMap = new \SplObjectStorage(); } /** diff --git a/src/Test/DoctrineMongoDbOdmTestCase.php b/src/Test/DoctrineMongoDbOdmTestCase.php index b2bec59a0f2..45c3b8f78fb 100644 --- a/src/Test/DoctrineMongoDbOdmTestCase.php +++ b/src/Test/DoctrineMongoDbOdmTestCase.php @@ -21,8 +21,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; -use function sys_get_temp_dir; - /** * Source: https://github.com/doctrine/DoctrineMongoDBBundle/blob/0174003844bc566bb4cb3b7d10c5528d1924d719/Tests/TestCase.php * Test got excluded from vendor in 4.x. diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php index 37edd2d3d9d..bbaa5099b08 100644 --- a/tests/Action/ExceptionActionTest.php +++ b/tests/Action/ExceptionActionTest.php @@ -24,7 +24,6 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use DomainException; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; @@ -82,7 +81,7 @@ public function testLegacyActionWithOperationExceptionToStatus( ) { $this->expectDeprecation('Since api-platform/core 2.7: Use "ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface" instead of "ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface".'); - $exception = new DomainException(); + $exception = new \DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -135,7 +134,7 @@ public function testActionWithOperationExceptionToStatus( ?array $operationExceptionToStatus, int $expectedStatusCode ) { - $exception = new DomainException(); + $exception = new \DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -196,86 +195,86 @@ public function provideOperationExceptionToStatusCases() ]; yield 'on global attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, null, 100, ]; yield 'on global attributes with empty resource and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], [], 100, ]; yield 'on global attributes and resource attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], null, 200, ]; yield 'on global attributes and resource attributes with empty operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], + [\DomainException::class => 100], + [\DomainException::class => 200], [], 200, ]; yield 'on global attributes and operation attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global attributes and operation attributes with empty resource attributes' => [ - [DomainException::class => 100], + [\DomainException::class => 100], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on global, resource and operation attributes' => [ - [DomainException::class => 100], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 100], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on resource attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], null, 200, ]; yield 'on resource attributes with empty operation attributes' => [ [], - [DomainException::class => 200], + [\DomainException::class => 200], [], 200, ]; yield 'on resource and operation attributes' => [ [], - [DomainException::class => 200], - [DomainException::class => 300], + [\DomainException::class => 200], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes' => [ [], null, - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; yield 'on operation attributes with empty resource attributes' => [ [], [], - [DomainException::class => 300], + [\DomainException::class => 300], 300, ]; } diff --git a/tests/Core/Metadata/Extractor/YamlExtractorTest.php b/tests/Core/Metadata/Extractor/YamlExtractorTest.php index a949062dbe6..6c0bb335c90 100644 --- a/tests/Core/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Core/Metadata/Extractor/YamlExtractorTest.php @@ -15,7 +15,6 @@ use ApiPlatform\Core\Metadata\Extractor\YamlExtractor; use ApiPlatform\Exception\InvalidArgumentException; -use Generator; /** * @author Kévin Dunglas @@ -56,7 +55,7 @@ public function testInvalidResources(string $path, string $exceptionRegex) } } - public function provideInvalidResources(): Generator + public function provideInvalidResources(): \Generator { yield [ __DIR__.'/../../../Fixtures/FileConfigurations/resourcesinvalid.yml', diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index 80041cad7e4..e35a61d23a0 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -23,7 +23,6 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; -use OutOfRangeException; use PHPUnit\Framework\TestCase; /** @@ -39,7 +38,7 @@ public function testApplyToCollectionWithValidOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -60,7 +59,7 @@ public function testApplyToCollectionWithWrongOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -81,7 +80,7 @@ public function testApplyToCollectionWithOrderOverridden(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -102,7 +101,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled(); $aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled(); @@ -130,7 +129,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); - $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index ddc584d7a38..eff328408d3 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Core\Annotation\ApiResource; -use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -49,7 +48,7 @@ class VoDummyInspection private $car; /** - * @var DateTime + * @var \DateTime * * @ODM\Field(type="date") * @Groups({"car_read", "car_write", "inspection_read", "inspection_write"}) @@ -58,11 +57,11 @@ class VoDummyInspection private $attributeWithoutConstructorEquivalent; - public function __construct(bool $accepted, VoDummyCar $car, DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(bool $accepted, VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') { $this->accepted = $accepted; $this->car = $car; - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; } @@ -81,7 +80,7 @@ public function getPerformed() return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index 235a99dc7bf..da0fcc9ab57 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -17,9 +17,6 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\State\AttributeResourceProvider; -use ArrayIterator; -use IteratorAggregate; -use Traversable; #[ApiResource( '/attribute_resources.{_format}', @@ -28,7 +25,7 @@ )] #[GetCollection] #[Post] -final class AttributeResources implements IteratorAggregate +final class AttributeResources implements \IteratorAggregate { /** * @var AttributeResource[] @@ -40,8 +37,8 @@ public function __construct(AttributeResource ...$collection) $this->collection = $collection; } - public function getIterator(): Traversable + public function getIterator(): \Traversable { - return new ArrayIterator($this->collection); + return new \ArrayIterator($this->collection); } } diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index d4d8ee89385..fdc2d6c8114 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -49,7 +49,7 @@ class VoDummyInspection private $car; /** - * @var DateTime + * @var \DateTime * * @ORM\Column(type="datetime") * @Groups({"car_read", "car_write", "inspection_read", "inspection_write"}) @@ -58,11 +58,11 @@ class VoDummyInspection private $attributeWithoutConstructorEquivalent; - public function __construct(bool $accepted, VoDummyCar $car, DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(bool $accepted, VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') { $this->accepted = $accepted; $this->car = $car; - $this->performed = $performed ?: new DateTime(); + $this->performed = $performed ?: new \DateTime(); $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; } @@ -81,7 +81,7 @@ public function getPerformed() return $this->performed; } - public function setPerformed(DateTime $performed) + public function setPerformed(\DateTime $performed) { $this->performed = $performed; diff --git a/tests/HttpCache/VarnishPurgerTest.php b/tests/HttpCache/VarnishPurgerTest.php index c6ebe1c234c..621c71beef2 100644 --- a/tests/HttpCache/VarnishPurgerTest.php +++ b/tests/HttpCache/VarnishPurgerTest.php @@ -18,7 +18,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -79,12 +78,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -96,12 +95,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null) { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/HttpCache/VarnishXKeyPurgerTest.php b/tests/HttpCache/VarnishXKeyPurgerTest.php index 4b720a7c30b..77049c7918b 100644 --- a/tests/HttpCache/VarnishXKeyPurgerTest.php +++ b/tests/HttpCache/VarnishXKeyPurgerTest.php @@ -18,7 +18,6 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; -use LogicException; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -109,12 +108,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -126,12 +125,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } public function getConfig($option = null) { - throw new LogicException('Not implemented'); + throw new \LogicException('Not implemented'); } }; diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index e310c802468..35a331e0998 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -22,7 +22,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\PropertyAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlPropertyAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlPropertyAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -90,7 +89,7 @@ public function testValidMetadata(string $extractorClass, PropertyAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, self::PROPERTY, $parameters, self::FIXTURES)); $factory = new ExtractorPropertyMetadataFactory($extractor); $property = $factory->create(self::RESOURCE_CLASS, self::PROPERTY); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiProperty::class, 0, $exception); } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index f8fe305f995..b340b05543a 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -35,7 +35,6 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\ResourceAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter; -use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -423,7 +422,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES)); $factory = new ExtractorResourceMetadataCollectionFactory($extractor); $collection = $factory->create(self::RESOURCE_CLASS); - } catch (Exception $exception) { + } catch (\Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception); } diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index 5a1f635d960..f8c1ac36094 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Tests\State\Pagination; use ApiPlatform\State\Pagination\TraversablePaginator; -use ArrayIterator; use PHPUnit\Framework\TestCase; class TraversablePaginatorTest extends TestCase @@ -30,7 +29,7 @@ public function testInitialize( float $lastPage, float $currentItems ): void { - $traversable = new ArrayIterator($results); + $traversable = new \ArrayIterator($results); $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); From 176fff2cb15efa01b6c898d0442a4f540d4ddeaa Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 16:37:36 +0100 Subject: [PATCH 16/19] fix(metadata): upgrade script keep operation name (#5109) origin: https://github.com/api-platform/core/pull/5105 Co-authored-by: WilliamPeralta --- src/Core/Upgrade/UpgradeApiResourceVisitor.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/Upgrade/UpgradeApiResourceVisitor.php b/src/Core/Upgrade/UpgradeApiResourceVisitor.php index ead9a207f01..df48a3dee7b 100644 --- a/src/Core/Upgrade/UpgradeApiResourceVisitor.php +++ b/src/Core/Upgrade/UpgradeApiResourceVisitor.php @@ -359,8 +359,8 @@ private function removeAttribute(Node\Stmt\Class_|Node\Stmt\Interface_ $node) } } foreach ($node->stmts as $k => $stmts) { - foreach ($stmts->attrGroups as $i => $attrGroups) { - foreach ($attrGroups->attrs as $j => $attrs) { + foreach ($stmts->attrGroups ?? [] as $i => $attrGroups) { + foreach ($attrGroups->attrs ?? [] as $j => $attrs) { if (str_ends_with(implode('\\', $attrs->name->parts), 'ApiSubresource')) { unset($node->stmts[$k]->attrGroups[$i]); break; @@ -443,6 +443,9 @@ private function legacyOperationsToOperations($legacyOperations, bool $isCollect $method = $operation['method'] ?? strtoupper($operationName); unset($operation['method']); + if (!isset($operation['path']) && !\in_array($operationName, ['get', 'post', 'put', 'patch', 'delete'], true)) { + $operation['name'] = $operationName; + } $operations[] = $this->createOperation($this->getOperationNamespace($method, $isCollection), $operation); } From d81ef53acb085aaf88c7a3b42150de3ed654f1e3 Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Nov 2022 20:04:19 +0100 Subject: [PATCH 17/19] chore: v2.7.3 changelog --- CHANGELOG.md | 15 ++++++++++++++- generate-changelog.sh | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 generate-changelog.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a0aaf797f..d99a1758e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v2.7.3 + +### Bug fixes + +* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) +* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) +* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) +* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) +* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) +* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload +* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) +* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) + ## 2.7.2 * Metadata: no skolem IRI by default @@ -1211,4 +1224,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file diff --git a/generate-changelog.sh b/generate-changelog.sh new file mode 100755 index 00000000000..f2903beb1b3 --- /dev/null +++ b/generate-changelog.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# usage: generate-changelog.sh previous_tag next_tag +# example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md +log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) + +diff=$( +printf "# Changelog\n\n" +printf "## %s\n\n" "$2" + +if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; +then + printf "### Bug fixes\n\n" + printf "$log" | grep fix | grep -v chore | sort + printf "\n\n" +fi + +if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; +then + printf "### Features\n\n" + printf "$log" | grep feat | grep -v chore | sort +fi +) + +changelog=$(tail -n+2 CHANGELOG.md) +printf "%s\n%s" "$diff" "$changelog" From 6771f53f87b25bf62dbdc7819e69bdf389810b7e Mon Sep 17 00:00:00 2001 From: soyuka Date: Fri, 4 Nov 2022 20:08:27 +0100 Subject: [PATCH 18/19] chore: v3.0.3 changelog --- CHANGELOG.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b82f03fd6d..1921bdef057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,23 @@ # Changelog -## 3.0.3 +## v3.0.3 -* Graphql: add a clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) +### Bug fixes + +* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) +* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) +* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) +* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) +* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) +* [44337ddb3](https://github.com/api-platform/core/commit/44337ddb3908d7b05ed75b75325b7941581f575b) fix(graphql): use right nested operation (#5102) +* [541b738e9](https://github.com/api-platform/core/commit/541b738e942156b711665952b50fbd4f060fcdea) fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) +* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload +* [7044c5a1b](https://github.com/api-platform/core/commit/7044c5a1b2895e72f0579d1e788740606f94dece) fix(doctrine): use abitrary index instead of value (#5079) +* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) +* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) +* [a4cd12b2a](https://github.com/api-platform/core/commit/a4cd12b2a73bc0f726c5724de790f885884e6113) fix: uri template should respect rfc 6570 (#5080) +* [bbeaf7082](https://github.com/api-platform/core/commit/bbeaf7082bba4a019206c3862425cf849d55addd) fix(graphql): always allow to query nested resources (#5112) +* [c1cb3cd2f](https://github.com/api-platform/core/commit/c1cb3cd2ff32c8b1ee694b0989efeb133fbd8438) Revert "fix(graphql): use right nested operation (#5102)" (#5111) ## 3.0.2 From d44ddb152780dc9a9f4ac4c157a074331a67a99f Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Fri, 4 Nov 2022 20:20:44 +0100 Subject: [PATCH 19/19] Revert "Merge 3.0 into main (#5121)" This reverts commit 6abd0fe0a69d4842eb6d5c31ef2bd6dce0e1d372. --- CHANGELOG.md | 32 ---- features/graphql/collection.feature | 46 ----- features/main/default_order.feature | 36 +--- generate-changelog.sh | 25 --- src/Doctrine/Odm/Extension/OrderExtension.php | 3 +- src/Doctrine/Orm/Filter/SearchFilter.php | 6 +- src/GraphQl/Action/EntrypointAction.php | 6 +- ...NestedOperationResourceMetadataFactory.php | 67 -------- src/GraphQl/Type/FieldsBuilder.php | 71 ++++---- src/Metadata/Operations.php | 7 +- ...butesResourceMetadataCollectionFactory.php | 139 ++++++++++++++- ...actorResourceMetadataCollectionFactory.php | 25 +-- ...ltersResourceMetadataCollectionFactory.php | 10 +- .../Factory/OperationDefaultsTrait.php | 160 ------------------ ...plateResourceMetadataCollectionFactory.php | 8 +- src/OpenApi/Factory/OpenApiFactory.php | 3 +- .../NormalizeOperationNameTrait.php | 3 +- .../PayloadArgumentResolver.php | 2 +- .../ApiPlatformExtension.php | 17 +- .../Resources/config/doctrine_mongodb_odm.xml | 5 - .../Bundle/Resources/config/doctrine_orm.xml | 6 - .../Bundle/Resources/config/graphql.xml | 13 +- src/Symfony/Routing/ApiLoader.php | 5 - src/Symfony/Routing/SkolemIriConverter.php | 3 +- src/Test/DoctrineMongoDbOdmTestCase.php | 6 +- tests/Action/ExceptionActionTest.php | 41 ++--- tests/Behat/DoctrineContext.php | 17 +- .../Common/Filter/SearchFilterTestTrait.php | 12 -- .../Odm/Extension/OrderExtensionTest.php | 11 +- .../Doctrine/Odm/Filter/SearchFilterTest.php | 15 -- .../Doctrine/Orm/Filter/SearchFilterTest.php | 8 - .../TestBundle/Document/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Document/Answer.php | 4 +- tests/Fixtures/TestBundle/Document/Book.php | 2 +- tests/Fixtures/TestBundle/Document/Dummy.php | 4 +- .../Document/DummyAggregateOffer.php | 4 +- .../TestBundle/Document/DummyOffer.php | 6 +- .../TestBundle/Document/DummyProduct.php | 2 +- .../TestBundle/Document/DummyValidation.php | 2 +- .../Fixtures/TestBundle/Document/FooDummy.php | 13 -- .../TestBundle/Document/FourthLevel.php | 12 +- .../Fixtures/TestBundle/Document/Greeting.php | 2 +- .../TestBundle/Document/NetworkPathDummy.php | 2 +- .../Fixtures/TestBundle/Document/Question.php | 4 +- .../TestBundle/Document/RelatedDummy.php | 14 +- .../Document/RelatedToDummyFriend.php | 10 +- .../TestBundle/Document/SlugChildDummy.php | 4 +- .../TestBundle/Document/SlugParentDummy.php | 4 +- .../TestBundle/Document/ThirdLevel.php | 10 +- .../TestBundle/Document/VoDummyInspection.php | 12 +- .../TestBundle/Entity/AbsoluteUrlDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Answer.php | 4 +- .../Entity/AttributeOnlyOperation.php | 21 --- .../TestBundle/Entity/AttributeResource.php | 2 +- .../TestBundle/Entity/AttributeResources.php | 11 +- tests/Fixtures/TestBundle/Entity/Book.php | 2 +- tests/Fixtures/TestBundle/Entity/Dummy.php | 4 +- .../TestBundle/Entity/DummyAggregateOffer.php | 4 +- .../Fixtures/TestBundle/Entity/DummyOffer.php | 6 +- .../TestBundle/Entity/DummyProduct.php | 2 +- .../Entity/DummyToUpgradeProduct.php | 49 ------ .../DummyToUpgradeWithOnlyAnnotation.php | 61 ------- .../DummyToUpgradeWithOnlyAttribute.php | 49 ------ .../TestBundle/Entity/DummyValidation.php | 2 +- tests/Fixtures/TestBundle/Entity/FooDummy.php | 13 -- .../TestBundle/Entity/FourthLevel.php | 12 +- tests/Fixtures/TestBundle/Entity/Greeting.php | 2 +- .../TestBundle/Entity/NetworkPathDummy.php | 2 +- tests/Fixtures/TestBundle/Entity/Question.php | 4 +- .../TestBundle/Entity/RelatedDummy.php | 24 +-- .../Entity/RelatedToDummyFriend.php | 10 +- .../TestBundle/Entity/SlugChildDummy.php | 4 +- .../TestBundle/Entity/SlugParentDummy.php | 4 +- tests/Fixtures/TestBundle/Entity/SoMany.php | 3 - .../Fixtures/TestBundle/Entity/ThirdLevel.php | 10 +- .../TestBundle/Entity/VoDummyInspection.php | 12 +- ...edOperationResourceMetadataFactoryTest.php | 44 ----- tests/GraphQl/Type/FieldsBuilderTest.php | 137 ++++++--------- tests/GraphQl/Type/TypeBuilderTest.php | 4 +- tests/HttpCache/VarnishPurgerTest.php | 9 +- tests/HttpCache/VarnishXKeyPurgerTest.php | 9 +- .../Command/JsonSchemaGenerateCommandTest.php | 6 +- .../PropertyMetadataCompatibilityTest.php | 3 +- .../ResourceMetadataCompatibilityTest.php | 23 +-- tests/Metadata/Extractor/XmlExtractorTest.php | 6 +- .../Metadata/Extractor/YamlExtractorTest.php | 8 +- tests/Metadata/Extractor/xml/valid.xml | 4 +- tests/Metadata/Extractor/yaml/valid.yaml | 4 +- ...sResourceMetadataCollectionFactoryTest.php | 37 ++-- ...eResourceMetadataCollectionFactoryTest.php | 8 +- .../Pagination/TraversablePaginatorTest.php | 3 +- .../PayloadArgumentResolverTest.php | 8 +- .../ApiPlatformExtensionTest.php | 37 ---- 93 files changed, 462 insertions(+), 1128 deletions(-) delete mode 100755 generate-changelog.sh delete mode 100644 src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php delete mode 100644 src/Metadata/Resource/Factory/OperationDefaultsTrait.php delete mode 100644 tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php delete mode 100644 tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php delete mode 100644 tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1921bdef057..2be83b8dcb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,5 @@ # Changelog -## v3.0.3 - -### Bug fixes - -* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) -* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) -* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) -* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) -* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) -* [44337ddb3](https://github.com/api-platform/core/commit/44337ddb3908d7b05ed75b75325b7941581f575b) fix(graphql): use right nested operation (#5102) -* [541b738e9](https://github.com/api-platform/core/commit/541b738e942156b711665952b50fbd4f060fcdea) fix(graphql): add clearer error message when TwigBundle is disabled but graphQL clients are enabled (#5064) -* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload -* [7044c5a1b](https://github.com/api-platform/core/commit/7044c5a1b2895e72f0579d1e788740606f94dece) fix(doctrine): use abitrary index instead of value (#5079) -* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) -* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) -* [a4cd12b2a](https://github.com/api-platform/core/commit/a4cd12b2a73bc0f726c5724de790f885884e6113) fix: uri template should respect rfc 6570 (#5080) -* [bbeaf7082](https://github.com/api-platform/core/commit/bbeaf7082bba4a019206c3862425cf849d55addd) fix(graphql): always allow to query nested resources (#5112) -* [c1cb3cd2f](https://github.com/api-platform/core/commit/c1cb3cd2ff32c8b1ee694b0989efeb133fbd8438) Revert "fix(graphql): use right nested operation (#5102)" (#5111) - ## 3.0.2 * Metadata: generate skolem IRI by default, use `genId: false` to disable **BC** @@ -73,19 +54,6 @@ Breaking changes: * Serializer: `skip_null_values` now defaults to `true` * Metadata: `Patch` is added to the automatic CRUD -## v2.7.3 - -### Bug fixes - -* [176fff2cb](https://github.com/api-platform/core/commit/176fff2cb15efa01b6c898d0442a4f540d4ddeaa) fix(metadata): upgrade script keep operation name (#5109) -* [1b64ebf6a](https://github.com/api-platform/core/commit/1b64ebf6a438222ae091ec3690063d0fb1b61977) fix: upgrade command remove ApiSubresource attribute (#5049) -* [27fcdc6b2](https://github.com/api-platform/core/commit/27fcdc6b270d1699e76c37ccda690b8a5ed8b4c9) fix(metadata): deprecate when user decorates in legacy mode (#5091) -* [310363d56](https://github.com/api-platform/core/commit/310363d56129c94cf4d51977f85486729e582fbc) fix: remove @internal annotation for Operations (#5089) -* [41bbad94e](https://github.com/api-platform/core/commit/41bbad94e93df49eb4ade0fe1307b20d9cd07102) fix: update yaml extractor test file coding standard (#5068) -* [59826bbe9](https://github.com/api-platform/core/commit/59826bbe9e246cf839bdc0c4d0d470f54e27b453) fix: only alias if exists for opcache preload -* [8250d41a3](https://github.com/api-platform/core/commit/8250d41a38913a17364d617875bb5a90f434ec48) fix(metadata): define a name on a single operation (#5090) -* [9c19fa171](https://github.com/api-platform/core/commit/9c19fa17110aac7dd39bff827091c00b42a80d4f) fix(metadata): add class key in payload argument resolver (#5067) - ## 2.7.2 * Metadata: no skolem IRI by default diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index 9767505171a..1540549f7d8 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -910,49 +910,3 @@ Feature: GraphQL collection support Then the response status code should be 200 And the response should be in JSON And the JSON node "data.fooDummies.collection" should have 1 element - - @createSchema - Scenario: Retrieve paginated collections using mixed pagination - Given there are 5 fooDummy objects with fake names - When I send the following GraphQL request: - """ - { - fooDummies(page: 1) { - collection { - id - name - soManies(first: 2) { - edges { - node { - content - } - cursor - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - } - } - paginationInfo { - itemsPerPage - lastPage - totalCount - } - } - } - """ - Then the response status code should be 200 - And the response should be in JSON - And the JSON node "data.fooDummies.collection" should have 3 elements - And the JSON node "data.fooDummies.collection[2].id" should exist - And the JSON node "data.fooDummies.collection[2].name" should exist - And the JSON node "data.fooDummies.collection[2].soManies" should exist - And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements - And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1" - And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA==" - And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3 - And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2 - And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5 diff --git a/features/main/default_order.feature b/features/main/default_order.feature index 7458c2456e9..91211014315 100644 --- a/features/main/default_order.feature +++ b/features/main/default_order.feature @@ -79,61 +79,35 @@ Feature: Default order "@type": "FooDummy", "id": 5, "name": "Balbo", - "dummy": "/dummies/5", - "soManies": [ - "/so_manies/13", - "/so_manies/14", - "/so_manies/15" - ] - + "dummy": "/dummies/5" }, { "@id": "/foo_dummies/3", "@type": "FooDummy", "id": 3, "name": "Sthenelus", - "dummy": "/dummies/3", - "soManies": [ - "/so_manies/7", - "/so_manies/8", - "/so_manies/9" - ] + "dummy": "/dummies/3" }, { "@id": "/foo_dummies/2", "@type": "FooDummy", "id": 2, "name": "Ephesian", - "dummy": "/dummies/2", - "soManies": [ - "/so_manies/4", - "/so_manies/5", - "/so_manies/6" - ] + "dummy": "/dummies/2" }, { "@id": "/foo_dummies/1", "@type": "FooDummy", "id": 1, "name": "Hawsepipe", - "dummy": "/dummies/1", - "soManies": [ - "/so_manies/1", - "/so_manies/2", - "/so_manies/3" - ] + "dummy": "/dummies/1" }, { "@id": "/foo_dummies/4", "@type": "FooDummy", "id": 4, "name": "Separativeness", - "dummy": "/dummies/4", - "soManies": [ - "/so_manies/10", - "/so_manies/11", - "/so_manies/12" - ] + "dummy": "/dummies/4" } ], "hydra:totalItems": 5, diff --git a/generate-changelog.sh b/generate-changelog.sh deleted file mode 100755 index f2903beb1b3..00000000000 --- a/generate-changelog.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# usage: generate-changelog.sh previous_tag next_tag -# example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md -log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) - -diff=$( -printf "# Changelog\n\n" -printf "## %s\n\n" "$2" - -if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; -then - printf "### Bug fixes\n\n" - printf "$log" | grep fix | grep -v chore | sort - printf "\n\n" -fi - -if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; -then - printf "### Features\n\n" - printf "$log" | grep feat | grep -v chore | sort -fi -) - -changelog=$(tail -n+2 CHANGELOG.md) -printf "%s\n%s" "$diff" "$changelog" diff --git a/src/Doctrine/Odm/Extension/OrderExtension.php b/src/Doctrine/Odm/Extension/OrderExtension.php index 77cd255d69b..f3cf5eb3725 100644 --- a/src/Doctrine/Odm/Extension/OrderExtension.php +++ b/src/Doctrine/Odm/Extension/OrderExtension.php @@ -19,6 +19,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\Sort; use Doctrine\Persistence\ManagerRegistry; +use OutOfRangeException; /** * Applies selected ordering while querying resource collection. @@ -99,7 +100,7 @@ private function hasSortStage(Builder $aggregationBuilder): bool // If at least one stage is sort, then it has sorting return true; } - } catch (\OutOfRangeException $outOfRangeException) { + } catch (OutOfRangeException) { // There is no more stages on the aggregation builder $shouldStop = true; } diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index 8401ebb842a..3f35706b3b5 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -179,7 +179,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild $parameters = []; foreach ($values as $key => $value) { $keyValueParameter = sprintf('%s_%s', $valueParameter, $key); - $parameters[] = [$caseSensitive ? $value : strtolower($value), $keyValueParameter]; + $parameters[$caseSensitive ? $value : strtolower($value)] = $keyValueParameter; $ors[] = match ($strategy) { self::STRATEGY_PARTIAL => $queryBuilder->expr()->like( @@ -209,9 +209,7 @@ protected function addWhereByStrategy(string $strategy, QueryBuilder $queryBuild } $queryBuilder->andWhere($queryBuilder->expr()->orX(...$ors)); - foreach ($parameters as $parameter) { - $queryBuilder->setParameter($parameter[1], $parameter[0]); - } + array_walk($parameters, $queryBuilder->setParameter(...)); } /** diff --git a/src/GraphQl/Action/EntrypointAction.php b/src/GraphQl/Action/EntrypointAction.php index dcdd4a1dfd8..f4c9af93e68 100644 --- a/src/GraphQl/Action/EntrypointAction.php +++ b/src/GraphQl/Action/EntrypointAction.php @@ -34,7 +34,7 @@ final class EntrypointAction { private int $debug; - public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly ?GraphiQlAction $graphiQlAction, private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) + public function __construct(private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly GraphiQlAction $graphiQlAction, private readonly GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null) { $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; } @@ -43,11 +43,11 @@ public function __invoke(Request $request): Response { try { if ($request->isMethod('GET') && 'html' === $request->getRequestFormat()) { - if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) { + if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled) { return ($this->graphiQlAction)($request); } - if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) { + if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled) { return ($this->graphQlPlaygroundAction)($request); } } diff --git a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php deleted file mode 100644 index 05163b0c2b7..00000000000 --- a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * 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\GraphQl\Metadata\Factory; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Resource\Factory\OperationDefaultsTrait; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - -final class GraphQlNestedOperationResourceMetadataFactory implements ResourceMetadataCollectionFactoryInterface -{ - use OperationDefaultsTrait; - - public function __construct(array $defaults, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, ?LoggerInterface $logger = null) - { - $this->defaults = $defaults; - $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); - $this->logger = $logger ?? new NullLogger(); - } - - public function create(string $resourceClass): ResourceMetadataCollection - { - $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass); - - if ($this->decorated) { - $resourceMetadataCollection = $this->decorated->create($resourceClass); - } - - if (0 < \count($resourceMetadataCollection)) { - return $resourceMetadataCollection; - } - - $shortName = (false !== $pos = strrpos($resourceClass, '\\')) ? substr($resourceClass, $pos + 1) : $resourceClass; - - $apiResource = new ApiResource( - class: $resourceClass, - shortName: $shortName - ); - - if (class_exists($resourceClass)) { - $refl = new \ReflectionClass($resourceClass); - $attribute = $refl->getAttributes(ApiResource::class)[0] ?? null; - $attributeInstance = $attribute?->newInstance(); - if ($filters = $attributeInstance?->getFilters()) { - $apiResource = $apiResource->withFilters($filters); - } - } - - $resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource); - - return $resourceMetadataCollection; - } -} diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index d64f1cb800b..8817899c8ef 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -20,7 +20,9 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\Operation as AbstractOperation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -45,7 +47,7 @@ */ final class FieldsBuilder implements FieldsBuilderInterface { - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?ResourceMetadataCollectionFactoryInterface $graphQlNestedOperationResourceMetadataFactory = null) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, private readonly TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ResolverFactoryInterface $collectionResolverFactory, private readonly ResolverFactoryInterface $itemMutationResolverFactory, private readonly ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) { } @@ -254,23 +256,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $resourceClass = $type->getClassName(); } - $resourceOperation = $rootOperation; - if ($resourceClass && $rootOperation->getClass() && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - try { - $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); - } catch (OperationNotFoundException) { - // If there is no query operation for a nested resource we force one to exist - $nestedResourceMetadataCollection = $this->graphQlNestedOperationResourceMetadataFactory->create($resourceClass); - $resourceOperation = $nestedResourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); - } - } - - if (!$resourceOperation instanceof Operation) { - throw new \LogicException('The resource operation should be a GraphQL operation.'); - } - - $graphqlType = $this->convertType($type, $input, $resourceOperation, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); + $graphqlType = $this->convertType($type, $input, $rootOperation, $resourceClass ?? '', $rootResource, $property, $depth, $forceNullable); $graphqlWrappedType = $graphqlType instanceof WrappingType ? $graphqlType->getWrappedType(true) : $graphqlType; $isStandardGraphqlType = \in_array($graphqlWrappedType, GraphQLType::getStandardTypes(), true); @@ -285,22 +271,43 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; + $resolverOperation = $rootOperation; + + if ($resourceClass && $this->resourceClassResolver->isResourceClass($resourceClass) && $rootOperation->getClass() !== $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $resolverOperation = $resourceMetadataCollection->getOperation(null, $isCollectionType); + + if (!$resolverOperation instanceof Operation) { + $resolverOperation = ($isCollectionType ? new QueryCollection() : new Query())->withOperation($resolverOperation); + } + } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if ($this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); + if ($this->pagination->isGraphQlEnabled($rootOperation)) { + $args = $this->getGraphQlPaginationArgs($rootOperation); + } + + // Find the collection operation to get filters, there might be a smarter way to do this + $operation = null; + if (!empty($resourceClass)) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + try { + $operation = $resourceMetadataCollection->getOperation(null, true); + } catch (OperationNotFoundException) { + } } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $rootOperation, $property, $depth, $operation); } if ($isStandardGraphqlType || $input) { $resolve = null; } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resolverOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resolverOperation); } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); + $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resolverOperation); } return [ @@ -361,13 +368,13 @@ private function getGraphQlPaginationArgs(Operation $queryOperation): array return $args; } - private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $resourceOperation, Operation $rootOperation, ?string $property, int $depth): array + private function getFilterArgs(array $args, ?string $resourceClass, string $rootResource, Operation $rootOperation, ?string $property, int $depth, ?AbstractOperation $operation = null): array { - if (null === $resourceClass) { + if (null === $operation || null === $resourceClass) { return $args; } - foreach ($resourceOperation->getFilters() ?? [] as $filterId) { + foreach ($operation->getFilters() ?? [] as $filterId) { if (!$this->filterLocator->has($filterId)) { continue; } @@ -375,7 +382,7 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); + $graphqlFilterType = $this->convertType($filterType, false, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { $graphqlFilterType = GraphQLType::listOf($graphqlFilterType); @@ -392,14 +399,14 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root array_walk_recursive($parsed, static function (&$value) use ($graphqlFilterType): void { $value = $graphqlFilterType; }); - $args = $this->mergeFilterArgs($args, $parsed, $resourceOperation, $key); + $args = $this->mergeFilterArgs($args, $parsed, $operation, $key); } } return $this->convertFilterArgsToTypes($args); } - private function mergeFilterArgs(array $args, array $parsed, ?Operation $operation = null, string $original = ''): array + private function mergeFilterArgs(array $args, array $parsed, ?AbstractOperation $operation = null, string $original = ''): array { foreach ($parsed as $key => $value) { // Never override keys that cannot be merged @@ -463,7 +470,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); @@ -480,7 +487,7 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati } if ($this->typeBuilder->isCollection($type)) { - return $this->pagination->isGraphQlEnabled($resourceOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation) : GraphQLType::listOf($graphqlType); + return $this->pagination->isGraphQlEnabled($rootOperation) && !$input ? $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $rootOperation) : GraphQLType::listOf($graphqlType); } return $forceNullable || !$graphqlType instanceof NullableType || $type->isNullable() || ($rootOperation instanceof Mutation && 'update' === $rootOperation->getName()) diff --git a/src/Metadata/Operations.php b/src/Metadata/Operations.php index 5565bc99bd0..cdbd4fe6bf3 100644 --- a/src/Metadata/Operations.php +++ b/src/Metadata/Operations.php @@ -13,6 +13,11 @@ namespace ApiPlatform\Metadata; +use RuntimeException; + +/** + * @internal + */ final class Operations implements \IteratorAggregate, \Countable { private array $operations = []; @@ -68,7 +73,7 @@ public function remove(string $key): self } } - throw new \RuntimeException(sprintf('Could not remove operation "%s".', $key)); + throw new RuntimeException(sprintf('Could not remove operation "%s".', $key)); } public function has(string $key): bool diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9b125aed5c3..9255c236fa8 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -14,12 +14,20 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Exception\ResourceClassNotFoundException; +use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -37,12 +45,12 @@ */ final class AttributesResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - use OperationDefaultsTrait; + private readonly LoggerInterface $logger; + private readonly CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; - public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, array $defaults = [], private readonly bool $graphQlEnabled = false) + public function __construct(private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, LoggerInterface $logger = null, private readonly array $defaults = [], private readonly bool $graphQlEnabled = false) { $this->logger = $logger ?? new NullLogger(); - $this->defaults = $defaults; $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); } @@ -156,6 +164,115 @@ private function buildResourceOperations(array $attributes, string $resourceClas return $resources; } + private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array + { + // Inherit from resource defaults + foreach (get_class_methods($resource) as $methodName) { + if (!str_starts_with($methodName, 'get')) { + continue; + } + + if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { + continue; + } + + if (null === ($value = $resource->{$methodName}())) { + continue; + } + + $operation = $operation->{'with'.substr($methodName, 3)}($value); + } + + $operation = $operation->withExtraProperties(array_merge( + $resource->getExtraProperties(), + $operation->getExtraProperties(), + $generated ? ['generated_operation' => true] : [] + )); + + // Add global defaults attributes to the operation + $operation = $this->addGlobalDefaults($operation); + + if ($operation instanceof GraphQlOperation) { + if (!$operation->getName()) { + throw new RuntimeException('No GraphQL operation name.'); + } + + if ($operation instanceof Mutation) { + $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); + } + + return [$operation->getName(), $operation]; + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); + } + + if ($operation->getRouteName()) { + /** @var HttpOperation $operation */ + $operation = $operation->withName($operation->getRouteName()); + } + + // Check for name conflict + if ($operation->getName()) { + if (null !== $resource->getOperations() && !$resource->getOperations()->has($operation->getName())) { + return [$operation->getName(), $operation]; + } + + $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); + /** @var HttpOperation $operation */ + $operation = $operation->withName(''); + } + + return [ + sprintf( + '_api_%s_%s%s', + $operation->getUriTemplate() ?: $operation->getShortName(), + strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), + $operation instanceof CollectionOperationInterface ? '_collection' : '', + ), + $operation, + ]; + } + + private function addGlobalDefaults(ApiResource|HttpOperation|GraphQlOperation $operation): ApiResource|HttpOperation|GraphQlOperation + { + $extraProperties = []; + foreach ($this->defaults as $key => $value) { + $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); + $getter = 'get'.$upperKey; + + if (!method_exists($operation, $getter)) { + if (!isset($extraProperties[$key])) { + $extraProperties[$key] = $value; + } + } else { + $currentValue = $operation->{$getter}(); + + if (\is_array($currentValue) && $currentValue) { + $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); + } + + if (null !== $currentValue) { + continue; + } + + $operation = $operation->{'with'.$upperKey}($value); + } + } + + return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); + } + + private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource + { + $resource = $resource + ->withShortName($resource->getShortName() ?? $shortName) + ->withClass($resourceClass); + + return $this->addGlobalDefaults($resource); + } + private function hasResourceAttributes(\ReflectionClass $reflectionClass): bool { foreach ($reflectionClass->getAttributes() as $attribute) { @@ -186,6 +303,22 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, return false; } + private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource + { + $graphQlOperations = []; + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $graphQlOperations[$key] = $operation; + } + + if ($resource->getMercure()) { + [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); + $graphQlOperations[$key] = $operation; + } + + return $resource->withGraphQlOperations($graphQlOperations); + } + private function getDefaultHttpOperations($resource): iterable { $post = new Post(); diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index 86c154f7bc4..c44358897ca 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -19,12 +19,7 @@ use ApiPlatform\Metadata\Extractor\ResourceExtractorInterface; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -93,7 +88,9 @@ private function buildResources(array $nodes, string $resourceClass): array } } - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); + if (isset($node['graphQlOperations'])) { + $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'], $resource)); + } $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); } @@ -151,20 +148,6 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar { $operations = []; - if (null === $data) { - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { - $operation = $this->getOperationWithDefaults($resource, $operation); - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - $operations[$operation->getName()] = $operation; - } - - return $operations; - } - foreach ($data as $attributes) { /** @var HttpOperation $operation */ $operation = (new $attributes['graphql_operation_class']())->withShortName($resource->getShortName()); @@ -192,7 +175,7 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (($this->defaults['attributes'] ?? []) as $key => $value) { $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); diff --git a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php index 10a711fe71a..bb68448c4c2 100644 --- a/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/FiltersResourceMetadataCollectionFactory.php @@ -50,21 +50,17 @@ public function create(string $resourceClass): ResourceMetadataCollection $filters = array_keys($this->readFilterAttributes($reflectionClass)); foreach ($resourceMetadataCollection as $i => $resource) { - foreach ($operations = $resource->getOperations() ?? [] as $operationName => $operation) { + foreach ($operations = $resource->getOperations() as $operationName => $operation) { $operations->add($operationName, $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters)))); } - if ($operations) { - $resourceMetadataCollection[$i] = $resource->withOperations($operations); - } + $resourceMetadataCollection[$i] = $resource->withOperations($operations); foreach ($graphQlOperations = $resource->getGraphQlOperations() ?? [] as $operationName => $operation) { $graphQlOperations[$operationName] = $operation->withFilters(array_unique(array_merge($resource->getFilters() ?? [], $operation->getFilters() ?? [], $filters))); } - if ($graphQlOperations) { - $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); - } + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); } return $resourceMetadataCollection; diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php deleted file mode 100644 index cdb91e879a7..00000000000 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ /dev/null @@ -1,160 +0,0 @@ - - * - * 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\Resource\Factory; - -use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; -use Psr\Log\LoggerInterface; -use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; - -trait OperationDefaultsTrait -{ - private CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; - private array $defaults = []; - private LoggerInterface $logger; - - private function addGlobalDefaults(ApiResource|Operation $operation): ApiResource|Operation - { - $extraProperties = []; - foreach ($this->defaults as $key => $value) { - $upperKey = ucfirst($this->camelCaseToSnakeCaseNameConverter->denormalize($key)); - $getter = 'get'.$upperKey; - - if (!method_exists($operation, $getter)) { - if (!isset($extraProperties[$key])) { - $extraProperties[$key] = $value; - } - } else { - $currentValue = $operation->{$getter}(); - - if (\is_array($currentValue) && $currentValue) { - $operation = $operation->{'with'.$upperKey}(array_merge($value, $currentValue)); - } - - if (null !== $currentValue) { - continue; - } - - $operation = $operation->{'with'.$upperKey}($value); - } - } - - return $operation->withExtraProperties(array_merge($extraProperties, $operation->getExtraProperties())); - } - - private function getResourceWithDefaults(string $resourceClass, string $shortName, ApiResource $resource): ApiResource - { - $resource = $resource - ->withShortName($resource->getShortName() ?? $shortName) - ->withClass($resourceClass); - - return $this->addGlobalDefaults($resource); - } - - private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource - { - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $i => $operation) { - [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); - $graphQlOperations[$key] = $operation; - } - - if ($resource->getMercure()) { - [$key, $operation] = $this->getOperationWithDefaults($resource, (new Subscription())->withDescription("Subscribes to the update event of a {$operation->getShortName()}.")); - $graphQlOperations[$key] = $operation; - } - - return $resource->withGraphQlOperations($graphQlOperations); - } - - private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array - { - // Inherit from resource defaults - foreach (get_class_methods($resource) as $methodName) { - if (!str_starts_with($methodName, 'get')) { - continue; - } - - if (!method_exists($operation, $methodName) || null !== $operation->{$methodName}()) { - continue; - } - - if (null === ($value = $resource->{$methodName}())) { - continue; - } - - $operation = $operation->{'with'.substr($methodName, 3)}($value); - } - - $operation = $operation->withExtraProperties(array_merge( - $resource->getExtraProperties(), - $operation->getExtraProperties(), - $generated ? ['generated_operation' => true] : [] - )); - - // Add global defaults attributes to the operation - $operation = $this->addGlobalDefaults($operation); - - if ($operation instanceof GraphQlOperation) { - if (!$operation->getName()) { - throw new RuntimeException('No GraphQL operation name.'); - } - - if ($operation instanceof Mutation) { - $operation = $operation->withDescription(ucfirst("{$operation->getName()}s a {$resource->getShortName()}.")); - } - - return [$operation->getName(), $operation]; - } - - if (!$operation instanceof HttpOperation) { - throw new RuntimeException(sprintf('Operation should be an instance of "%s"', HttpOperation::class)); - } - - if ($operation->getRouteName()) { - /** @var HttpOperation $operation */ - $operation = $operation->withName($operation->getRouteName()); - } - - // Check for name conflict - if ($operation->getName() && null !== ($operations = $resource->getOperations())) { - if (!$operations->has($operation->getName())) { - return [$operation->getName(), $operation]; - } - - $this->logger->warning(sprintf('The operation "%s" already exists on the resource "%s", pick a different name or leave it empty. In the meantime we will generate a unique name.', $operation->getName(), $resource->getClass())); - /** @var HttpOperation $operation */ - $operation = $operation->withName(''); - } - - return [ - sprintf( - '_api_%s_%s%s', - $operation->getUriTemplate() ?: $operation->getShortName(), - strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), - $operation instanceof CollectionOperationInterface ? '_collection' : '', - ), - $operation, - ]; - } -} diff --git a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php index 7b4a5ca7c70..1ceacfc5636 100644 --- a/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactory.php @@ -105,7 +105,7 @@ private function generateUriTemplate(HttpOperation $operation): string } } - return sprintf('%s{._format}', $uriTemplate); + return sprintf('%s.{_format}', $uriTemplate); } private function configureUriVariables(ApiResource|HttpOperation $operation): ApiResource|HttpOperation @@ -144,12 +144,8 @@ private function configureUriVariables(ApiResource|HttpOperation $operation): Ap } $operation = $operation->withUriVariables($uriVariables); - if (str_ends_with($uriTemplate, '{._format}')) { - $uriTemplate = substr($uriTemplate, 0, -10); - } - $route = (new Route($uriTemplate))->compile(); - $variables = $route->getPathVariables(); + $variables = array_filter($route->getPathVariables(), fn ($v): bool => '_format' !== $v); if (\count($variables) !== \count($uriVariables)) { $newUriVariables = []; diff --git a/src/OpenApi/Factory/OpenApiFactory.php b/src/OpenApi/Factory/OpenApiFactory.php index 75f1d2ee8bb..34dc68f2cfe 100644 --- a/src/OpenApi/Factory/OpenApiFactory.php +++ b/src/OpenApi/Factory/OpenApiFactory.php @@ -333,8 +333,7 @@ private function flattenMimeTypes(array $responseFormats): array */ private function getPath(string $path): string { - // Handle either API Platform's URI Template (rfc6570) or Symfony's route - if (str_ends_with($path, '{._format}') || str_ends_with($path, '.{_format}')) { + if (str_ends_with($path, '.{_format}')) { $path = substr($path, 0, -10); } diff --git a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php index 2d37d93b8f1..53bb0b22a10 100644 --- a/src/OpenApi/Serializer/NormalizeOperationNameTrait.php +++ b/src/OpenApi/Serializer/NormalizeOperationNameTrait.php @@ -22,6 +22,7 @@ trait NormalizeOperationNameTrait { private function normalizeOperationName(string $operationName): string { - return preg_replace('/^_/', '', str_replace(['/', '{._format}', '{', '}'], ['', '', '_', ''], $operationName)); + // .{_format} is related to the symfony router + return preg_replace('/^_/', '', str_replace(['/', '.{_format}', '{', '}'], ['', '', '_', ''], $operationName)); } } diff --git a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php index bf11ad4d37b..97cd93678b3 100644 --- a/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php +++ b/src/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolver.php @@ -71,6 +71,6 @@ private function getExpectedInputClass(Request $request): ?string $context = $this->serializationContextBuilder->createFromRequest($request, false, RequestAttributesExtractor::extractAttributes($request)); - return $context['input']['class'] ?? $context['resource_class'] ?? null; + return $context['input'] ?? $context['resource_class']; } } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 3b1c8faeb5a..fcc023abb68 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -53,7 +53,6 @@ use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Yaml\Yaml; -use Twig\Environment; /** * The extension of this bundle. @@ -463,12 +462,9 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array { $enabled = $this->isConfigEnabled($container, $config['graphql']); - $graphiqlEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql']); - $graphqlPlayGroundEnabled = $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground']); - $container->setParameter('api_platform.graphql.enabled', $enabled); - $container->setParameter('api_platform.graphql.graphiql.enabled', $graphiqlEnabled); - $container->setParameter('api_platform.graphql.graphql_playground.enabled', $graphqlPlayGroundEnabled); + $container->setParameter('api_platform.graphql.graphiql.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphiql'])); + $container->setParameter('api_platform.graphql.graphql_playground.enabled', $enabled && $this->isConfigEnabled($container, $config['graphql']['graphql_playground'])); $container->setParameter('api_platform.graphql.collection.pagination', $config['graphql']['collection']['pagination']); if (!$enabled) { @@ -480,15 +476,6 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array $loader->load('graphql.xml'); - // @phpstan-ignore-next-line because PHPStan uses the container of the test env cache and in test the parameter kernel.bundles always contains the key TwigBundle - if (!class_exists(Environment::class) || !isset($container->getParameter('kernel.bundles')['TwigBundle'])) { - if ($graphiqlEnabled || $graphqlPlayGroundEnabled) { - throw new RuntimeException(sprintf('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the %s environnement or disable GraphiQL and GraphQL Playground.', $container->getParameter('kernel.environment'))); - } - $container->removeDefinition('api_platform.graphql.action.graphiql'); - $container->removeDefinition('api_platform.graphql.action.graphql_playground'); - } - $container->registerForAutoconfiguration(QueryItemResolverInterface::class) ->addTag('api_platform.graphql.query_resolver'); $container->registerForAutoconfiguration(QueryCollectionResolverInterface::class) diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 29f9a7a761f..4cecc9d339a 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -156,11 +156,6 @@ - - - - - diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index 0b6949bb9b8..a46540ab0a9 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -166,12 +166,6 @@ - - - - - - diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 0f713b273b9..debaefb508c 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -49,8 +49,8 @@ - - + + %kernel.debug% @@ -133,7 +133,6 @@ %api_platform.graphql.nesting_separator% - @@ -275,14 +274,6 @@ - - - %api_platform.defaults% - - - - - diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index a9b636e063c..923a9b50110 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -75,11 +75,6 @@ public function load(mixed $data, string $type = null): RouteCollection $path = str_replace(sprintf('{%s}', $parameterName), $expandedValue, $path); } - // Within Symfony .{_format} is a special parameter but the rfc6570 specifies label expansion with a dot operator - if (str_ends_with($path, '{._format}')) { - $path = str_replace('{._format}', '.{_format}', $path); - } - if (($controller = $operation->getController()) && !$this->container->has($controller)) { throw new RuntimeException(sprintf('There is no builtin action for the "%s" operation. You need to define the controller yourself.', $operationName)); } diff --git a/src/Symfony/Routing/SkolemIriConverter.php b/src/Symfony/Routing/SkolemIriConverter.php index c11facaa008..051efeccb4e 100644 --- a/src/Symfony/Routing/SkolemIriConverter.php +++ b/src/Symfony/Routing/SkolemIriConverter.php @@ -17,6 +17,7 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Operation; +use SplObjectStorage; use Symfony\Component\Routing\RouterInterface; /** @@ -35,7 +36,7 @@ final class SkolemIriConverter implements IriConverterInterface public function __construct(RouterInterface $router) { $this->router = $router; - $this->objectHashMap = new \SplObjectStorage(); + $this->objectHashMap = new SplObjectStorage(); } /** diff --git a/src/Test/DoctrineMongoDbOdmTestCase.php b/src/Test/DoctrineMongoDbOdmTestCase.php index 6d63d70b87f..5ee6c03ee15 100644 --- a/src/Test/DoctrineMongoDbOdmTestCase.php +++ b/src/Test/DoctrineMongoDbOdmTestCase.php @@ -20,6 +20,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use function sys_get_temp_dir; + /** * Source: https://github.com/doctrine/DoctrineMongoDBBundle/blob/0174003844bc566bb4cb3b7d10c5528d1924d719/Tests/TestCase.php * Test got excluded from vendor in 4.x. @@ -30,8 +32,8 @@ public static function createTestDocumentManager($paths = []): DocumentManager { $config = new Configuration(); $config->setAutoGenerateProxyClasses(Configuration::AUTOGENERATE_FILE_NOT_EXISTS); - $config->setProxyDir(\sys_get_temp_dir()); - $config->setHydratorDir(\sys_get_temp_dir()); + $config->setProxyDir(sys_get_temp_dir()); + $config->setHydratorDir(sys_get_temp_dir()); $config->setProxyNamespace('SymfonyTests\Doctrine'); $config->setHydratorNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AttributeDriver($paths, new AttributeReader())); // @phpstan-ignore-line diff --git a/tests/Action/ExceptionActionTest.php b/tests/Action/ExceptionActionTest.php index 27914032b87..2088fdb7e50 100644 --- a/tests/Action/ExceptionActionTest.php +++ b/tests/Action/ExceptionActionTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use DomainException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; @@ -73,7 +74,7 @@ public function testActionWithOperationExceptionToStatus( ?array $operationExceptionToStatus, int $expectedStatusCode ): void { - $exception = new \DomainException(); + $exception = new DomainException(); $flattenException = FlattenException::create($exception); $serializer = $this->prophesize(SerializerInterface::class); @@ -134,86 +135,86 @@ public function provideOperationExceptionToStatusCases(): \Generator ]; yield 'on global attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], null, null, 100, ]; yield 'on global attributes with empty resource and operation attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], [], [], 100, ]; yield 'on global attributes and resource attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], + [DomainException::class => 100], + [DomainException::class => 200], null, 200, ]; yield 'on global attributes and resource attributes with empty operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], + [DomainException::class => 100], + [DomainException::class => 200], [], 200, ]; yield 'on global attributes and operation attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], null, - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on global attributes and operation attributes with empty resource attributes' => [ - [\DomainException::class => 100], + [DomainException::class => 100], [], - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on global, resource and operation attributes' => [ - [\DomainException::class => 100], - [\DomainException::class => 200], - [\DomainException::class => 300], + [DomainException::class => 100], + [DomainException::class => 200], + [DomainException::class => 300], 300, ]; yield 'on resource attributes' => [ [], - [\DomainException::class => 200], + [DomainException::class => 200], null, 200, ]; yield 'on resource attributes with empty operation attributes' => [ [], - [\DomainException::class => 200], + [DomainException::class => 200], [], 200, ]; yield 'on resource and operation attributes' => [ [], - [\DomainException::class => 200], - [\DomainException::class => 300], + [DomainException::class => 200], + [DomainException::class => 300], 300, ]; yield 'on operation attributes' => [ [], null, - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; yield 'on operation attributes with empty resource attributes' => [ [], [], - [\DomainException::class => 300], + [DomainException::class => 300], 300, ]; } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 37aaa67205b..5e02353fcfb 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -286,10 +286,10 @@ public function thereArePaginationEntities(int $nb): void public function thereAreOfTheseSoManyObjects(int $nb): void { for ($i = 1; $i <= $nb; ++$i) { - $soMany = $this->buildSoMany(); - $soMany->content = 'Many #'.$i; + $dummy = $this->isOrm() ? new SoMany() : new SoManyDocument(); + $dummy->content = 'Many #'.$i; - $this->manager->persist($soMany); + $this->manager->persist($dummy); } $this->manager->flush(); @@ -340,12 +340,6 @@ public function thereAreFooDummyObjectsWithFakeNames($nb): void $foo = $this->buildFooDummy(); $foo->setName($names[$i]); $foo->setDummy($dummy); - for ($j = 0; $j < 3; ++$j) { - $soMany = $this->buildSoMany(); - $soMany->content = "So many $j"; - $soMany->fooDummy = $foo; - $foo->soManies->add($soMany); - } $this->manager->persist($foo); } @@ -2206,11 +2200,6 @@ private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDu return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument(); } - private function buildSoMany(): SoMany|SoManyDocument - { - return $this->isOrm() ? new SoMany() : new SoManyDocument(); - } - private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument { return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument(); diff --git a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php index 51b8811918d..a7fc3780999 100644 --- a/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php +++ b/tests/Doctrine/Common/Filter/SearchFilterTestTrait.php @@ -297,18 +297,6 @@ private function provideApplyTestArguments(): array ], ], ], - 'partial (multiple almost same values; case insensitive)' => [ - [ - 'id' => null, - 'name' => 'ipartial', - ], - [ - 'name' => [ - 'blue car', - 'Blue Car', - ], - ], - ], 'start' => [ [ 'id' => null, diff --git a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php index ce722cdc0c4..5c3f97ba515 100644 --- a/tests/Doctrine/Odm/Extension/OrderExtensionTest.php +++ b/tests/Doctrine/Odm/Extension/OrderExtensionTest.php @@ -22,6 +22,7 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; +use OutOfRangeException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,7 +39,7 @@ public function testApplyToCollectionWithValidOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -59,7 +60,7 @@ public function testApplyToCollectionWithWrongOrder(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['name' => 'asc'])->shouldNotBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -80,7 +81,7 @@ public function testApplyToCollectionWithOrderOverridden(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'DESC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -101,7 +102,7 @@ public function testApplyToCollectionWithOrderOverriddenWithNoDirection(): void { $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['foo' => 'ASC'])->shouldBeCalled(); $aggregationBuilderProphecy->sort(['foo' => 'ASC', 'bar' => 'DESC'])->shouldBeCalled(); @@ -129,7 +130,7 @@ public function testApplyToCollectionWithOrderOverriddenWithAssociation(): void $lookupProphecy->alias('author_lkup')->shouldBeCalled(); $aggregationBuilderProphecy->lookup(Dummy::class)->shouldBeCalled()->willReturn($lookupProphecy->reveal()); $aggregationBuilderProphecy->unwind('$author_lkup')->shouldBeCalled(); - $aggregationBuilderProphecy->getStage(0)->willThrow(new \OutOfRangeException('message')); + $aggregationBuilderProphecy->getStage(0)->willThrow(new OutOfRangeException('message')); $aggregationBuilderProphecy->sort(['author_lkup.name' => 'ASC'])->shouldBeCalled(); $classMetadataProphecy = $this->prophesize(ClassMetadata::class); diff --git a/tests/Doctrine/Odm/Filter/SearchFilterTest.php b/tests/Doctrine/Odm/Filter/SearchFilterTest.php index 5dd394ce965..28b721f3475 100644 --- a/tests/Doctrine/Odm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Odm/Filter/SearchFilterTest.php @@ -426,21 +426,6 @@ public function provideApplyTestData(): array ], $filterFactory, ], - 'partial (multiple almost same values; case insensitive)' => [ - [ - [ - '$match' => [ - 'name' => [ - '$in' => [ - new Regex('blue car', 'i'), - new Regex('Blue Car', 'i'), - ], - ], - ], - ], - ], - $filterFactory, - ], 'start' => [ [ [ diff --git a/tests/Doctrine/Orm/Filter/SearchFilterTest.php b/tests/Doctrine/Orm/Filter/SearchFilterTest.php index 7ad972534a7..2d2f2a79e1f 100644 --- a/tests/Doctrine/Orm/Filter/SearchFilterTest.php +++ b/tests/Doctrine/Orm/Filter/SearchFilterTest.php @@ -352,14 +352,6 @@ public function provideApplyTestData(): array ], $filterFactory, ], - 'partial (multiple almost same values; case insensitive)' => [ - sprintf('SELECT %s FROM %s %1$s WHERE LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_0, \'%%\')) OR LOWER(%1$s.name) LIKE LOWER(CONCAT(\'%%\', :name_p1_1, \'%%\'))', $this->alias, Dummy::class), - [ - 'name_p1_0' => 'blue car', - 'name_p1_1' => 'blue car', - ], - $filterFactory, - ], 'start' => [ sprintf('SELECT %s FROM %s %1$s WHERE %1$s.name LIKE CONCAT(:name_p1_0, \'%%\')', $this->alias, Dummy::class), ['name_p1_0' => 'partial'], diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php index 23b8bea0b1d..1209f780a9a 100644 --- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ODM\Document] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Document/Answer.php b/tests/Fixtures/TestBundle/Document/Answer.php index 2b45253e55d..03023a4c892 100644 --- a/tests/Fixtures/TestBundle/Document/Answer.php +++ b/tests/Fixtures/TestBundle/Document/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ODM\Document] class Answer { diff --git a/tests/Fixtures/TestBundle/Document/Book.php b/tests/Fixtures/TestBundle/Document/Book.php index c74a8a30328..e95179ce536 100644 --- a/tests/Fixtures/TestBundle/Document/Book.php +++ b/tests/Fixtures/TestBundle/Document/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ODM\Document] class Book { diff --git a/tests/Fixtures/TestBundle/Document/Dummy.php b/tests/Fixtures/TestBundle/Document/Dummy.php index cfa585544e0..4a8b56c991c 100644 --- a/tests/Fixtures/TestBundle/Document/Dummy.php +++ b/tests/Fixtures/TestBundle/Document/Dummy.php @@ -29,8 +29,8 @@ * @author Alexandre Delplace */ #[ApiResource(extraProperties: ['doctrine_mongodb' => ['execute_options' => ['allowDiskUse' => true]]], filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.mongodb.boolean', 'my_dummy.mongodb.date', 'my_dummy.mongodb.exists', 'my_dummy.mongodb.numeric', 'my_dummy.mongodb.order', 'my_dummy.mongodb.range', 'my_dummy.mongodb.search', 'my_dummy.property'], operations: [new Get()])] #[ODM\Document] class Dummy { diff --git a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php index 8ccf3657399..ebec8ed9c59 100644 --- a/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyOffer.php b/tests/Fixtures/TestBundle/Document/DummyOffer.php index 518829e632b..80bb1aecaa8 100644 --- a/tests/Fixtures/TestBundle/Document/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Document/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Document/DummyProduct.php b/tests/Fixtures/TestBundle/Document/DummyProduct.php index 065956e5ee7..7ea04a0447a 100644 --- a/tests/Fixtures/TestBundle/Document/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Document/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ODM\Document] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Document/DummyValidation.php b/tests/Fixtures/TestBundle/Document/DummyValidation.php index a0fa6b10033..587fd26b7be 100644 --- a/tests/Fixtures/TestBundle/Document/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Document/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation{._format}'), + new Post(uriTemplate: 'dummy_validation.{_format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Document/FooDummy.php b/tests/Fixtures/TestBundle/Document/FooDummy.php index 8736509e855..cc9fe959f25 100644 --- a/tests/Fixtures/TestBundle/Document/FooDummy.php +++ b/tests/Fixtures/TestBundle/Document/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; /** @@ -44,17 +42,6 @@ class FooDummy #[ODM\ReferenceOne(targetDocument: Dummy::class, cascade: ['persist'], storeAs: 'id')] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ODM\ReferenceMany(targetDocument: SoMany::class, cascade: ['persist'], storeAs: 'id')] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Document/FourthLevel.php b/tests/Fixtures/TestBundle/Document/FourthLevel.php index bf653bfbc37..46a9cd1d615 100644 --- a/tests/Fixtures/TestBundle/Document/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Document/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Document/Greeting.php b/tests/Fixtures/TestBundle/Document/Greeting.php index bb6ec15928a..086b22e84b1 100644 --- a/tests/Fixtures/TestBundle/Document/Greeting.php +++ b/tests/Fixtures/TestBundle/Document/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Greeting { diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php index bd2794e67a5..41b63270bf8 100644 --- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ODM\Document] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Document/Question.php b/tests/Fixtures/TestBundle/Document/Question.php index 417f78864b9..6375515e66c 100644 --- a/tests/Fixtures/TestBundle/Document/Question.php +++ b/tests/Fixtures/TestBundle/Document/Question.php @@ -19,8 +19,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class Question { diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index a400aa06450..0ca2c57dc04 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -37,13 +37,13 @@ * @author Alexandre Delplace */ #[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.mongodb.friends'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.mongodb.friends'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ODM\Document] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php index 8955436b628..254fd5da549 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Document/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.mongodb.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.mongodb.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ODM\Document] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php index 890cf391652..4d210239e04 100644 --- a/tests/Fixtures/TestBundle/Document/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ODM\Document] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php index ed34ea94bd6..4ff555b8764 100644 --- a/tests/Fixtures/TestBundle/Document/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Document/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ODM\Document] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index 046b89afb16..e4b6fe19cfa 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -26,11 +26,11 @@ * @author Alexandre Delplace */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ODM\Document] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php index 703abcff6a3..4501a935a59 100644 --- a/tests/Fixtures/TestBundle/Document/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Document/VoDummyInspection.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Document; use ApiPlatform\Metadata\ApiResource; +use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -26,12 +27,9 @@ class VoDummyInspection #[ODM\Field(type: 'date')] private \DateTime $performed; - private $attributeWithoutConstructorEquivalent; - - public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(#[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] #[ODM\Field(type: 'bool')] private bool $accepted, #[Groups(['inspection_read', 'inspection_write'])] #[ODM\ReferenceOne(targetDocument: VoDummyCar::class, inversedBy: 'inspections')] private VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new \DateTime(); - $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; + $this->performed = $performed ?: new DateTime(); } public function isAccepted(): bool @@ -44,12 +42,12 @@ public function getCar(): VoDummyCar return $this->car; } - public function getPerformed(): \DateTime + public function getPerformed(): DateTime { return $this->performed; } - public function setPerformed(\DateTime $performed) + public function setPerformed(DateTime $performed) { $this->performed = $performed; diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php index dfb1f919c21..ff2fb260aca 100644 --- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php +++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)] -#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])] #[ORM\Entity] class AbsoluteUrlDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Answer.php b/tests/Fixtures/TestBundle/Entity/Answer.php index 945fd5e3b6d..7a6fb033669 100644 --- a/tests/Fixtures/TestBundle/Entity/Answer.php +++ b/tests/Fixtures/TestBundle/Entity/Answer.php @@ -29,8 +29,8 @@ * Answer. */ #[ApiResource(operations: [new Get(), new Put(), new Patch(), new Delete(), new GetCollection(normalizationContext: ['groups' => ['foobar']])])] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer{._format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions/{relatedQuestions}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], toProperty: 'answer'), 'relatedQuestions' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer.{_format}', uriVariables: ['id' => new Link(fromClass: Question::class, identifiers: ['id'], fromProperty: 'answer')], status: 200, operations: [new Get()])] #[ORM\Entity] class Answer { diff --git a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php b/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php deleted file mode 100644 index e13550bb603..00000000000 --- a/tests/Fixtures/TestBundle/Entity/AttributeOnlyOperation.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * 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\Entity; - -use ApiPlatform\Metadata\Get; - -#[Get(name: 'my own name')] -final class AttributeOnlyOperation -{ -} diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResource.php b/tests/Fixtures/TestBundle/Entity/AttributeResource.php index 0c4649e27e7..560251f7c69 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResource.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResource.php @@ -31,7 +31,7 @@ #[Put] #[Delete] #[ApiResource( - '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', inputFormats: ['json' => ['application/merge-patch+json']], status: 301, provider: AttributeResourceProvider::class, diff --git a/tests/Fixtures/TestBundle/Entity/AttributeResources.php b/tests/Fixtures/TestBundle/Entity/AttributeResources.php index e2397c8d938..6685647c810 100644 --- a/tests/Fixtures/TestBundle/Entity/AttributeResources.php +++ b/tests/Fixtures/TestBundle/Entity/AttributeResources.php @@ -17,15 +17,18 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\State\AttributeResourceProvider; +use ArrayIterator; +use IteratorAggregate; +use Traversable; #[ApiResource( - '/attribute_resources{._format}', + '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], provider: AttributeResourceProvider::class )] #[GetCollection] #[Post] -final class AttributeResources implements \IteratorAggregate +final class AttributeResources implements IteratorAggregate { /** * @var AttributeResource[] @@ -37,8 +40,8 @@ public function __construct(AttributeResource ...$collection) $this->collection = $collection; } - public function getIterator(): \Traversable + public function getIterator(): Traversable { - return new \ArrayIterator($this->collection); + return new ArrayIterator($this->collection); } } diff --git a/tests/Fixtures/TestBundle/Entity/Book.php b/tests/Fixtures/TestBundle/Entity/Book.php index f892b81cb3f..45a30eb6807 100644 --- a/tests/Fixtures/TestBundle/Entity/Book.php +++ b/tests/Fixtures/TestBundle/Entity/Book.php @@ -22,7 +22,7 @@ * * @author Antoine Bluchet */ -#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}{._format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] +#[ApiResource(operations: [new Get(), new Get(uriTemplate: '/books/by_isbn/{isbn}.{_format}', requirements: ['isbn' => '.+'], uriVariables: 'isbn')])] #[ORM\Entity] class Book { diff --git a/tests/Fixtures/TestBundle/Entity/Dummy.php b/tests/Fixtures/TestBundle/Entity/Dummy.php index 69e9384db63..33fc651dbd7 100644 --- a/tests/Fixtures/TestBundle/Entity/Dummy.php +++ b/tests/Fixtures/TestBundle/Entity/Dummy.php @@ -28,8 +28,8 @@ * @author Kévin Dunglas */ #[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] class Dummy { diff --git a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php index 7089cdf2a70..33d5def4a21 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php @@ -28,8 +28,8 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyAggregateOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyOffer.php b/tests/Fixtures/TestBundle/Entity/DummyOffer.php index 5e9d52dad81..8448b8bf21c 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyOffer.php +++ b/tests/Fixtures/TestBundle/Entity/DummyOffer.php @@ -26,9 +26,9 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers.{_format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyOffer { diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php index f2a5475d46a..6901e719705 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php +++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php @@ -28,7 +28,7 @@ * @author Antoine Bluchet */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class DummyProduct { diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php deleted file mode 100644 index cabc7a4352c..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeProduct.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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\Entity; - -use ApiPlatform\Core\Annotation\ApiResource; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; - -/** - * @ORM\Entity - * - * @ApiResource - */ -class DummyToUpgradeProduct -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @var Collection - * - * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAnnotation::class) - */ - private $dummysToUpgradeWithOnlyAnnotation; - - /** - * @var Collection - * - * @ORM\OneToMany(mappedBy="dummyToUpgradeProduct", targetEntity=DummyToUpgradeWithOnlyAttribute::class) - */ - private $dummysToUpgradeWithOnlyAttribute; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php deleted file mode 100644 index 576ca2a55f0..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAnnotation.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * 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\Entity; - -use ApiPlatform\Core\Annotation\ApiFilter; -use ApiPlatform\Core\Annotation\ApiProperty; -use ApiPlatform\Core\Annotation\ApiResource; -use ApiPlatform\Core\Annotation\ApiSubresource; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\DateFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ExistsFilter; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * @ORM\Entity - * - * @ApiResource - * - * @ApiFilter(SearchFilter::class, properties={"id"}) - */ -class DummyToUpgradeWithOnlyAnnotation -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - * @Groups({"chicago", "friends"}) - * @ApiProperty(writable=false) - * @ApiFilter(DateFilter::class) - */ - private $id; - - /** - * @var DummyToUpgradeProduct - * - * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", cascade={"persist"}, inversedBy="dummysToUpgradeWithOnlyAnnotation") - * @ORM\JoinColumn(nullable=false) - * @Groups({"barcelona", "chicago", "friends"}) - * - * @ApiSubresource - * - * @ApiProperty(iri="DummyToUpgradeWithOnlyAnnotation.dummyToUpgradeProduct") - * @ApiFilter(SearchFilter::class) - * @ApiFilter(ExistsFilter::class) - */ - private $dummyToUpgradeProduct; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php b/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php deleted file mode 100644 index f9a1fd1f592..00000000000 --- a/tests/Fixtures/TestBundle/Entity/DummyToUpgradeWithOnlyAttribute.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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\Entity; - -use ApiPlatform\Core\Annotation\ApiProperty; -use ApiPlatform\Core\Annotation\ApiResource; -use ApiPlatform\Core\Annotation\ApiSubresource; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * @ORM\Entity - */ -#[ApiResource()] -class DummyToUpgradeWithOnlyAttribute -{ - /** - * @var int - * - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - #[Groups(['chicago', 'friends'])] - #[ApiProperty(writable: false)] - private $id; - - /** - * @var DummyToUpgradeProduct - * - * @ORM\ManyToOne(targetEntity="DummyToUpgradeProduct", inversedBy="dummysToUpgradeWithOnlyAttribute") - * @ORM\JoinColumn(nullable=false) - */ - #[Groups(['barcelona', 'chicago', 'friends'])] - #[ApiSubresource] - #[ApiProperty(iri: 'DummyToUpgradeWithOnlyAttribute.dummyToUpgradeProduct')] - private $dummyToUpgradeProduct; -} diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidation.php b/tests/Fixtures/TestBundle/Entity/DummyValidation.php index 8500aa1556b..e06a4f02d29 100644 --- a/tests/Fixtures/TestBundle/Entity/DummyValidation.php +++ b/tests/Fixtures/TestBundle/Entity/DummyValidation.php @@ -21,7 +21,7 @@ #[ApiResource(operations: [ new GetCollection(), - new Post(uriTemplate: 'dummy_validation{._format}'), + new Post(uriTemplate: 'dummy_validation.{_format}'), new Post(routeName: 'post_validation_groups', validationContext: ['groups' => ['a']]), new Post(routeName: 'post_validation_sequence', validationContext: ['groups' => 'app.dummy_validation.group_generator']), ] diff --git a/tests/Fixtures/TestBundle/Entity/FooDummy.php b/tests/Fixtures/TestBundle/Entity/FooDummy.php index 92596dad682..b744c0c2a55 100644 --- a/tests/Fixtures/TestBundle/Entity/FooDummy.php +++ b/tests/Fixtures/TestBundle/Entity/FooDummy.php @@ -15,8 +15,6 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\QueryCollection; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -46,17 +44,6 @@ class FooDummy #[ORM\ManyToOne(targetEntity: Dummy::class, cascade: ['persist'])] private ?Dummy $dummy = null; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: SoMany::class, mappedBy: 'fooDummy', cascade: ['persist'])] - public Collection $soManies; - - public function __construct() - { - $this->soManies = new ArrayCollection(); - } - public function getId(): ?int { return $this->id; diff --git a/tests/Fixtures/TestBundle/Entity/FourthLevel.php b/tests/Fixtures/TestBundle/Entity/FourthLevel.php index b62eaed1b27..85161c78705 100644 --- a/tests/Fixtures/TestBundle/Entity/FourthLevel.php +++ b/tests/Fixtures/TestBundle/Entity/FourthLevel.php @@ -26,12 +26,12 @@ * @author Alan Poulain */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level.{_format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class FourthLevel { diff --git a/tests/Fixtures/TestBundle/Entity/Greeting.php b/tests/Fixtures/TestBundle/Entity/Greeting.php index d74e8217b55..ee0bcd5602d 100644 --- a/tests/Fixtures/TestBundle/Entity/Greeting.php +++ b/tests/Fixtures/TestBundle/Entity/Greeting.php @@ -19,7 +19,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/people/{id}/sent_greetings.{_format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Greeting { diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php index 46530b96c5e..30070090fda 100644 --- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php +++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php @@ -20,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)] -#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])] #[ORM\Entity] class NetworkPathDummy { diff --git a/tests/Fixtures/TestBundle/Entity/Question.php b/tests/Fixtures/TestBundle/Entity/Question.php index 78b1afd5026..0ffc8bfd2cb 100644 --- a/tests/Fixtures/TestBundle/Entity/Question.php +++ b/tests/Fixtures/TestBundle/Entity/Question.php @@ -19,8 +19,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/answers/{id}/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/answers/{id}/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: Answer::class, identifiers: ['id'], toProperty: 'answer')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/questions/{id}/answer/related_questions.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'], fromProperty: 'answer'), 'answer' => new Link(fromClass: Answer::class, identifiers: [], expandedValue: 'answer', toProperty: 'answer')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class Question { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index a6e60f79169..bbb36c24aca 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -35,22 +35,14 @@ * * @author Kévin Dunglas */ -#[ApiResource( - graphQlOperations: [ - new Query(name: 'item_query'), - new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), - ], - types: ['https://schema.org/Product'], - normalizationContext: ['groups' => ['friends']], - filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] -)] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']])], types: ['https://schema.org/Product'], normalizationContext: ['groups' => ['friends']], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id.{_format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] #[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ORM\Entity] class RelatedDummy extends ParentDummy implements \Stringable diff --git a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php index c584c30596b..b7823a78d86 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedToDummyFriend.php @@ -25,11 +25,11 @@ * Related To Dummy Friend represent an association table for a manytomany relation. */ #[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ORM\Entity] class RelatedToDummyFriend { diff --git a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php index c1d8c5b21e4..bab3c01e521 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugChildDummy.php @@ -20,8 +20,8 @@ use Doctrine\ORM\Mapping as ORM; #[ApiResource] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugParentDummy::class, identifiers: ['slug'], toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy/child_dummies.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], fromProperty: 'parentDummy'), 'parentDummy' => new Link(fromClass: SlugParentDummy::class, identifiers: [], expandedValue: 'parent_dummy', toProperty: 'parentDummy')], status: 200, operations: [new GetCollection()])] #[ORM\Entity] class SlugChildDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php index 104ca2cc97e..c6412d78229 100644 --- a/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SlugParentDummy.php @@ -25,8 +25,8 @@ * Custom Identifier Dummy With Subresource. */ #[ApiResource(uriVariables: 'slug')] -#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy{._format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_parent_dummies/{slug}/child_dummies/{childDummies}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: self::class, identifiers: ['slug'], toProperty: 'parentDummy'), 'childDummies' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/slug_child_dummies/{slug}/parent_dummy.{_format}', uriVariables: ['slug' => new Link(fromClass: SlugChildDummy::class, identifiers: ['slug'], fromProperty: 'parentDummy')], status: 200, operations: [new Get()])] #[ORM\Entity] class SlugParentDummy { diff --git a/tests/Fixtures/TestBundle/Entity/SoMany.php b/tests/Fixtures/TestBundle/Entity/SoMany.php index e3770b8007d..65a2ad93790 100644 --- a/tests/Fixtures/TestBundle/Entity/SoMany.php +++ b/tests/Fixtures/TestBundle/Entity/SoMany.php @@ -31,7 +31,4 @@ class SoMany public $id; #[ORM\Column(nullable: true)] public $content; - - #[ORM\ManyToOne] - public ?FooDummy $fooDummy; } diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 1ca2e4c6f43..1d18d362d73 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -25,11 +25,11 @@ * @author Kévin Dunglas */ #[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level.{_format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] #[ORM\Entity] class ThirdLevel { diff --git a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php index 7eebd395415..fc3eb0561f8 100644 --- a/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php +++ b/tests/Fixtures/TestBundle/Entity/VoDummyInspection.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; use ApiPlatform\Metadata\ApiResource; +use DateTime; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -27,12 +28,9 @@ class VoDummyInspection #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private \DateTime $performed; - private $attributeWithoutConstructorEquivalent; - - public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, \DateTime $performed = null, string $parameterWhichIsNotClassAttribute = '') + public function __construct(#[ORM\Column(type: 'boolean')] #[Groups(['car_read', 'car_write', 'inspection_read', 'inspection_write'])] private bool $accepted, #[ORM\ManyToOne(targetEntity: VoDummyCar::class, inversedBy: 'inspections')] #[Groups(['inspection_read', 'inspection_write'])] private ?VoDummyCar $car, DateTime $performed = null, private string $attributeWithoutConstructorEquivalent = '') { - $this->performed = $performed ?: new \DateTime(); - $this->attributeWithoutConstructorEquivalent = $parameterWhichIsNotClassAttribute; + $this->performed = $performed ?: new DateTime(); } public function isAccepted(): bool @@ -45,12 +43,12 @@ public function getCar(): ?VoDummyCar return $this->car; } - public function getPerformed(): \DateTime + public function getPerformed(): DateTime { return $this->performed; } - public function setPerformed(\DateTime $performed) + public function setPerformed(DateTime $performed) { $this->performed = $performed; diff --git a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php deleted file mode 100644 index 65c4b65c51f..00000000000 --- a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * 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\GraphQl\Metadata\Factory; - -use ApiPlatform\GraphQl\Metadata\Factory\GraphQlNestedOperationResourceMetadataFactory; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -final class GraphQlNestedOperationResourceMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testCreate(): void - { - $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $decorated->create('someClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('someClass')); - - $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500], $decorated->reveal()); - $apiResource = $metadataFactory->create('someClass')[0]; - $this->assertCount(5, $apiResource->getGraphQlOperations()); - } - - public function testCreateWithResource(): void - { - $metadataFactory = new GraphQlNestedOperationResourceMetadataFactory(['status' => 500]); - $apiResource = $metadataFactory->create(RelatedDummy::class)[0]; - $this->assertNotEmpty($apiResource->getFilters()); - $this->assertEquals('RelatedDummy', $apiResource->getShortName()); - } -} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index c5b59ad50b7..c7bb3ba897a 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -56,18 +56,29 @@ class FieldsBuilderTest extends TestCase use ProphecyTrait; private ObjectProphecy $propertyNameCollectionFactoryProphecy; + private ObjectProphecy $propertyMetadataFactoryProphecy; + private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $graphQlNestedOperationResourceMetadataFactoryProphecy; + private ObjectProphecy $typesContainerProphecy; + private ObjectProphecy $typeBuilderProphecy; + private ObjectProphecy $typeConverterProphecy; + private ObjectProphecy $itemResolverFactoryProphecy; + private ObjectProphecy $collectionResolverFactoryProphecy; + private ObjectProphecy $itemMutationResolverFactoryProphecy; + private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; + private ObjectProphecy $filterLocatorProphecy; + private ObjectProphecy $resourceClassResolverProphecy; + private FieldsBuilder $fieldsBuilder; /** @@ -87,13 +98,12 @@ protected function setUp(): void $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $this->graphQlNestedOperationResourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); } private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__', $this->graphQlNestedOperationResourceMetadataFactoryProphecy->reveal()); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -129,6 +139,7 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -139,8 +150,8 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'no resource field configuration' => ['resourceClass', (new Query())->withName('action'), [], null, null, []], + 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -153,7 +164,7 @@ public function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { + 'nominal item case' => ['resourceClass', (new Query())->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item']), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -168,7 +179,7 @@ public function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -181,7 +192,7 @@ public function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + 'resourceClass', (new Query())->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -209,6 +220,7 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation)->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); @@ -232,8 +244,8 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withName('action'), [], null, null, []], + 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -262,7 +274,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with filters' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -296,7 +308,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withArgs([])->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -310,7 +322,7 @@ public function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -326,7 +338,7 @@ public function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { + 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection'])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -359,6 +371,8 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); + $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -369,7 +383,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -389,7 +403,7 @@ public function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { + 'custom description' => ['resourceClass', (new Mutation())->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation']), $inputGraphqlType = new ObjectType(['name' => 'input']), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -421,6 +435,7 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeBuilderProphecy->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $operation->getName())->willReturn($graphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); @@ -432,9 +447,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], + 'mercure not enabled' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription']), new ObjectType(['name' => 'input']), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -454,7 +469,7 @@ public function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { + 'custom description' => ['resourceClass', (new Subscription())->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription']), $inputGraphqlType = new ObjectType(['name' => 'input']), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -483,8 +498,6 @@ public function subscriptionFieldsProvider(): array public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?AdvancedNameConverterInterface $advancedNameConverter = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceClass')->willReturn(true); - $this->resourceClassResolverProphecy->isResourceClass('nestedResourceNoQueryClass')->willReturn(true); $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(false); $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { @@ -492,32 +505,20 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); - if ('propertyObject' === $propertyName) { $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); + $this->resourceMetadataCollectionFactoryProphecy->create('objectClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { }); } - if ('propertyNestedResource' === $propertyName) { - $nestedResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { - }); - } - if ('propertyNestedResourceNoQuery' === $propertyName) { - $nestedResourceQueryOperation = new Query(); - $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceNoQueryClass')->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withDescription('A description.')->withGraphQlOperations([])])); - $this->graphQlNestedOperationResourceMetadataFactoryProphecy->create('nestedResourceNoQueryClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('nestedResourceNoQueryClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceNoQueryClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType'])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceNoQueryClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { - }); - } + $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'anotherResourceClass', $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->resourceMetadataCollectionFactoryProphecy->create('resourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); + $this->resourceMetadataCollectionFactoryProphecy->create('anotherResourceClass')->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations(['item_query' => new Query()])])); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverter) { @@ -534,7 +535,7 @@ public function resourceObjectTypeFieldsProvider(): array $advancedNameConverter->normalize('field', 'resourceClass')->willReturn('normalizedField'); return [ - 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), @@ -562,7 +563,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with advanced name converter' => ['resourceClass', new Query(), [ 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), ], @@ -581,7 +582,7 @@ public function resourceObjectTypeFieldsProvider(): array ], $advancedNameConverter->reveal(), ], - 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query input' => ['resourceClass', new Query(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), @@ -600,7 +601,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'query with simple non-null string array property' => ['resourceClass', new Query(), [ 'property' => (new ApiProperty())->withBuiltinTypes([ new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), @@ -620,40 +621,12 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), - [ - 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), - 'propertyNestedResourceNoQuery' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceNoQueryClass')])->withReadable(true)->withWritable(true), - ], - false, 0, null, - [ - 'id' => [ - 'type' => GraphQLType::nonNull(GraphQLType::id()), - ], - 'propertyNestedResource' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - 'propertyNestedResourceNoQuery' => [ - 'type' => GraphQLType::nonNull(new ObjectType(['name' => 'objectType'])), - 'description' => null, - 'args' => [], - 'resolve' => static function (): void { - }, - 'deprecationReason' => null, - ], - ], - ], - 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation non input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL, false, 'objectClass')])->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -677,7 +650,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -713,7 +686,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + 'mutation nested input' => ['resourceClass', (new Mutation())->withName('mutation'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -732,7 +705,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), + 'delete mutation input' => ['resourceClass', (new Mutation())->withName('delete'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -744,7 +717,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), + 'create mutation input' => ['resourceClass', (new Mutation())->withName('create'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -760,7 +733,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), + 'update mutation input' => ['resourceClass', (new Mutation())->withName('update'), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -779,7 +752,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription non input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), @@ -799,7 +772,7 @@ public function resourceObjectTypeFieldsProvider(): array ], ], ], - 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + 'subscription input' => ['resourceClass', new Subscription(), [ 'property' => new ApiProperty(), 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), @@ -814,13 +787,13 @@ public function resourceObjectTypeFieldsProvider(): array 'clientSubscriptionId' => GraphQLType::string(), ], ], - 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata non input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ], - 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'null io metadata input' => ['resourceClass', new Query(), [ 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), ], @@ -829,7 +802,7 @@ public function resourceObjectTypeFieldsProvider(): array 'clientMutationId' => GraphQLType::string(), ], ], - 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), + 'invalid types' => ['resourceClass', new Query(), [ 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 77ba4f7e842..66b0e7cd855 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -483,7 +483,7 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); @@ -538,7 +538,7 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); /** @var ObjectType $resourcePaginatedCollectionType */ - $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'test', $operation); + $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', $operation); $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); diff --git a/tests/HttpCache/VarnishPurgerTest.php b/tests/HttpCache/VarnishPurgerTest.php index faca342dc91..ffc2a5b53f4 100644 --- a/tests/HttpCache/VarnishPurgerTest.php +++ b/tests/HttpCache/VarnishPurgerTest.php @@ -17,6 +17,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; +use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -78,12 +79,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -95,12 +96,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } }; diff --git a/tests/HttpCache/VarnishXKeyPurgerTest.php b/tests/HttpCache/VarnishXKeyPurgerTest.php index 96e380752b9..33c64da8678 100644 --- a/tests/HttpCache/VarnishXKeyPurgerTest.php +++ b/tests/HttpCache/VarnishXKeyPurgerTest.php @@ -17,6 +17,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; +use LogicException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; @@ -108,12 +109,12 @@ public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLengt public function send(RequestInterface $request, array $options = []): ResponseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function request($method, $uri, array $options = []): ResponseInterface @@ -125,12 +126,12 @@ public function request($method, $uri, array $options = []): ResponseInterface public function requestAsync($method, $uri, array $options = []): PromiseInterface { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } public function getConfig($option = null): void { - throw new \LogicException('Not implemented'); + throw new LogicException('Not implemented'); } }; diff --git a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php index ae69822be21..fb6040a5de1 100644 --- a/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php +++ b/tests/JsonSchema/Command/JsonSchemaGenerateCommandTest.php @@ -49,21 +49,21 @@ public function testExecuteWithoutOption(): void public function testExecuteWithItemOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}{._format}_get', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies/{id}.{_format}_get', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithCollectionOperationGet(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_get_collection', '--type' => 'output']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_get_collection', '--type' => 'output']); $this->assertJson($this->tester->getDisplay()); } public function testExecuteWithJsonldFormatOption(): void { - $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies{._format}_post', '--format' => 'jsonld']); + $this->tester->run(['command' => 'api:json-schema:generate', 'resource' => $this->entityClass, '--operation' => '_api_/dummies.{_format}_post', '--format' => 'jsonld']); $result = $this->tester->getDisplay(); $this->assertStringContainsString('@id', $result); diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 4c34da9000f..04279076970 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\PropertyAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlPropertyAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlPropertyAdapter; +use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Type; @@ -86,7 +87,7 @@ public function testValidMetadata(string $extractorClass, PropertyAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, self::PROPERTY, $parameters, self::FIXTURES)); $factory = new ExtractorPropertyMetadataFactory($extractor); $property = $factory->create(self::RESOURCE_CLASS, self::PROPERTY); - } catch (\Exception $exception) { + } catch (Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiProperty::class, 0, $exception); } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index e6ce7a3727d..20342ad36e6 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -20,13 +20,10 @@ use ApiPlatform\Metadata\Extractor\YamlResourceExtractor; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\DeleteMutation; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; @@ -37,6 +34,7 @@ use ApiPlatform\Tests\Metadata\Extractor\Adapter\ResourceAdapterInterface; use ApiPlatform\Tests\Metadata\Extractor\Adapter\XmlResourceAdapter; use ApiPlatform\Tests\Metadata\Extractor\Adapter\YamlResourceAdapter; +use Exception; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -226,7 +224,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase [ 'name' => 'custom_operation_name', 'method' => 'GET', - 'uriTemplate' => '/users/{userId}/comments{._format}', + 'uriTemplate' => '/users/{userId}/comments.{_format}', 'shortName' => self::SHORT_NAME, 'description' => 'A list of Comments', 'types' => ['Comment'], @@ -332,7 +330,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], ], [ - 'uriTemplate' => '/users/{userId}/comments/{commentId}{._format}', + 'uriTemplate' => '/users/{userId}/comments/{commentId}.{_format}', 'class' => Get::class, 'uriVariables' => [ 'userId' => [ @@ -422,7 +420,7 @@ public function testValidMetadata(string $extractorClass, ResourceAdapterInterfa $extractor = new $extractorClass($adapter(self::RESOURCE_CLASS, $parameters, self::FIXTURES)); $factory = new ExtractorResourceMetadataCollectionFactory($extractor); $collection = $factory->create(self::RESOURCE_CLASS); - } catch (\Exception $exception) { + } catch (Exception $exception) { throw new AssertionFailedError('Failed asserting that the schema is valid according to '.ApiResource::class, 0, $exception); } @@ -455,16 +453,7 @@ private function buildApiResources(): array $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); } - $resource = $resource->withOperations(new Operations($operations)); - - // Build default GraphQL operations - $graphQlOperations = []; - foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $graphQlOperation) { - $description = $graphQlOperation instanceof Mutation ? ucfirst("{$graphQlOperation->getName()}s a {$resource->getShortName()}.") : null; - $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withName($graphQlOperation->getName())->withDescription($description); - } - - $resources[] = $resource->withGraphQlOperations($graphQlOperations); + $resources[] = $resource->withOperations(new Operations($operations)); continue; } @@ -602,7 +591,7 @@ private function withGraphQlOperations(array $values, ?array $fixtures): array return $operations; } - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation + private function getOperationWithDefaults(ApiResource $resource, HttpOperation $operation): HttpOperation { foreach (get_class_methods($resource) as $methodName) { if (!str_starts_with($methodName, 'get')) { diff --git a/tests/Metadata/Extractor/XmlExtractorTest.php b/tests/Metadata/Extractor/XmlExtractorTest.php index b229f52f1b6..dc1f9b45e8f 100644 --- a/tests/Metadata/Extractor/XmlExtractorTest.php +++ b/tests/Metadata/Extractor/XmlExtractorTest.php @@ -99,7 +99,7 @@ public function testValidXML(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/comments{._format}', + 'uriTemplate' => '/users/{author}/comments.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -177,7 +177,7 @@ public function testValidXML(): void [ 'name' => 'custom_operation_name', 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/comments{._format}', + 'uriTemplate' => '/users/{author}/comments.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, @@ -268,7 +268,7 @@ public function testValidXML(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/comments/{id}{._format}', + 'uriTemplate' => '/users/{userId}/comments/{id}.{_format}', 'shortName' => null, 'description' => 'User comments', 'routePrefix' => null, diff --git a/tests/Metadata/Extractor/YamlExtractorTest.php b/tests/Metadata/Extractor/YamlExtractorTest.php index cede4108ebc..843df41f723 100644 --- a/tests/Metadata/Extractor/YamlExtractorTest.php +++ b/tests/Metadata/Extractor/YamlExtractorTest.php @@ -166,7 +166,7 @@ public function testValidYaml(): void 'write' => null, ], [ - 'uriTemplate' => '/users/{author}/programs{._format}', + 'uriTemplate' => '/users/{author}/programs.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -230,7 +230,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => GetCollection::class, - 'uriTemplate' => '/users/{author}/programs{._format}', + 'uriTemplate' => '/users/{author}/programs.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -305,7 +305,7 @@ public function testValidYaml(): void [ 'name' => null, 'class' => Get::class, - 'uriTemplate' => '/users/{userId}/programs/{id}{._format}', + 'uriTemplate' => '/users/{userId}/programs/{id}.{_format}', 'shortName' => null, 'description' => 'User programs', 'routePrefix' => null, @@ -471,7 +471,7 @@ public function testInvalidYaml(string $path, string $error): void public function getInvalidPaths(): array { return [ - [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml".'], + [__DIR__.'/yaml/invalid/invalid_resources.yaml', '"resources" setting is expected to be null or an array, string given in "'.__DIR__.'/yaml/invalid/invalid_resources.yaml'.'".'], ]; } } diff --git a/tests/Metadata/Extractor/xml/valid.xml b/tests/Metadata/Extractor/xml/valid.xml index cfcccc202d3..3269a7ba104 100644 --- a/tests/Metadata/Extractor/xml/valid.xml +++ b/tests/Metadata/Extractor/xml/valid.xml @@ -7,7 +7,7 @@ @@ -96,7 +96,7 @@ - + diff --git a/tests/Metadata/Extractor/yaml/valid.yaml b/tests/Metadata/Extractor/yaml/valid.yaml index c3a80a5acac..d001ba20899 100644 --- a/tests/Metadata/Extractor/yaml/valid.yaml +++ b/tests/Metadata/Extractor/yaml/valid.yaml @@ -3,7 +3,7 @@ resources: ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program: - ~ - - uriTemplate: /users/{author}/programs{._format} + - uriTemplate: /users/{author}/programs.{_format} uriVariables: ['author'] types: ['someirischema'] description: User programs @@ -13,7 +13,7 @@ resources: operations: ApiPlatform\Metadata\GetCollection: ~ ApiPlatform\Metadata\Get: - uriTemplate: /users/{userId}/programs/{id}{._format} + uriTemplate: /users/{userId}/programs/{id}.{_format} types: ['anotheririschema'] uriVariables: userId: [ApiPlatform\Tests\Fixtures\TestBundle\Entity\User, 'author'] diff --git a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php index 6510080c88d..0420c8d3be8 100644 --- a/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactoryTest.php @@ -28,7 +28,6 @@ use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeDefaultOperations; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeOnlyOperation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResource; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AttributeResources; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExtraPropertiesResource; @@ -82,11 +81,11 @@ class: AttributeResource::class, new ApiResource( shortName: 'AttributeResource', class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', operations: [ - '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_get' => new Get( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_get' => new Get( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 4, @@ -95,9 +94,9 @@ class: AttributeResource::class, // @noRector \Rector\Php81\Rector\Array_\FirstClassCallableRector processor: [AttributeResourceProcessor::class, 'process'] ), - '_api_/dummy/{dummyId}/attribute_resources/{identifier}{._format}_patch' => new Patch( + '_api_/dummy/{dummyId}/attribute_resources/{identifier}.{_format}_patch' => new Patch( class: AttributeResource::class, - uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}{._format}', + uriTemplate: '/dummy/{dummyId}/attribute_resources/{identifier}.{_format}', shortName: 'AttributeResource', inputFormats: ['json' => ['application/merge-patch+json']], priority: 5, @@ -120,17 +119,17 @@ class: AttributeResource::class, $this->assertEquals( new ResourceMetadataCollection(AttributeResources::class, [ new ApiResource( - uriTemplate: '/attribute_resources{._format}', + uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResources', normalizationContext: ['skip_null_values' => true], class: AttributeResources::class, provider: AttributeResourceProvider::class, operations: [ - '_api_/attribute_resources{._format}_get_collection' => new GetCollection( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, + '_api_/attribute_resources.{_format}_get_collection' => new GetCollection( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 1, provider: AttributeResourceProvider::class, ), - '_api_/attribute_resources{._format}_post' => new Post( - shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources{._format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, + '_api_/attribute_resources.{_format}_post' => new Post( + shortName: 'AttributeResources', class: AttributeResources::class, uriTemplate: '/attribute_resources.{_format}', normalizationContext: ['skip_null_values' => true], priority: 2, provider: AttributeResourceProvider::class, ), ], graphQlOperations: $this->getDefaultGraphqlOperations('AttributeResources', AttributeResources::class, AttributeResourceProvider::class) @@ -210,20 +209,4 @@ public function testExtraProperties(): void $this->assertEquals($extraPropertiesResource[0]->getExtraProperties(), ['foo' => 'bar']); $this->assertEquals($extraPropertiesResource->getOperation('_api_ExtraPropertiesResource_get')->getExtraProperties(), ['foo' => 'bar']); } - - public function testOverrideNameWithoutOperations(): void - { - $attributeResourceMetadataCollectionFactory = new AttributesResourceMetadataCollectionFactory(); - - $operation = new HttpOperation(shortName: 'AttributeOnlyOperation', class: AttributeOnlyOperation::class); - $this->assertEquals(new ResourceMetadataCollection(AttributeOnlyOperation::class, [ - new ApiResource( - shortName: 'AttributeOnlyOperation', - class: AttributeOnlyOperation::class, - operations: [ - 'my own name' => (new Get(name: 'my own name', priority: 1))->withOperation($operation), - ] - ), - ]), $attributeResourceMetadataCollectionFactory->create(AttributeOnlyOperation::class)); - } } diff --git a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php index 7603509b3a9..d60638d3f71 100644 --- a/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php +++ b/tests/Metadata/Resource/Factory/UriTemplateResourceMetadataCollectionFactoryTest.php @@ -115,10 +115,10 @@ class: AttributeResource::class, shortName: 'AttributeResource', class: AttributeResource::class, operations: [ - '_api_/attribute_resources/{id}{._format}_get' => new Get(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_get'), - '_api_/attribute_resources/{id}{._format}_put' => new Put(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_put'), - '_api_/attribute_resources/{id}{._format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_delete'), - '_api_/attribute_resources{._format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources{._format}_get_collection'), + '_api_/attribute_resources/{id}.{_format}_get' => new Get(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_get'), + '_api_/attribute_resources/{id}.{_format}_put' => new Put(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_put'), + '_api_/attribute_resources/{id}.{_format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}.{_format}_delete'), + '_api_/attribute_resources.{_format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources.{_format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources.{_format}_get_collection'), ] ), new ApiResource( diff --git a/tests/State/Pagination/TraversablePaginatorTest.php b/tests/State/Pagination/TraversablePaginatorTest.php index e9138e7571d..ba51143119d 100644 --- a/tests/State/Pagination/TraversablePaginatorTest.php +++ b/tests/State/Pagination/TraversablePaginatorTest.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Tests\State\Pagination; use ApiPlatform\State\Pagination\TraversablePaginator; +use ArrayIterator; use PHPUnit\Framework\TestCase; class TraversablePaginatorTest extends TestCase @@ -29,7 +30,7 @@ public function testInitialize( float $lastPage, int $currentItems ): void { - $traversable = new \ArrayIterator($results); + $traversable = new ArrayIterator($results); $paginator = new TraversablePaginator($traversable, $currentPage, $perPage, $totalItems); diff --git a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php index a574d5b9dc2..ad0f265a015 100644 --- a/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php +++ b/tests/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php @@ -205,7 +205,7 @@ public function provideIntegrationCases(): iterable yield 'simple' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations{._format}_put', + '_api_operation_name' => '_api_/resource_implementations.{_format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload): void {}, @@ -215,7 +215,7 @@ static function (ResourceImplementation $payload): void {}, yield 'with another argument named $data' => [ $this->createRequest('PUT', [ '_api_resource_class' => ResourceImplementation::class, - '_api_operation_name' => '_api_/resource_implementations{._format}_put', + '_api_operation_name' => '_api_/resource_implementations.{_format}_put', 'data' => $resource, ]), static function (ResourceImplementation $payload, $data): void {}, @@ -230,7 +230,7 @@ private function createArgumentResolver(): PayloadArgumentResolver (new ApiResource())->withShortName('ResourceImplementation')->withOperations(new Operations([ 'update' => new Put(), 'update_no_deserialize' => (new Put())->withDeserialize(false), - 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class, 'name' => 'NotResource']), + 'update_with_dto' => (new Put())->withInput(['class' => NotResource::class]), 'create' => new Post(), ])), ])); @@ -251,7 +251,7 @@ private function createArgumentResolver(): PayloadArgumentResolver ]; if ('update_with_dto' === $request->attributes->get('_api_operation_name')) { - $context['input'] = ['class' => NotResource::class, 'name' => 'NotResource']; + $context['input'] = NotResource::class; } else { $context['input'] = null; } diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 5be3cc47aed..cf89456b6af 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -70,9 +70,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Uid\AbstractUid; @@ -165,7 +163,6 @@ protected function setUp(): void 'kernel.bundles' => [ 'DoctrineBundle' => DoctrineBundle::class, 'SecurityBundle' => SecurityBundle::class, - 'TwigBundle' => TwigBundle::class, ], 'kernel.bundles_metadata' => [ 'TestBundle' => [ @@ -176,7 +173,6 @@ protected function setUp(): void ], 'kernel.project_dir' => __DIR__.'/../../../Fixtures/app', 'kernel.debug' => false, - 'kernel.environment' => 'test', ]); $this->container = new ContainerBuilder($containerParameterBag); @@ -671,8 +667,6 @@ public function testGraphQlConfiguration(): void 'api_platform.graphql.normalizer.validation_exception', 'api_platform.graphql.normalizer.http_exception', 'api_platform.graphql.normalizer.runtime_exception', - 'api_platform.graphql_metadata.resource.metadata_collection_factory', - 'api_platform.graphql_metadata.resource.metadata_collection_factory.filters', ]; $aliases = [ @@ -699,37 +693,6 @@ public function testGraphQlConfiguration(): void $this->assertServiceHasTags('api_platform.graphql.normalizer.runtime_exception', ['serializer.normalizer']); } - public function testRuntimeExceptionIsThrownIfTwigIsNotEnabledButGraphqlClientsAre(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('GraphiQL and GraphQL Playground interfaces depend on Twig. Please activate TwigBundle for the test environnement or disable GraphiQL and GraphQL Playground.'); - - (new ApiPlatformExtension())->load($config, $this->container); - } - - public function testGraphqlClientsDefinitionsAreRemovedIfDisabled(): void - { - $config = self::DEFAULT_CONFIG; - $config['api_platform']['graphql']['enabled'] = true; - $config['api_platform']['graphql']['graphiql']['enabled'] = false; - $config['api_platform']['graphql']['graphql_playground']['enabled'] = false; - $this->container->getParameterBag()->set('kernel.bundles', [ - 'DoctrineBundle' => DoctrineBundle::class, - 'SecurityBundle' => SecurityBundle::class, - ]); - - (new ApiPlatformExtension())->load($config, $this->container); - - $this->assertNotContainerHasService('api_platform.graphql.action.graphiql'); - $this->assertNotContainerHasService('api_platform.graphql.action.graphql_playground'); - } - public function testDoctrineOrmConfiguration(): void { $config = self::DEFAULT_CONFIG;