diff --git a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php b/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php deleted file mode 100644 index cb032637ab1..00000000000 --- a/src/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactory.php +++ /dev/null @@ -1,58 +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 - ); - - $resourceMetadataCollection[0] = $this->addDefaultGraphQlOperations($apiResource); - - return $resourceMetadataCollection; - } -} diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index ee5f673d1a5..26a9c7d42c3 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -14,7 +14,6 @@ namespace ApiPlatform\GraphQl\Type; use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; use ApiPlatform\Metadata\GraphQl\Mutation; @@ -45,7 +44,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) { } @@ -68,6 +67,10 @@ public function getNodeQueryFields(): array */ public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array { + if ($operation instanceof Query && $operation->getNested()) { + return []; + } + $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) { @@ -85,6 +88,10 @@ public function getItemQueryFields(string $resourceClass, Operation $operation, */ public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array { + if ($operation instanceof Query && $operation->getNested()) { + return []; + } + $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) { @@ -257,17 +264,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $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'); - // Add filters from the metadata defined on the resource itself. - if ($filters = $resourceMetadataCollection[0]?->getFilters()) { - $resourceOperation = $resourceOperation->withFilters($filters); - } - } + $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); } if (!$resourceOperation instanceof Operation) { diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 720196f4a41..8be0411010d 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -50,7 +50,7 @@ public function getSchema(): Schema foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); foreach ($resourceMetadataCollection as $resourceMetadata) { - foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operationName => $operation) { + foreach ($resourceMetadata->getGraphQlOperations() ?? [] as $operation) { $configuration = null !== $operation->getArgs() ? ['args' => $operation->getArgs()] : []; if ($operation instanceof Query && $operation instanceof CollectionOperationInterface) { diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index acc2d9429ee..480b102bb2c 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -19,7 +19,6 @@ 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\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\Pagination; @@ -74,12 +73,9 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo } if ('item_query' === $operationName || 'collection_query' === $operationName) { - // Test if the collection/item operation exists and it has different groups - try { - if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) { - $shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item'; - } - } catch (OperationNotFoundException) { + // Test if the collection/item has different groups + if ($resourceMetadataCollection->getOperation($operation instanceof CollectionOperationInterface ? 'item_query' : 'collection_query')->getNormalizationContext() !== $operation->getNormalizationContext()) { + $shortName .= $operation instanceof CollectionOperationInterface ? 'Collection' : 'Item'; } } @@ -126,13 +122,7 @@ public function getResourceObjectType(?string $resourceClass, ResourceMetadataCo $wrappedOperationName = $operation instanceof Query ? $operationName : 'item_query'; } - try { - $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName); - } catch (OperationNotFoundException) { - $wrappedOperation = ('collection_query' === $wrappedOperationName ? new QueryCollection() : new Query()) - ->withResource($resourceMetadataCollection[0]) - ->withName($wrappedOperationName); - } + $wrappedOperation = $resourceMetadataCollection->getOperation($wrappedOperationName); $fields = [ lcfirst($wrappedOperation->getShortName()) => $this->getResourceObjectType($resourceClass, $resourceMetadataCollection, $wrappedOperation instanceof Operation ? $wrappedOperation : null, $input, true, $depth), diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index c76a70256b3..6ba6df9192e 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -16,10 +16,8 @@ use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Exception\ResourceClassNotFoundException; -use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use GraphQL\Error\SyntaxError; @@ -144,25 +142,20 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati } $operationName = $rootOperation->getName(); - $isCollection = $rootOperation instanceof CollectionOperationInterface || 'collection_query' === $operationName; + $isCollection = $this->typeBuilder->isCollection($type); - // We're retrieving the type of a property which is a relation to the rootResource - if ($resourceClass !== $rootResource && $property && $rootOperation instanceof Query) { - $isCollection = $this->typeBuilder->isCollection($type); + // We're retrieving the type of a property which is a relation to the root resource. + if ($resourceClass !== $rootResource && $rootOperation instanceof Query) { $operationName = $isCollection ? 'collection_query' : 'item_query'; } try { $operation = $resourceMetadataCollection->getOperation($operationName); - - if (!$operation instanceof Operation) { - throw new OperationNotFoundException(); - } } catch (OperationNotFoundException) { - /** @var Operation $operation */ - $operation = ($isCollection ? new QueryCollection() : new Query()) - ->withResource($resourceMetadataCollection[0]) - ->withName($operationName); + $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query'); + } + if (!$operation instanceof Operation) { + throw new OperationNotFoundException(); } return $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 27ec2d61e98..d229115fa6c 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -15,8 +15,10 @@ use ApiPlatform\Exception\InvalidArgumentException; 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\Post; use Symfony\Component\Config\Util\XmlUtils; @@ -332,9 +334,18 @@ private function buildGraphQlOperations(\SimpleXMLElement $resource, array $root } } + $collection = $this->phpize($operation, 'collection', 'bool', false); + if (Query::class === $class && $collection) { + $class = QueryCollection::class; + } + + $delete = $this->phpize($operation, 'delete', 'bool', false); + if (Mutation::class === $class && $delete) { + $class = DeleteMutation::class; + } + $data[] = array_merge($datum, [ 'graphql_operation_class' => $class, - 'collection' => $this->phpize($operation, 'collection', 'bool'), 'resolver' => $this->phpize($operation, 'resolver', 'string'), 'args' => $this->buildArgs($operation), 'class' => $this->phpize($operation, 'class', 'string'), diff --git a/src/Metadata/Extractor/schema/resources.xsd b/src/Metadata/Extractor/schema/resources.xsd index 02cf1182ed7..3aa198190a0 100644 --- a/src/Metadata/Extractor/schema/resources.xsd +++ b/src/Metadata/Extractor/schema/resources.xsd @@ -65,6 +65,8 @@ + + diff --git a/src/Metadata/GraphQl/Operation.php b/src/Metadata/GraphQl/Operation.php index 4e34b8be80e..652dcff810f 100644 --- a/src/Metadata/GraphQl/Operation.php +++ b/src/Metadata/GraphQl/Operation.php @@ -19,7 +19,6 @@ class Operation extends AbstractOperation { /** - * @param string $resolver * @param mixed|null $input * @param mixed|null $output * @param mixed|null $mercure diff --git a/src/Metadata/GraphQl/Query.php b/src/Metadata/GraphQl/Query.php index 036dd48a8f3..4f799c79677 100644 --- a/src/Metadata/GraphQl/Query.php +++ b/src/Metadata/GraphQl/Query.php @@ -68,9 +68,24 @@ public function __construct( ?string $name = null, $provider = null, $processor = null, - array $extraProperties = [] + array $extraProperties = [], + + protected ?bool $nested = null, ) { parent::__construct(...\func_get_args()); $this->name = $name ?: 'item_query'; } + + public function getNested(): ?bool + { + return $this->nested; + } + + public function withNested(?bool $nested = null): self + { + $self = clone $this; + $self->nested = $nested; + + return $self; + } } diff --git a/src/Metadata/GraphQl/QueryCollection.php b/src/Metadata/GraphQl/QueryCollection.php index 0e84eafaa83..054a87d60f0 100644 --- a/src/Metadata/GraphQl/QueryCollection.php +++ b/src/Metadata/GraphQl/QueryCollection.php @@ -70,9 +70,24 @@ public function __construct( ?string $name = null, $provider = null, $processor = null, - array $extraProperties = [] + array $extraProperties = [], + + protected ?bool $nested = null, ) { parent::__construct(...\func_get_args()); $this->name = $name ?: 'collection_query'; } + + public function getNested(): ?bool + { + return $this->nested; + } + + public function withNested(?bool $nested = null): self + { + $self = clone $this; + $self->nested = $nested; + + return $self; + } } diff --git a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php index 9b125aed5c3..dfd7babcb3c 100644 --- a/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/AttributesResourceMetadataCollectionFactory.php @@ -15,17 +15,10 @@ use ApiPlatform\Exception\ResourceClassNotFoundException; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Delete; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Patch; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\CreateProvider; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -63,7 +56,7 @@ public function create(string $resourceClass): ResourceMetadataCollection } if ($this->hasResourceAttributes($reflectionClass)) { - foreach ($this->buildResourceOperations($reflectionClass->getAttributes(), $resourceClass) as $i => $resource) { + foreach ($this->buildResourceOperations($reflectionClass->getAttributes(), $resourceClass) as $resource) { $resourceMetadataCollection[] = $resource; } } @@ -92,7 +85,16 @@ private function buildResourceOperations(array $attributes, string $resourceClas foreach ($attributes as $attribute) { if (ApiResource::class === $attribute->getName()) { - $resources[++$index] = $this->getResourceWithDefaults($resourceClass, $shortName, $attribute->newInstance()); + $resource = $this->getResourceWithDefaults($resourceClass, $shortName, $attribute->newInstance()); + $operations = []; + foreach ($resource->getOperations() ?? new Operations() as $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $operations[$key] = $operation; + } + if ($operations) { + $resource = $resource->withOperations(new Operations($operations)); + } + $resources[++$index] = $resource; continue; } @@ -122,30 +124,34 @@ private function buildResourceOperations(array $attributes, string $resourceClas // Loop again and set default operations if none where found foreach ($resources as $index => $resource) { - $operations = []; - - foreach ($resource->getOperations() ?? $this->getDefaultHttpOperations($resource) as $i => $operation) { - [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, $resource->getOperations() ? false : true); - $operations[$key] = $operation; + if (null === $resource->getOperations()) { + $operations = []; + foreach ($this->getDefaultHttpOperations($resource) as $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation, true); + $operations[$key] = $operation; + } + $resources[$index] = $resource->withOperations(new Operations($operations)); } - $resources[$index] = $resources[$index]->withOperations(new Operations($operations)); $graphQlOperations = $resource->getGraphQlOperations(); - if ([] === $graphQlOperations || !$this->graphQlEnabled) { + if (!$this->graphQlEnabled) { continue; } if (null === $graphQlOperations) { - // Add default graphql operations on the first resource + // Add default GraphQL operations on the first resource if (0 === $index) { $resources[$index] = $this->addDefaultGraphQlOperations($resources[$index]); } continue; } + $resources[$index] = $this->completeGraphQlOperations($resources[$index]); + $graphQlOperations = $resources[$index]->getGraphQlOperations(); + $graphQlOperationsWithDefaults = []; - foreach ($graphQlOperations as $i => $operation) { + foreach ($graphQlOperations as $operation) { [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); $graphQlOperationsWithDefaults[$key] = $operation; } @@ -185,14 +191,4 @@ private function hasSameOperation(ApiResource $resource, string $operationClass, return false; } - - private function getDefaultHttpOperations($resource): iterable - { - $post = new Post(); - if ($resource->getUriTemplate() && !$resource->getProvider()) { - $post = $post->withProvider(CreateProvider::class); - } - - return [new Get(), new GetCollection(), $post, new Put(), new Patch(), new Delete()]; - } } diff --git a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php index 86c154f7bc4..abc365f2ec2 100644 --- a/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/ExtractorResourceMetadataCollectionFactory.php @@ -14,22 +14,12 @@ namespace ApiPlatform\Metadata\Resource\Factory; use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\Delete; 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; -use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; /** @@ -39,10 +29,12 @@ */ final class ExtractorResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - private readonly CamelCaseToSnakeCaseNameConverter $camelCaseToSnakeCaseNameConverter; + use OperationDefaultsTrait; - public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, private readonly array $defaults = []) + public function __construct(private readonly ResourceExtractorInterface $extractor, private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, array $defaults = [], LoggerInterface $logger = null) { + $this->logger = $logger ?? new NullLogger(); + $this->defaults = $defaults; $this->camelCaseToSnakeCaseNameConverter = new CamelCaseToSnakeCaseNameConverter(); } @@ -93,25 +85,25 @@ private function buildResources(array $nodes, string $resourceClass): array } } - $resource = $resource->withGraphQlOperations($this->buildGraphQlOperations($node['graphQlOperations'] ?? null, $resource)); + $resource = $this->addGraphQlOperations($node['graphQlOperations'] ?? null, $resource); - $resources[] = $resource->withOperations(new Operations($this->buildOperations($node['operations'] ?? null, $resource))); + $resources[] = $this->addOperations($node['operations'] ?? null, $resource); } return $resources; } - private function buildOperations(?array $data, ApiResource $resource): array + private function addOperations(?array $data, ApiResource $resource): ApiResource { $operations = []; if (null === $data) { - foreach ([new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $operation) { - $operationName = sprintf('_api_%s_%s%s', $resource->getShortName(), strtolower($operation->getMethod()), $operation instanceof CollectionOperationInterface ? '_collection' : ''); - $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); + foreach ($this->getDefaultHttpOperations($resource) as $operation) { + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $operations[$key] = $operation; } - return $operations; + return $resource->withOperations(new Operations($operations)); } foreach ($data as $attributes) { @@ -138,31 +130,19 @@ private function buildOperations(?array $data, ApiResource $resource): array $operation = $operation->withExtraProperties(array_merge($operation->getExtraProperties(), [$key => $value])); } - if (empty($attributes['name'])) { - $attributes['name'] = sprintf('_api_%s_%s%s', $operation->getUriTemplate() ?: $operation->getShortName(), strtolower($operation->getMethod()), $operation instanceof CollectionOperationInterface ? '_collection' : ''); - } - $operations[$attributes['name']] = $this->getOperationWithDefaults($resource, $operation)->withName($attributes['name']); + [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); + $operations[$key] = $operation; } - return $operations; + return $resource->withOperations(new Operations($operations)); } - private function buildGraphQlOperations(?array $data, ApiResource $resource): array + private function addGraphQlOperations(?array $data, ApiResource $resource): ApiResource { $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; + return $this->addDefaultGraphQlOperations($resource); } foreach ($data as $attributes) { @@ -189,34 +169,8 @@ private function buildGraphQlOperations(?array $data, ApiResource $resource): ar $operations[] = $operation; } - return $operations; - } - - private function getOperationWithDefaults(ApiResource $resource, Operation $operation): Operation - { - foreach (($this->defaults['attributes'] ?? []) as $key => $value) { - $key = $this->camelCaseToSnakeCaseNameConverter->denormalize($key); - if (null === $operation->{'get'.ucfirst($key)}()) { - $operation = $operation->{'with'.ucfirst($key)}($value); - } - } - - 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); - } + $resource = $resource->withGraphQlOperations($operations); - return $operation; + return $this->completeGraphQlOperations($resource); } } diff --git a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php index cdb91e879a7..36ab490078d 100644 --- a/src/Metadata/Resource/Factory/OperationDefaultsTrait.php +++ b/src/Metadata/Resource/Factory/OperationDefaultsTrait.php @@ -16,6 +16,9 @@ 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; @@ -24,6 +27,10 @@ use ApiPlatform\Metadata\GraphQl\Subscription; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\State\CreateProvider; use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; @@ -71,10 +78,20 @@ private function getResourceWithDefaults(string $resourceClass, string $shortNam return $this->addGlobalDefaults($resource); } + private function getDefaultHttpOperations($resource): iterable + { + $post = new Post(); + if ($resource->getUriTemplate() && !$resource->getProvider()) { + $post = $post->withProvider(CreateProvider::class); + } + + return [new Get(), new GetCollection(), $post, new Put(), new Patch(), new Delete()]; + } + 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) { + foreach ([new QueryCollection(), new Query(), (new Mutation())->withName('update'), (new DeleteMutation())->withName('delete'), (new Mutation())->withName('create')] as $operation) { [$key, $operation] = $this->getOperationWithDefaults($resource, $operation); $graphQlOperations[$key] = $operation; } @@ -87,6 +104,38 @@ private function addDefaultGraphQlOperations(ApiResource $resource): ApiResource return $resource->withGraphQlOperations($graphQlOperations); } + /** + * Adds nested query operations if there are no existing query ones on the resource. + * They are needed when the resource is queried inside a root query, using a relation. + * Since the nested argument is used, root queries will not be generated for these operations. + */ + private function completeGraphQlOperations(ApiResource $resource): ApiResource + { + $graphQlOperations = $resource->getGraphQlOperations(); + + $hasQueryOperation = false; + $hasQueryCollectionOperation = false; + foreach ($graphQlOperations as $operation) { + if ($operation instanceof Query && !$operation instanceof QueryCollection) { + $hasQueryOperation = true; + } + if ($operation instanceof QueryCollection) { + $hasQueryCollectionOperation = true; + } + } + + if (!$hasQueryOperation) { + $queryOperation = (new Query())->withNested(true); + $graphQlOperations[$queryOperation->getName()] = $queryOperation; + } + if (!$hasQueryCollectionOperation) { + $queryCollectionOperation = (new QueryCollection())->withNested(true); + $graphQlOperations[$queryCollectionOperation->getName()] = $queryCollectionOperation; + } + + return $resource->withGraphQlOperations($graphQlOperations); + } + private function getOperationWithDefaults(ApiResource $resource, Operation $operation, bool $generated = false): array { // Inherit from resource defaults @@ -147,13 +196,15 @@ private function getOperationWithDefaults(ApiResource $resource, Operation $oper $operation = $operation->withName(''); } + $operationName = $operation->getName() ?? sprintf( + '_api_%s_%s%s', + $operation->getUriTemplate() ?: $operation->getShortName(), + strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), + $operation instanceof CollectionOperationInterface ? '_collection' : '', + ); + return [ - sprintf( - '_api_%s_%s%s', - $operation->getUriTemplate() ?: $operation->getShortName(), - strtolower($operation->getMethod() ?? HttpOperation::METHOD_GET), - $operation instanceof CollectionOperationInterface ? '_collection' : '', - ), + $operationName, $operation, ]; } diff --git a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml index 29f9a7a761f..329b20dfd8d 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_mongodb_odm.xml @@ -157,10 +157,6 @@ - - - - diff --git a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml index 0b6949bb9b8..856d4dc1ab5 100644 --- a/src/Symfony/Bundle/Resources/config/doctrine_orm.xml +++ b/src/Symfony/Bundle/Resources/config/doctrine_orm.xml @@ -167,11 +167,6 @@ - - - - - diff --git a/src/Symfony/Bundle/Resources/config/graphql.xml b/src/Symfony/Bundle/Resources/config/graphql.xml index 0f713b273b9..b5a8e4e80e5 100644 --- a/src/Symfony/Bundle/Resources/config/graphql.xml +++ b/src/Symfony/Bundle/Resources/config/graphql.xml @@ -133,7 +133,6 @@ %api_platform.graphql.nesting_separator% - @@ -275,14 +274,6 @@ - - - %api_platform.defaults% - - - - - diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml index 91d5d1a669a..d3d9dd8a64e 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml @@ -21,6 +21,7 @@ %api_platform.defaults% + diff --git a/src/Symfony/Bundle/Resources/config/metadata/yaml.xml b/src/Symfony/Bundle/Resources/config/metadata/yaml.xml index 2e417e7c578..f3d0209f3f3 100644 --- a/src/Symfony/Bundle/Resources/config/metadata/yaml.xml +++ b/src/Symfony/Bundle/Resources/config/metadata/yaml.xml @@ -24,6 +24,7 @@ %api_platform.defaults% + diff --git a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php b/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php deleted file mode 100644 index 3d70eca7197..00000000000 --- a/tests/GraphQl/Metadata/Factory/GraphQlNestedOperationResourceMetadataFactoryTest.php +++ /dev/null @@ -1,43 +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->assertEquals('RelatedDummy', $apiResource->getShortName()); - } -} diff --git a/tests/GraphQl/Type/FieldsBuilderTest.php b/tests/GraphQl/Type/FieldsBuilderTest.php index cf2cd9257f2..5313db3537d 100644 --- a/tests/GraphQl/Type/FieldsBuilderTest.php +++ b/tests/GraphQl/Type/FieldsBuilderTest.php @@ -58,7 +58,6 @@ class FieldsBuilderTest extends TestCase private ObjectProphecy $propertyNameCollectionFactoryProphecy; private ObjectProphecy $propertyMetadataFactoryProphecy; private ObjectProphecy $resourceMetadataCollectionFactoryProphecy; - private ObjectProphecy $graphQlNestedOperationResourceMetadataFactoryProphecy; private ObjectProphecy $typesContainerProphecy; private ObjectProphecy $typeBuilderProphecy; private ObjectProphecy $typeConverterProphecy; @@ -87,13 +86,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 @@ -140,6 +138,7 @@ public function itemQueryFieldsProvider(): array { return [ 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nested item query' => ['resourceClass', (new Query())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], new ObjectType(['name' => 'item']), function (): void {}, []], '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' => [ @@ -233,6 +232,7 @@ public function collectionQueryFieldsProvider(): array { return [ 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], + 'nested collection query' => ['resourceClass', (new QueryCollection())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], GraphQLType::listOf(new ObjectType(['name' => 'collection'])), function (): void {}, []], '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 { }, [ @@ -506,14 +506,6 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $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.')->withFilters(['search_filter'])->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->withFilters(['search_filter']))->willReturn(static function (): void { - }); - } } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); @@ -623,7 +615,6 @@ 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, [ @@ -638,14 +629,6 @@ public function resourceObjectTypeFieldsProvider(): array }, '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'), diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 77ba4f7e842..ee6dc591d4e 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -75,7 +75,9 @@ protected function setUp(): void public function testGetResourceObjectType(): void { - $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', [ + (new ApiResource())->withGraphQlOperations(['collection_query' => new QueryCollection()]), + ]); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('shortName', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); @@ -99,7 +101,9 @@ public function testGetResourceObjectType(): void public function testGetResourceObjectTypeOutputClass(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', [ + (new ApiResource())->withGraphQlOperations(['collection_query' => new QueryCollection()]), + ]); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('shortName', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); @@ -108,7 +112,7 @@ public function testGetResourceObjectTypeOutputClass(): void /** @var Operation $operation */ $operation = (new Query())->withShortName('shortName')->withDescription('description')->withOutput(['class' => 'outputClass']); /** @var ObjectType $resourceObjectType */ - $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadata, $operation, false); + $resourceObjectType = $this->typeBuilder->getResourceObjectType('resourceClass', $resourceMetadataCollection, $operation, false); $this->assertSame('shortName', $resourceObjectType->name); $this->assertSame('description', $resourceObjectType->description); $this->assertSame($this->defaultFieldResolver, $resourceObjectType->resolveFieldFn); @@ -249,8 +253,8 @@ public function testGetResourceObjectTypeMutation(): void $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description'), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), - ]), - ]); + 'collection_query' => new QueryCollection(), + ])]); $this->typesContainerProphecy->has('createShortNamePayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('createShortNamePayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); @@ -351,8 +355,8 @@ public function testGetResourceObjectTypeSubscription(): void $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), - ]), - ]); + 'collection_query' => new QueryCollection(), + ])]); $this->typesContainerProphecy->has('updateShortNameSubscriptionPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('updateShortNameSubscriptionPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); diff --git a/tests/GraphQl/Type/TypeConverterTest.php b/tests/GraphQl/Type/TypeConverterTest.php index aec61a133a5..6e7634b7900 100644 --- a/tests/GraphQl/Type/TypeConverterTest.php +++ b/tests/GraphQl/Type/TypeConverterTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -170,13 +171,14 @@ public function testConvertTypeInputResource(): void */ public function testConvertTypeCollectionResource(Type $type, ObjectType $expectedGraphqlType): void { - /** @var Operation $operation */ - $operation = (new Query())->withName('test'); - $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [(new ApiResource())->withShortName('DummyValue')->withGraphQlOperations(['test' => $operation])]); + $collectionOperation = new QueryCollection(); + $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [ + (new ApiResource())->withShortName('DummyValue')->withGraphQlOperations(['collection_query' => $collectionOperation]), + ]); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true); $this->resourceMetadataCollectionFactoryProphecy->create('dummyValue')->shouldBeCalled()->willReturn($graphqlResourceMetadata); - $this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, $operation, false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType); + $this->typeBuilderProphecy->getResourceObjectType('dummyValue', $graphqlResourceMetadata, $collectionOperation, false, false, 0)->shouldBeCalled()->willReturn($expectedGraphqlType); /** @var Operation $rootOperation */ $rootOperation = (new Query())->withName('test'); diff --git a/tests/Metadata/Extractor/Adapter/YamlResourceAdapter.php b/tests/Metadata/Extractor/Adapter/YamlResourceAdapter.php index 96895bcf164..41d376cdab7 100644 --- a/tests/Metadata/Extractor/Adapter/YamlResourceAdapter.php +++ b/tests/Metadata/Extractor/Adapter/YamlResourceAdapter.php @@ -38,7 +38,7 @@ public function __invoke(string $resourceClass, array $parameters, array $fixtur $resource = []; foreach ($parameters as $parameter) { $parameterName = $parameter->getName(); - $resource[$parameterName] = \array_key_exists($parameterName, $fixture) ? $fixture[$parameterName] : null; + $resource[$parameterName] = $fixture[$parameterName] ?? null; } $yaml['resources'][$resourceClass][] = $resource; } diff --git a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php index e6ce7a3727d..420a0e274c1 100644 --- a/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/ResourceMetadataCompatibilityTest.php @@ -163,7 +163,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], 'shortName' => self::SHORT_NAME, 'description' => 'A list of Comments', - 'class' => GetCollection::class, + 'class' => Mutation::class, 'urlGenerationStrategy' => 0, 'deprecationReason' => 'I don\'t know', 'normalizationContext' => [ @@ -207,18 +207,26 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'serialize' => true, 'priority' => 200, 'extraProperties' => [ + 'custom_property' => 'Lorem ipsum dolor sit amet', + 'another_custom_property' => [ + 'Lorem ipsum' => 'Dolor sit amet', + ], 'foo' => 'bar', ], ], ], 'queries' => [ [ - 'class' => Get::class, + 'class' => Query::class, + ], + [ + 'class' => QueryCollection::class, + 'collection' => true, ], ], 'subscriptions' => [ [ - 'class' => Post::class, + 'class' => Subscription::class, ], ], ], @@ -328,6 +336,10 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'serialize' => true, 'priority' => 200, 'extraProperties' => [ + 'custom_property' => 'Lorem ipsum dolor sit amet', + 'another_custom_property' => [ + 'Lorem ipsum' => 'Dolor sit amet', + ], 'foo' => 'bar', ], ], @@ -452,7 +464,7 @@ private function buildApiResources(): array $operations = []; foreach ([new Get(), new GetCollection(), new Post(), new Put(), new Patch(), new Delete()] as $operation) { $operationName = sprintf('_api_%s_%s%s', $resource->getShortName(), strtolower($operation->getMethod()), $operation instanceof CollectionOperationInterface ? '_collection' : ''); - $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation)->withName($operationName); + $operations[$operationName] = $this->getOperationWithDefaults($resource, $operation); } $resource = $resource->withOperations(new Operations($operations)); @@ -461,7 +473,7 @@ private function buildApiResources(): array $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); + $graphQlOperations[$graphQlOperation->getName()] = $this->getOperationWithDefaults($resource, $graphQlOperation)->withDescription($description); } $resources[] = $resource->withGraphQlOperations($graphQlOperations); @@ -547,10 +559,8 @@ private function withOperations(array $values, ?array $fixtures): Operations throw new \RuntimeException(sprintf('Unknown Operation parameter "%s".', $parameter)); } - if (null === $operation->getName()) { - $operation = $operation->withName(sprintf('_api_%s_%s%s', $operation->getUriTemplate() ?: $operation->getShortName(), strtolower($operation->getMethod()), $operation instanceof CollectionOperationInterface ? '_collection' : '')); - } - $operations[$operation->getName()] = $operation; + $operationName = $operation->getName() ?? sprintf('_api_%s_%s%s', $operation->getUriTemplate() ?: $operation->getShortName(), strtolower($operation->getMethod()), $operation instanceof CollectionOperationInterface ? '_collection' : ''); + $operations[$operationName] = $operation; } return new Operations($operations); @@ -559,22 +569,11 @@ private function withOperations(array $values, ?array $fixtures): Operations private function withGraphQlOperations(array $values, ?array $fixtures): array { $operations = []; - foreach ($values as $type => $graphQlOperations) { - switch ($type) { - case 'queries': - $class = Query::class; - break; - case 'mutations': - $class = Mutation::class; - break; - case 'subscriptions': - $class = Subscription::class; - break; - default: - } - + foreach ($values as $graphQlOperations) { foreach ($graphQlOperations as $value) { - $operation = new $class(); // @phpstan-ignore-line + $class = $value['class']; + $operation = new $class(); + unset($value['collection']); foreach (self::BASE as $parameter) { if ((!\array_key_exists($parameter, $value) || null === $value[$parameter]) && isset($fixtures[$parameter])) { diff --git a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 5be3cc47aed..006ad7e788d 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -671,8 +671,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 = [