Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ api_platform:
form: ['multipart/form-data']
```

## v3.2.20

### Bug fixes

* [90c9fb31a](https://github.com/api-platform/core/commit/90c9fb31a322a2c7891fbbafb75d60b09fd67772) fix(symfony): register api_error route (#6281)
* [d061c3811](https://github.com/api-platform/core/commit/d061c381120b858c471157dbdccbb5084a39fb04) fix(graphql): increment graphql normalizer priority (#6283)

## v3.2.19

### Bug fixes
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"doctrine/orm": "<2.14.0",
"doctrine/mongodb-odm": "<2.4",
"doctrine/persistence": "<1.3",
"symfony/framework-bundle": "6.4.6 || 7.0.6",
"symfony/var-exporter": "<6.1.1",
"phpunit/phpunit": "<9.5",
"phpspec/prophecy": "<1.15",
Expand Down
74 changes: 72 additions & 2 deletions features/graphql/query.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Feature: GraphQL query support

@createSchema
Scenario: Retrieve an item with different relations to the same resource
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
When I send the following GraphQL request:
"""
{
Expand All @@ -33,6 +33,10 @@ Feature: GraphQL query support
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
Expand Down Expand Up @@ -70,7 +74,7 @@ Feature: GraphQL query support

@createSchema
Scenario: Retrieve embedded collections
Given there are 2 multiRelationsDummy objects having each a manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
When I send the following GraphQL request:
"""
{
Expand All @@ -81,6 +85,10 @@ Feature: GraphQL query support
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
Expand Down Expand Up @@ -113,10 +121,13 @@ Feature: GraphQL query support
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "errors" should not exist
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.name" should be equal to "RelatedManyToOneResolveDummy #2"
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
Expand All @@ -135,6 +146,65 @@ Feature: GraphQL query support
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[2].node.name" should be equal to "NestedPaginatedDummy3"
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[3].node.name" should be equal to "NestedPaginatedDummy4"

@createSchema
Scenario: Retrieve an item with different relations (all unset)
Given there are 2 multiRelationsDummy objects having each 0 manyToOneRelation, 0 manyToManyRelations, 0 oneToManyRelations and 0 embeddedRelations
When I send the following GraphQL request:
"""
{
multiRelationsDummy(id: "/multi_relations_dummies/2") {
id
name
manyToOneRelation {
id
name
}
manyToOneResolveRelation {
id
name
}
manyToManyRelations {
edges{
node {
id
name
}
}
}
oneToManyRelations {
edges{
node {
id
name
}
}
}
nestedCollection {
name
}
nestedPaginatedCollection {
edges{
node {
name
}
}
}
}
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/json"
And the JSON node "errors" should not exist
And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
And the JSON node "data.multiRelationsDummy.manyToOneRelation" should be null
And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation" should be null
And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 0 element
And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 0 element
And the JSON node "data.multiRelationsDummy.nestedCollection" should have 0 element
And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 0 element

@createSchema @!mongodb
Scenario: Retrieve an item with child relation to the same resource
Given there are tree dummies
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/CollectionResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\CloneTrait;
use ApiPlatform\State\Pagination\ArrayPaginator;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -40,7 +41,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -44,7 +45,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
if (null === $resourceClass || null === $operation) {
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ItemResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -41,7 +42,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
// Data already fetched and normalized (field or nested resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface;
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Metadata\Util\CloneTrait;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -37,7 +38,7 @@ public function __construct(private readonly ReadStageInterface $readStage, priv
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array {
if (null === $resourceClass || null === $operation) {
Expand Down
30 changes: 20 additions & 10 deletions src/GraphQl/Resolver/Factory/ResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\State\Pagination\ArrayPaginator;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
Expand All @@ -30,21 +31,30 @@ public function __construct(
) {
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
// Data already fetched and normalized (field or nested resource)
if ($body = $source[$info->fieldName] ?? null) {
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation, $propertyMetadataFactory) {
if (\array_key_exists($info->fieldName, $source ?? [])) {
$body = $source[$info->fieldName];

// special treatment for nested resources without a resolver/provider
if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && (!$operation->getProvider() || NoopProvider::class === $operation->getProvider())) {
return $this->resolve($source, $args, $info, $rootClass, $operation, new ArrayPaginator($body, 0, \count($body)));
return \is_array($body) ? $this->resolve(
$source,
$args,
$info,
$rootClass,
$operation,
new ArrayPaginator($body, 0, \count($body))
) : $body;
}

return $body;
}

if (null === $resourceClass && \array_key_exists($info->fieldName, $source ?? [])) {
return $body;
$propertyMetadata = $rootClass ? $propertyMetadataFactory?->create($rootClass, $info->fieldName) : null;
$type = $propertyMetadata?->getBuiltinTypes()[0] ?? null;
// Data already fetched and normalized (field or nested resource)
if ($body || null === $resourceClass || ($type && !$type->isCollection())) {
return $body;
}
}

// If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response.
Expand Down
3 changes: 2 additions & 1 deletion src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\GraphQl\Resolver\Factory;

use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;

/**
* Builds a GraphQL resolver.
Expand All @@ -22,5 +23,5 @@
*/
interface ResolverFactoryInterface
{
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable;
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable;
}
6 changes: 5 additions & 1 deletion src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
namespace ApiPlatform\GraphQl\Tests\Resolver\Factory;

use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\GraphQl\Mutation;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\ProviderInterface;
use GraphQL\Type\Definition\ResolveInfo;
Expand All @@ -38,11 +40,13 @@ public function testGraphQlResolver(?string $resourceClass = null, ?string $root
$provider->expects($this->once())->method('provide')->with($providedOperation ?: $operation, [], $context)->willReturn($body);
$processor = $this->createMock(ProcessorInterface::class);
$processor->expects($this->once())->method('process')->with($body, $processedOperation ?: $operation, [], $context)->willReturn($returnValue);
$propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactory->expects($this->once())->method('create')->with($rootClass, 'test')->willReturn(new ApiProperty(schema: ['type' => 'array']));
$resolveInfo = $this->createMock(ResolveInfo::class);
$resolveInfo->fieldName = 'test';

$resolverFactory = new ResolverFactory($provider, $processor);
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation)(['test' => null], [], [], $resolveInfo), $returnValue);
$this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation, $propertyMetadataFactory)(['test' => null], [], [], $resolveInfo), $returnValue);
}

public static function graphQlQueries(): array
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQl/Type/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ private function getResourceFieldConfiguration(?string $property, ?string $field
if ($isStandardGraphqlType || $input) {
$resolve = null;
} else {
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation);
$resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory);
}
} else {
if ($isStandardGraphqlType || $input) {
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/Resources/config/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<!-- Run before serializer.normalizer.json_serializable and before serializer.normalizer.backed_enum which is at 880 -->
<tag name="serializer.normalizer" priority="-890" />
</service>

Expand Down
12 changes: 0 additions & 12 deletions src/Symfony/Bundle/Resources/config/routing/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,4 @@

<requirement key="index">index</requirement>
</route>

<route id="api_errors" path="/errors/{status}">
<default key="_controller">api_platform.action.not_exposed</default>
<default key="status">500</default>

<requirement key="status">\d+</requirement>
</route>

<route id="api_validation_errors" path="/validation_errors/{id}">
<default key="_controller">api_platform.action.not_exposed</default>
</route>

</routes>
18 changes: 18 additions & 0 deletions src/Symfony/Bundle/Resources/config/routing/errors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>

<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing
http://symfony.com/schema/routing/routing-1.0.xsd">

<route id="api_errors" path="/errors/{status}">
<default key="_controller">api_platform.action.not_exposed</default>
<default key="status">500</default>

<requirement key="status">\d+</requirement>
</route>

<route id="api_validation_errors" path="/validation_errors/{id}">
<default key="_controller">api_platform.action.not_exposed</default>
</route>
</routes>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface;
use ApiPlatform\Metadata\GraphQl\Operation;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use GraphQL\Type\Definition\ResolveInfo;
use Symfony\Component\HttpFoundation\RequestStack;

Expand All @@ -24,7 +25,7 @@ public function __construct(private readonly ResolverFactoryInterface $resolverF
{
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null): callable
public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable
{
return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) {
if ($this->requestStack && null !== $request = $this->requestStack->getCurrentRequest()) {
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Routing/ApiLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public function supports(mixed $resource, ?string $type = null): bool
private function loadExternalFiles(RouteCollection $routeCollection): void
{
$routeCollection->addCollection($this->fileLoader->load('genid.xml'));
$routeCollection->addCollection($this->fileLoader->load('errors.xml'));

if ($this->entrypointEnabled) {
$routeCollection->addCollection($this->fileLoader->load('api.xml'));
Expand Down
Loading