Skip to content

Commit

Permalink
Merge pull request #64 from adri/feature/exception-mapping
Browse files Browse the repository at this point in the history
Exceptions can be mapped to UserError and UserWarning
  • Loading branch information
mcg-web authored Sep 21, 2016
2 parents ece07fe + 0c9dab1 commit 0ef2a6b
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 2 deletions.
24 changes: 24 additions & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ public function getConfigTreeBuilder()
->end()
->end()
->end()
->arrayNode('exceptions')
->children()
->arrayNode('warnings')
->treatNullLike(array())
->prototype('scalar')->end()
->end()
->arrayNode('errors')
->treatNullLike(array())
->prototype('scalar')->end()
->end()
->arrayNode('types')
->addDefaultsIfNotSet()
->children()
->scalarNode('warnings')
->defaultValue('Overblog\\GraphQLBundle\\Error\\UserWarning')
->end()
->scalarNode('errors')
->defaultValue('Overblog\\GraphQLBundle\\Error\\UserError')
->end()
->end()
->end()
->end()
->end()

->arrayNode('builders')
->children()
->arrayNode('field')
Expand Down
31 changes: 31 additions & 0 deletions DependencyInjection/OverblogGraphQLExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ private function setErrorHandlerArguments(array $config, ContainerBuilder $conta
->replaceArgument(0, $config['definitions']['internal_error_message'])
;
}

if (isset($config['definitions']['exceptions'])) {
$container
->getDefinition($this->getAlias().'.error_handler')
->replaceArgument(2, $this->buildExceptionMap($config['definitions']['exceptions']))
;
}
}

private function setSchemaBuilderArguments(array $config, ContainerBuilder $container)
Expand Down Expand Up @@ -142,4 +149,28 @@ public function getConfiguration(array $config, ContainerBuilder $container)
{
return new Configuration($container->getParameter('kernel.debug'));
}

/**
* Returns a list of custom exceptions mapped to error/warning classes.
*
* @param array $config
* @return array Custom exception map, [exception => UserError/UserWarning].
*/
private function buildExceptionMap(array $exceptionConfig)
{
$exceptionMap = [];
$typeMap = $exceptionConfig['types'];

foreach ($exceptionConfig as $type => $exceptionList) {
if ('types' === $type) {
continue;
}

foreach ($exceptionList as $exception) {
$exceptionMap[$exception] = $typeMap[$type];
}
}

return $exceptionMap;
}
}
30 changes: 28 additions & 2 deletions Error/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ class ErrorHandler
/** @var string */
private $internalErrorMessage;

public function __construct($internalErrorMessage = null, LoggerInterface $logger = null)
/** @var array */
private $exceptionMap;

public function __construct($internalErrorMessage = null, LoggerInterface $logger = null, array $exceptionMap = [])
{
$this->logger = (null === $logger) ? new NullLogger() : $logger;
if (empty($internalErrorMessage)) {
$internalErrorMessage = self::DEFAULT_ERROR_MESSAGE;
}
$this->internalErrorMessage = $internalErrorMessage;
$this->exceptionMap = $exceptionMap;
}

/**
Expand All @@ -54,7 +58,7 @@ protected function treatExceptions(array $errors, $throwRawException)

/** @var Error $error */
foreach ($errors as $error) {
$rawException = $error->getPrevious();
$rawException = $this->convertException($error->getPrevious());

// Parse error or user error
if (null === $rawException) {
Expand Down Expand Up @@ -129,4 +133,26 @@ public function handleErrors(ExecutionResult $executionResult, $throwRawExceptio
$executionResult->extensions['warnings'] = array_map(['GraphQL\Error', 'formatError'], $exceptions['extensions']['warnings']);
}
}

/**
* Tries to convert a raw exception into a user warning or error
* that is displayed to the user.
*
* @param \Exception $rawException
* @return \Exception
*/
protected function convertException(\Exception $rawException = null)
{
if (null === $rawException) {
return null;
}

if (!empty($this->exceptionMap[get_class($rawException)])) {
$errorClass = $this->exceptionMap[get_class($rawException)];

return new $errorClass($rawException->getMessage(), $rawException->getCode(), $rawException);
}

return $rawException;
}
}
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,25 @@ class CharacterResolver
}
```

If you want to map your own exceptions to warnings and errors you can
define a custom exception mapping:

```yaml
#app/config/config.yml
overblog_graphql:
#...
definitions:
#...
exceptions:
warnings:
- "Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException"
errors:
- "InvalidArgumentException"
```
The message of those exceptions are then shown to the user like other
`UserError`s or `UserWarning`s.

Security
--------

Expand Down
1 change: 1 addition & 0 deletions Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
arguments:
- ~
- "@?logger"
- []

overblog_graphql.request_executor:
class: Overblog\GraphQLBundle\Request\Executor
Expand Down
36 changes: 36 additions & 0 deletions Tests/DependencyInjection/OverblogGraphQLTypesExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ public function testInternalConfigKeysShouldNotBeUsed($internalConfigKey)
$this->extension->load($configs, $this->container);
}

/**
* @runInSeparateProcess
*/
public function testCustomExceptions()
{
$ext = new OverblogGraphQLExtension();
$ext->load(
[
[
'definitions' => [
'exceptions' => [
'warnings' => [
'Symfony\Component\Routing\Exception\ResourceNotFoundException'
],
'errors' => [
'InvalidArgumentException'
],
]
],
],
],
$this->container
);

$expectedExceptionMap = [
'Symfony\Component\Routing\Exception\ResourceNotFoundException' => 'Overblog\\GraphQLBundle\\Error\\UserWarning',
'InvalidArgumentException' => 'Overblog\\GraphQLBundle\\Error\\UserError',
];

$definition = $this->container->getDefinition('overblog_graphql.error_handler');
$this->assertEquals($expectedExceptionMap, $definition->getArgument(2));
}

/**
* @runInSeparateProcess
*/
public function testCustomBuilders()
{
$ext = new OverblogGraphQLExtension();
Expand Down
27 changes: 27 additions & 0 deletions Tests/Error/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,31 @@ public function testMaskErrorWithoutWrappedExceptionAndThrowExceptionSetToTrue()

$this->assertEquals($expected, $executionResult->toArray());
}

public function testConvertExceptionToUserWarning()
{
$errorHandler = new ErrorHandler(null, null, ["InvalidArgumentException" => 'Overblog\\GraphQLBundle\\Error\\UserWarning']);

$executionResult = new ExecutionResult(
null,
[
new Error('Error with invalid argument exception', null, new \InvalidArgumentException('Invalid argument exception')),
]
);

$errorHandler->handleErrors($executionResult, true);

$expected = [
'data' => null,
'extensions' => [
'warnings' => [
[
'message' => 'Error with invalid argument exception',
],
],
],
];

$this->assertEquals($expected, $executionResult->toArray());
}
}
56 changes: 56 additions & 0 deletions Tests/Functional/Exception/ExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the OverblogGraphQLBundle package.
*
* (c) Overblog <http://github.com/overblog/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Overblog\GraphQLBundle\Tests\Functional\Exception;

use Overblog\GraphQLBundle\Tests\Functional\TestCase;

/**
* Class ConnectionTest.
*
* @see https://github.com/graphql/graphql-relay-js/blob/master/src/connection/__tests__/connection.js
*/
class ExceptionTest extends TestCase
{
protected function setUp()
{
parent::setUp();

static::createAndBootKernel(['test_case' => 'exception']);
}

public function testExceptionIsMappedToAWarning()
{
$query = <<<EOF
query ExceptionQuery {
test
}
EOF;

$expectedData = [
'test' => null,
];

$expectedErrors = [
[
'message' => 'Invalid argument exception',
'locations' => [
[
'line' => 2,
'column' => 5,
]
],
],
];

$this->assertGraphQL($query, $expectedData, $expectedErrors);
}
}
20 changes: 20 additions & 0 deletions Tests/Functional/app/Exception/ExampleException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the OverblogGraphQLBundle package.
*
* (c) Overblog <http://github.com/overblog/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Overblog\GraphQLBundle\Tests\Functional\app\Exception;

class ExampleException
{
public function throwException()
{
throw new \InvalidArgumentException('Invalid argument exception');
}
}
20 changes: 20 additions & 0 deletions Tests/Functional/app/config/exception/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
imports:
- { resource: ../config.yml }
- { resource: services.yml }

parameters:
overblog_graphql.type_class_namespace: "Overblog\\GraphQLBundle\\Exception\\__DEFINITIONS__"

overblog_graphql:
definitions:
exceptions:
errors:
- "InvalidArgumentException"
schema:
query: Query
mutation: ~
mappings:
types:
-
type: yml
dir: "%kernel.root_dir%/config/exception/mapping"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Query:
type: object
config:
fields:
test:
type: "Boolean"
resolve: "@=resolver('example_exception')"

5 changes: 5 additions & 0 deletions Tests/Functional/app/config/exception/services.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
overblog_graphql.test.exception:
class: Overblog\GraphQLBundle\Tests\Functional\app\Exception\ExampleException
tags:
- { name: "overblog_graphql.resolver", alias: "example_exception", method: "throwException" }

0 comments on commit 0ef2a6b

Please sign in to comment.