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%
+