Skip to content

Commit 16e682e

Browse files
committed
Adding GraphQL custom error format support
1 parent 8bd3dca commit 16e682e

File tree

19 files changed

+461
-16
lines changed

19 files changed

+461
-16
lines changed

features/graphql/mutation.feature

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,12 @@ Feature: GraphQL mutation support
335335
Then the response status code should be 200
336336
And the response should be in JSON
337337
And the header "Content-Type" should be equal to "application/json"
338-
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
338+
And the JSON node "errors[0].status" should be equal to "400"
339+
And the JSON node "errors[0].message" should be equal to "Validation Exception"
340+
And the JSON node "errors[0].violations" should exist
341+
And the JSON node "errors[0].violations[0].path" should be equal to "name"
342+
And the JSON node "errors[0].violations[0].message" should be equal to "This value should not be blank."
343+
339344

340345
Scenario: Execute a custom mutation
341346
Given there are 1 dummyCustomMutation objects

src/Bridge/Symfony/Bundle/ApiPlatformBundle.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\DataProviderPass;
1818
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\ElasticsearchClientPass;
1919
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\FilterPass;
20+
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlExceptionFormatterPass;
2021
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlMutationResolverPass;
2122
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlQueryResolverPass;
2223
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\GraphQlTypePass;
2324
use ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler\MetadataAwareNameConverterPass;
2425
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
25-
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
2626
use Symfony\Component\DependencyInjection\ContainerBuilder;
2727
use Symfony\Component\HttpKernel\Bundle\Bundle;
2828

@@ -48,6 +48,7 @@ public function build(ContainerBuilder $container)
4848
$container->addCompilerPass(new GraphQlTypePass());
4949
$container->addCompilerPass(new GraphQlQueryResolverPass());
5050
$container->addCompilerPass(new GraphQlMutationResolverPass());
51+
$container->addCompilerPass(new GraphQlExceptionFormatterPass());
5152
$container->addCompilerPass(new MetadataAwareNameConverterPass());
5253
}
5354
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
2929
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
3030
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
31+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
3132
use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface;
3233
use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
3334
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
@@ -396,6 +397,8 @@ private function registerGraphQlConfiguration(ContainerBuilder $container, array
396397
->addTag('api_platform.graphql.mutation_resolver');
397398
$container->registerForAutoconfiguration(GraphQlTypeInterface::class)
398399
->addTag('api_platform.graphql.type');
400+
$container->registerForAutoconfiguration(ExceptionFormatterInterface::class)
401+
->addTag('api_platform.graphql.exception_formatter');
399402
}
400403

401404
private function registerLegacyBundlesConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Compiler;
15+
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
/**
21+
* Injects GraphQL Exception formatters.
22+
*
23+
* @internal
24+
*
25+
* @author Mahmood Bazdar <[email protected]>
26+
*/
27+
class GraphQlExceptionFormatterPass implements CompilerPassInterface
28+
{
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function process(ContainerBuilder $container)
33+
{
34+
if (!$container->getParameter('api_platform.graphql.enabled')) {
35+
return;
36+
}
37+
38+
$formatters = [];
39+
foreach ($container->findTaggedServiceIds('api_platform.graphql.exception_formatter', true) as $serviceId => $tags) {
40+
foreach ($tags as $tag) {
41+
$formatters[$tag['id'] ?? $serviceId] = new Reference($serviceId);
42+
}
43+
}
44+
$container->getDefinition('api_platform.graphql.exception_formatter_locator')->addArgument($formatters);
45+
$container->getDefinition('api_platform.graphql.exception_formatter_factory')->addArgument(array_keys($formatters));
46+
}
47+
}

src/Bridge/Symfony/Bundle/Resources/config/graphql.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,26 @@
159159
<argument type="service" id="api_platform.graphql.types_container" />
160160
<argument type="service" id="api_platform.graphql.fields_builder" />
161161
</service>
162+
<!-- Exception formatter -->
163+
<service id="api_platform.graphql.validation_exception_formatter" class="ApiPlatform\Core\GraphQl\Exception\Formatter\ValidationExceptionFormatter">
164+
<tag name="api_platform.graphql.exception_formatter" />
165+
</service>
162166

167+
<service id="api_platform.graphql.exception_formatter_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
168+
<tag name="container.service_locator" />
169+
</service>
170+
171+
<service id="api_platform.graphql.exception_formatter_factory" class="ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterFactory">
172+
<argument type="service" id="api_platform.graphql.exception_formatter_locator" />
173+
</service>
163174
<!-- Action -->
164175

165176
<service id="api_platform.graphql.action.entrypoint" class="ApiPlatform\Core\GraphQl\Action\EntrypointAction" public="true">
166177
<argument type="service" id="api_platform.graphql.schema_builder" />
167178
<argument type="service" id="api_platform.graphql.executor" />
168179
<argument type="service" id="api_platform.graphql.action.graphiql" />
169180
<argument type="service" id="api_platform.graphql.action.graphql_playground" />
181+
<argument type="service" id="api_platform.graphql.exception_formatter_factory" />
170182
<argument>%kernel.debug%</argument>
171183
<argument>%api_platform.graphql.graphiql.enabled%</argument>
172184
<argument>%api_platform.graphql.graphql_playground.enabled%</argument>

src/GraphQl/Action/EntrypointAction.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313

1414
namespace ApiPlatform\Core\GraphQl\Action;
1515

16+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterFactory;
17+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
1618
use ApiPlatform\Core\GraphQl\ExecutorInterface;
1719
use ApiPlatform\Core\GraphQl\Type\SchemaBuilderInterface;
1820
use GraphQL\Error\Debug;
1921
use GraphQL\Error\Error;
2022
use GraphQL\Error\UserError;
23+
use GraphQL\Error\FormattedError;
2124
use GraphQL\Executor\ExecutionResult;
2225
use Symfony\Component\HttpFoundation\JsonResponse;
2326
use Symfony\Component\HttpFoundation\Request;
@@ -39,8 +42,9 @@ final class EntrypointAction
3942
private $graphiqlEnabled;
4043
private $graphQlPlaygroundEnabled;
4144
private $defaultIde;
45+
private $exceptionFormatterFactory;
4246

43-
public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, GraphiQlAction $graphiQlAction, GraphQlPlaygroundAction $graphQlPlaygroundAction, bool $debug = false, bool $graphiqlEnabled = false, bool $graphQlPlaygroundEnabled = false, $defaultIde = false)
47+
public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInterface $executor, GraphiQlAction $graphiQlAction, GraphQlPlaygroundAction $graphQlPlaygroundAction, ExceptionFormatterFactory $exceptionFormatterFactory, bool $debug = false, bool $graphiqlEnabled = false, bool $graphQlPlaygroundEnabled = false, $defaultIde = false)
4448
{
4549
$this->schemaBuilder = $schemaBuilder;
4650
$this->executor = $executor;
@@ -50,6 +54,7 @@ public function __construct(SchemaBuilderInterface $schemaBuilder, ExecutorInter
5054
$this->graphiqlEnabled = $graphiqlEnabled;
5155
$this->graphQlPlaygroundEnabled = $graphQlPlaygroundEnabled;
5256
$this->defaultIde = $defaultIde;
57+
$this->exceptionFormatterFactory = $exceptionFormatterFactory;
5358
}
5459

5560
public function __invoke(Request $request): Response
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
use ApiPlatform\Core\GraphQl\Type\Definition\TypeInterface;
17+
use Psr\Container\ContainerInterface;
18+
19+
/**
20+
* Get the registered services corresponding to GraphQL exception formatters.
21+
*
22+
* @author Mahmood Bazdar <[email protected]>
23+
*/
24+
class ExceptionFormatterFactory implements ExceptionFormatterFactoryInterface
25+
{
26+
private $exceptionFormatterLocator;
27+
private $exceptionFormatterIds;
28+
29+
/**
30+
* @param string[] $exceptionFormatterIds
31+
*/
32+
public function __construct(ContainerInterface $exceptionFormatterLocator, array $exceptionFormatterIds)
33+
{
34+
$this->exceptionFormatterLocator = $exceptionFormatterLocator;
35+
$this->exceptionFormatterIds = $exceptionFormatterIds;
36+
}
37+
38+
public function getExceptionFormatters(): array
39+
{
40+
$exceptionFormatters = [];
41+
42+
foreach ($this->exceptionFormatterIds as $exceptionFormatterId) {
43+
/** @var TypeInterface $exceptionFormatter */
44+
$exceptionFormatter = $this->exceptionFormatterLocator->get($exceptionFormatterId);
45+
$exceptionFormatters[$exceptionFormatterId] = $exceptionFormatter;
46+
}
47+
48+
return $exceptionFormatters;
49+
}
50+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
/**
17+
* Get Exception formatters.
18+
*
19+
* @author Mahmood Bazdar <[email protected]>
20+
*/
21+
interface ExceptionFormatterFactoryInterface
22+
{
23+
public function getExceptionFormatters(): array;
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception;
15+
16+
use GraphQL\Error\Error;
17+
18+
/**
19+
* @expremintal
20+
*
21+
* @author Mahmood Bazdar <[email protected]>
22+
*/
23+
interface ExceptionFormatterInterface
24+
{
25+
/**
26+
* Formats the exception and returns the formatted array.
27+
*/
28+
public function format(Error $error): array;
29+
30+
/**
31+
* Check the exception, return true if you can format the exception.
32+
*/
33+
public function supports(\Throwable $exception): bool;
34+
35+
/**
36+
* Priority of your formatter in container. Higher number will be called sooner.
37+
*/
38+
public function getPriority(): int;
39+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\GraphQl\Exception\Formatter;
15+
16+
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
17+
use ApiPlatform\Core\GraphQl\Exception\ExceptionFormatterInterface;
18+
use GraphQL\Error\Error;
19+
use GraphQL\Error\FormattedError;
20+
use Symfony\Component\HttpFoundation\Response;
21+
use Symfony\Component\Validator\ConstraintViolation;
22+
23+
/**
24+
* Formats Validation exception.
25+
*
26+
* @author Mahmood Bazdar <[email protected]>
27+
*/
28+
class ValidationExceptionFormatter implements ExceptionFormatterInterface
29+
{
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function format(Error $error): array
34+
{
35+
/**
36+
* @var ValidationException
37+
*/
38+
$validationException = $error->getPrevious();
39+
$error = FormattedError::createFromException($error);
40+
$error['message'] = $validationException->getMessage();
41+
$error['status'] = Response::HTTP_BAD_REQUEST;
42+
$error['extensions']['category'] = Error::CATEGORY_GRAPHQL;
43+
$error['violations'] = [];
44+
45+
/** @var ConstraintViolation $violation */
46+
foreach ($validationException->getConstraintViolationList() as $violation) {
47+
$error['violations'][] = [
48+
'path' => $violation->getPropertyPath(),
49+
'message' => $violation->getMessage(),
50+
];
51+
}
52+
53+
return $error;
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public function supports(\Throwable $exception): bool
60+
{
61+
return $exception instanceof ValidationException;
62+
}
63+
64+
/**
65+
* {@inheritdoc}
66+
*/
67+
public function getPriority(): int
68+
{
69+
return -100;
70+
}
71+
}

0 commit comments

Comments
 (0)