From e7c34f84a407e03eab9331b56de913e066fa43dd Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Sun, 13 Mar 2016 16:58:16 +0100 Subject: [PATCH 1/3] fix php cs --- .styleci.yml | 6 ++++++ .travis.yml | 6 +++--- DependencyInjection/OverblogGraphQLExtension.php | 2 +- DependencyInjection/TypesConfiguration.php | 4 ++-- Error/ErrorHandler.php | 2 +- OverblogGraphQLBundle.php | 2 +- Relay/Connection/ConnectionType.php | 4 ++-- Relay/Mutation/InputType.php | 2 +- Relay/Mutation/PayloadType.php | 2 +- Relay/Node/GlobalId.php | 2 +- Request/Executor.php | 2 +- Resolver/AbstractResolver.php | 2 +- Resolver/ConfigResolver.php | 2 +- Tests/Functional/TestCase.php | 2 +- Tests/Functional/app/AppKernel.php | 4 ++-- Tests/Resolver/ConfigResolverTest.php | 1 - 16 files changed, 25 insertions(+), 20 deletions(-) diff --git a/.styleci.yml b/.styleci.yml index 974f5fa5d..013c1474f 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1 +1,7 @@ preset: symfony +enabled: + - ordered_use + - short_array_syntax + +disabled: + - unalign_equals diff --git a/.travis.yml b/.travis.yml index 4b81309c4..9da8c0867 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,9 @@ php: - hhvm branches: - only: - - master - - /^\d+\.\d+$/ + only: + - master + - /^\d+\.\d+$/ matrix: fast_finish: true diff --git a/DependencyInjection/OverblogGraphQLExtension.php b/DependencyInjection/OverblogGraphQLExtension.php index 65310a610..5016573cb 100644 --- a/DependencyInjection/OverblogGraphQLExtension.php +++ b/DependencyInjection/OverblogGraphQLExtension.php @@ -16,12 +16,12 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; diff --git a/DependencyInjection/TypesConfiguration.php b/DependencyInjection/TypesConfiguration.php index 79450e2d2..3d75457ec 100644 --- a/DependencyInjection/TypesConfiguration.php +++ b/DependencyInjection/TypesConfiguration.php @@ -130,7 +130,7 @@ private function addFieldsSelection($name, $enabledBuilder = true) ->prototype('array') ->beforeNormalization() ->ifString() - ->then(function ($v) { return array('builder' => $v); }) + ->then(function ($v) { return ['builder' => $v]; }) ->end() ->children() ->append($this->addTypeSelection()) @@ -138,7 +138,7 @@ private function addFieldsSelection($name, $enabledBuilder = true) ->info('Use to build dynamic args. Can be combine with args.') ->beforeNormalization() ->ifString() - ->then(function ($v) { return array('name' => $v); }) + ->then(function ($v) { return ['name' => $v]; }) ->end() ->children() ->scalarNode('name') diff --git a/Error/ErrorHandler.php b/Error/ErrorHandler.php index 28ba7f3f9..6edecae8e 100644 --- a/Error/ErrorHandler.php +++ b/Error/ErrorHandler.php @@ -37,7 +37,7 @@ public function __construct($internalErrorMessage = null, LoggerInterface $logge /** * @param Error[] $errors - * @param $throwRawException + * @param boolean $throwRawException * * @return Error[] * diff --git a/OverblogGraphQLBundle.php b/OverblogGraphQLBundle.php index dffe55749..471cb40fd 100644 --- a/OverblogGraphQLBundle.php +++ b/OverblogGraphQLBundle.php @@ -17,9 +17,9 @@ use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverPass; use Overblog\GraphQLBundle\DependencyInjection\Compiler\TypePass; use Overblog\GraphQLBundle\DependencyInjection\OverblogGraphQLExtension; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Symfony\Component\DependencyInjection\ContainerBuilder; class OverblogGraphQLBundle extends Bundle { diff --git a/Relay/Connection/ConnectionType.php b/Relay/Connection/ConnectionType.php index ec6446211..bffff0302 100644 --- a/Relay/Connection/ConnectionType.php +++ b/Relay/Connection/ConnectionType.php @@ -11,10 +11,10 @@ namespace Overblog\GraphQLBundle\Relay\Connection; -use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Config; -use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\Type; use GraphQL\Utils; use Overblog\GraphQLBundle\Definition\MergeFieldTrait; diff --git a/Relay/Mutation/InputType.php b/Relay/Mutation/InputType.php index c21944f1b..c1bfbe084 100644 --- a/Relay/Mutation/InputType.php +++ b/Relay/Mutation/InputType.php @@ -11,9 +11,9 @@ namespace Overblog\GraphQLBundle\Relay\Mutation; -use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Utils; use Overblog\GraphQLBundle\Definition\MergeFieldTrait; diff --git a/Relay/Mutation/PayloadType.php b/Relay/Mutation/PayloadType.php index 1431100c3..53033841e 100644 --- a/Relay/Mutation/PayloadType.php +++ b/Relay/Mutation/PayloadType.php @@ -11,9 +11,9 @@ namespace Overblog\GraphQLBundle\Relay\Mutation; -use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Utils; use Overblog\GraphQLBundle\Definition\MergeFieldTrait; diff --git a/Relay/Node/GlobalId.php b/Relay/Node/GlobalId.php index e426ffab9..f91b28327 100644 --- a/Relay/Node/GlobalId.php +++ b/Relay/Node/GlobalId.php @@ -24,7 +24,7 @@ public static function fromGlobalId($globalId) { $unBasedGlobalId = base64_decode($globalId); - list($type, $id) = array_merge(explode(static::SEPARATOR, $unBasedGlobalId), array(true)); + list($type, $id) = array_merge(explode(static::SEPARATOR, $unBasedGlobalId), [true]); return [ 'type' => $type, diff --git a/Request/Executor.php b/Request/Executor.php index 61d28960b..85aa84e6c 100644 --- a/Request/Executor.php +++ b/Request/Executor.php @@ -16,9 +16,9 @@ use GraphQL\GraphQL; use GraphQL\Schema; use Overblog\GraphQLBundle\Error\ErrorHandler; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Overblog\GraphQLBundle\Event\Events; use Overblog\GraphQLBundle\Event\ExecutorContextEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class Executor { diff --git a/Resolver/AbstractResolver.php b/Resolver/AbstractResolver.php index 4fdcedfe6..14d4ac294 100644 --- a/Resolver/AbstractResolver.php +++ b/Resolver/AbstractResolver.php @@ -11,8 +11,8 @@ namespace Overblog\GraphQLBundle\Resolver; -use Overblog\GraphQLBundle\Resolver\Cache\CacheInterface; use Overblog\GraphQLBundle\Resolver\Cache\ArrayCache; +use Overblog\GraphQLBundle\Resolver\Cache\CacheInterface; abstract class AbstractResolver implements ResolverInterface { diff --git a/Resolver/ConfigResolver.php b/Resolver/ConfigResolver.php index 52811d175..5beeb80ea 100644 --- a/Resolver/ConfigResolver.php +++ b/Resolver/ConfigResolver.php @@ -13,8 +13,8 @@ use GraphQL\Executor\Executor; use GraphQL\Type\Definition\ResolveInfo; -use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\Definition\FieldInterface; use Overblog\GraphQLBundle\Error\UserError; use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; diff --git a/Tests/Functional/TestCase.php b/Tests/Functional/TestCase.php index ee5e23006..2a6c022ee 100644 --- a/Tests/Functional/TestCase.php +++ b/Tests/Functional/TestCase.php @@ -30,7 +30,7 @@ protected static function getKernelClass() /** * {@inheritdoc} */ - protected static function createKernel(array $options = array()) + protected static function createKernel(array $options = []) { $class = self::getKernelClass(); diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index f5a86d3cb..415f3aebd 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -26,11 +26,11 @@ class AppKernel extends Kernel */ public function registerBundles() { - return array( + return [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new \Symfony\Bundle\TwigBundle\TwigBundle(), new \Overblog\GraphQLBundle\OverblogGraphQLBundle(), - ); + ]; } public function __construct($environment, $debug, $testCase = null) diff --git a/Tests/Resolver/ConfigResolverTest.php b/Tests/Resolver/ConfigResolverTest.php index 909773bbd..82ab4a8d8 100644 --- a/Tests/Resolver/ConfigResolverTest.php +++ b/Tests/Resolver/ConfigResolverTest.php @@ -15,7 +15,6 @@ use Overblog\GraphQLBundle\Relay\Connection\Output\ConnectionBuilder; use Overblog\GraphQLBundle\Resolver\ConfigResolver; use Overblog\GraphQLBundle\Tests\DIContainerMockTrait; -use Symfony\Component\ExpressionLanguage\Expression; class ConfigResolverTest extends \PHPUnit_Framework_TestCase { From e89399c00612c4baa0a843b655c39e26d0a85bb8 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Mon, 14 Mar 2016 15:17:35 +0100 Subject: [PATCH 2/3] split ConfigResolver --- Resolver/AbstractResolver.php | 7 - Resolver/Config/AbstractConfigSolution.php | 115 ++++++ Resolver/Config/ConfigSolutionInterface.php | 16 + Resolver/Config/FieldsConfigSolution.php | 203 +++++++++++ .../Config/ResolveCallbackConfigSolution.php | 32 ++ Resolver/Config/TypeConfigSolution.php | 60 ++++ .../Config/UniqueConfigSolutionInterface.php | 22 ++ Resolver/Config/ValuesConfigSolution.php | 26 ++ Resolver/ConfigResolver.php | 327 +----------------- Resolver/ResolverInterface.php | 8 + Resources/config/services.yml | 12 +- 11 files changed, 506 insertions(+), 322 deletions(-) create mode 100644 Resolver/Config/AbstractConfigSolution.php create mode 100644 Resolver/Config/ConfigSolutionInterface.php create mode 100644 Resolver/Config/FieldsConfigSolution.php create mode 100644 Resolver/Config/ResolveCallbackConfigSolution.php create mode 100644 Resolver/Config/TypeConfigSolution.php create mode 100644 Resolver/Config/UniqueConfigSolutionInterface.php create mode 100644 Resolver/Config/ValuesConfigSolution.php diff --git a/Resolver/AbstractResolver.php b/Resolver/AbstractResolver.php index 14d4ac294..076cf0915 100644 --- a/Resolver/AbstractResolver.php +++ b/Resolver/AbstractResolver.php @@ -78,13 +78,6 @@ public function getSolutionOptions($name) return isset($this->solutionOptions[$name]) ? $this->solutionOptions[$name] : []; } - /** - * @param $input - * - * @return mixed - */ - abstract public function resolve($input); - /** * @param mixed $solution * diff --git a/Resolver/Config/AbstractConfigSolution.php b/Resolver/Config/AbstractConfigSolution.php new file mode 100644 index 000000000..fbd76dc9d --- /dev/null +++ b/Resolver/Config/AbstractConfigSolution.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +use GraphQL\Type\Definition\ResolveInfo; +use Overblog\GraphQLBundle\Definition\Argument; +use Overblog\GraphQLBundle\Resolver\ArgResolver; +use Overblog\GraphQLBundle\Resolver\FieldResolver; +use Overblog\GraphQLBundle\Resolver\TypeResolver; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\OptionsResolver\OptionsResolver; + +abstract class AbstractConfigSolution implements ConfigSolutionInterface +{ + /** + * @var ExpressionLanguage + */ + private $expressionLanguage; + + /** + * @var TypeResolver + */ + private $typeResolver; + + /** + * @var FieldResolver + */ + private $fieldResolver; + + /** + * @var ArgResolver + */ + private $argResolver; + + /** + * @param ExpressionLanguage $expressionLanguage + * @return AbstractConfigSolution + */ + public function setExpressionLanguage($expressionLanguage) + { + $this->expressionLanguage = $expressionLanguage; + return $this; + } + + /** + * @param TypeResolver $typeResolver + * @return AbstractConfigSolution + */ + public function setTypeResolver($typeResolver) + { + $this->typeResolver = $typeResolver; + return $this; + } + + /** + * @param FieldResolver $fieldResolver + * @return AbstractConfigSolution + */ + public function setFieldResolver($fieldResolver) + { + $this->fieldResolver = $fieldResolver; + return $this; + } + + /** + * @param ArgResolver $argResolver + * @return AbstractConfigSolution + */ + public function setArgResolver($argResolver) + { + $this->argResolver = $argResolver; + return $this; + } + + protected function solveUsingExpressionLanguageIfNeeded($expression, array $values = []) + { + if (is_string($expression) && 0 === strpos($expression, '@=')) { + return $this->expressionLanguage->evaluate(substr($expression, 2), $values); + } + + return $expression; + } + + protected function solveResolveCallbackArgs() + { + $args = func_get_args(); + $optionResolver = new OptionsResolver(); + $optionResolver->setDefaults([null, null, null]); + + $args = $optionResolver->resolve($args); + + $arg1IsResolveInfo = $args[1] instanceof ResolveInfo; + + $value = $args[0]; + /** @var ResolveInfo $info */ + $info = $arg1IsResolveInfo ? $args[1] : $args[2]; + /** @var Argument $resolverArgs */ + $resolverArgs = new Argument(!$arg1IsResolveInfo ? $args[1] : []); + + return [ + 'value' => $value, + 'args' => $resolverArgs, + 'info' => $info, + ]; + } +} diff --git a/Resolver/Config/ConfigSolutionInterface.php b/Resolver/Config/ConfigSolutionInterface.php new file mode 100644 index 000000000..f42e1b458 --- /dev/null +++ b/Resolver/Config/ConfigSolutionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +interface ConfigSolutionInterface +{ +} diff --git a/Resolver/Config/FieldsConfigSolution.php b/Resolver/Config/FieldsConfigSolution.php new file mode 100644 index 000000000..b016e0aa7 --- /dev/null +++ b/Resolver/Config/FieldsConfigSolution.php @@ -0,0 +1,203 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Error\UserError; +use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; +use Overblog\GraphQLBundle\Relay\Connection\Output\Edge; + +class FieldsConfigSolution extends AbstractConfigSolution implements UniqueConfigSolutionInterface +{ + /** + * @var TypeConfigSolution + */ + private $typeConfigSolution; + + /** + * @var ResolveCallbackConfigSolution + */ + private $resolveCallbackConfigSolution; + + public function __construct(TypeConfigSolution $typeConfigSolution, ResolveCallbackConfigSolution $resolveCallbackConfigSolution) + { + $this->typeConfigSolution = $typeConfigSolution; + $this->resolveCallbackConfigSolution = $resolveCallbackConfigSolution; + } + + public function solve($values, $config) + { + foreach ($values as $field => &$options) { + if (isset($options['builder']) && is_string($options['builder'])) { + $alias = $options['builder']; + + $fieldBuilder = $this->configResolver->getFieldResolver()->resolve($alias); + $builderConfig = []; + if (isset($options['builderConfig'])) { + if (!is_array($options['builderConfig'])) { + $options['builderConfig'] = [$options['builderConfig']]; + } + $builderConfig = $this->configResolver->resolve($options['builderConfig']); + } + $builderConfig['name'] = $field; + + $access = isset($options['access']) ? $options['access'] : null; + + if ($fieldBuilder instanceof FieldInterface) { + $options = $fieldBuilder->toFieldDefinition($builderConfig); + } elseif (is_callable($fieldBuilder)) { + $options = call_user_func_array($fieldBuilder, [$builderConfig]); + } elseif (is_object($fieldBuilder)) { + $options = get_object_vars($fieldBuilder); + } else { + throw new \RuntimeException(sprintf('Could not build field "%s".', $alias)); + } + + $options['access'] = $access; + $options = $this->resolveResolveAndAccessIfNeeded($options); + + unset($options['builderConfig'], $options['builder']); + + continue; + } + + if (isset($options['type'])) { + $options['type'] = $this->typeConfigSolution->solveTypeCallback($options['type']); + } + + if (isset($options['args'])) { + foreach ($options['args'] as &$argsOptions) { + $argsOptions['type'] = $this->typeConfigSolution->solveTypeCallback($argsOptions['type']); + if (isset($argsOptions['defaultValue'])) { + $argsOptions['defaultValue'] = $this->solveUsingExpressionLanguageIfNeeded($argsOptions['defaultValue']); + } + } + } + + if (isset($options['argsBuilder'])) { + $alias = $options['argsBuilder']['name']; + + $argsBuilder = $this->configResolver->getArgResolver()->resolve($alias); + $argsBuilderConfig = []; + if (isset($options['argsBuilder']['config'])) { + if (!is_array($options['argsBuilder']['config'])) { + $options['argsBuilder']['config'] = [$options['argsBuilder']['config']]; + } + $argsBuilderConfig = $this->configResolver->resolve($options['argsBuilder']['config']); + } + + $options['args'] = isset($options['args']) ? $options['args'] : []; + + if ($argsBuilder instanceof ArgsInterface) { + $options['args'] = array_merge($argsBuilder->toArgsDefinition($argsBuilderConfig), $options['args']); + } elseif (is_callable($argsBuilder)) { + $options['args'] = array_merge(call_user_func_array($argsBuilder, [$argsBuilderConfig]), $options['args']); + } elseif (is_object($argsBuilder)) { + $options['args'] = array_merge(get_object_vars($argsBuilder), $options['args']); + } else { + throw new \RuntimeException(sprintf('Could not build args "%s".', $alias)); + } + + unset($options['argsBuilder']); + } + + $options = $this->resolveResolveAndAccessIfNeeded($options); + + if (isset($options['deprecationReason'])) { + $options['deprecationReason'] = $this->solveUsingExpressionLanguageIfNeeded($options['deprecationReason']); + } + } + + return $values; + } + + private function resolveResolveAndAccessIfNeeded(array $options) + { + $treatedOptions = $options; + + if (isset($treatedOptions['resolve'])) { + $treatedOptions['resolve'] = $this->resolveCallbackConfigSolution->solve($treatedOptions['resolve']); + } + + if (isset($treatedOptions['access'])) { + $resolveCallback = $this->configResolver->getDefaultResolveFn(); + + if (isset($treatedOptions['resolve'])) { + $resolveCallback = $treatedOptions['resolve']; + } + + $treatedOptions['resolve'] = $this->resolveAccessAndWrapResolveCallback($treatedOptions['access'], $resolveCallback); + } + unset($treatedOptions['access']); + + return $treatedOptions; + } + + private function resolveAccessAndWrapResolveCallback($expression, callable $resolveCallback = null) + { + return function () use ($expression, $resolveCallback) { + $args = func_get_args(); + + $result = null !== $resolveCallback ? call_user_func_array($resolveCallback, $args) : null; + + $values = call_user_func_array([$this, 'resolveResolveCallbackArgs'], $args); + + $checkAccess = function ($object, $throwException = false) use ($expression, $values) { + try { + $access = $this->solveUsingExpressionLanguageIfNeeded( + $expression, + array_merge($values, ['object' => $object]) + ); + } catch (\Exception $e) { + $access = false; + } + + if ($throwException && !$access) { + throw new UserError('Access denied to this field.'); + } + + return $access; + }; + + switch (true) { + case is_array($result) || $result instanceof \ArrayAccess: + $result = array_filter( + array_map( + function ($object) use ($checkAccess) { + return $checkAccess($object) ? $object : null; + }, + $result + ) + ); + break; + + case $result instanceof Connection: + $result->edges = array_map( + function (Edge $edge) use ($checkAccess) { + $edge->node = $checkAccess($edge->node) ? $edge->node : null; + + return $edge; + }, + $result->edges + ); + break; + + default: + $checkAccess($result, true); + break; + } + + return $result; + }; + } +} diff --git a/Resolver/Config/ResolveCallbackConfigSolution.php b/Resolver/Config/ResolveCallbackConfigSolution.php new file mode 100644 index 000000000..c26d30435 --- /dev/null +++ b/Resolver/Config/ResolveCallbackConfigSolution.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +class ResolveCallbackConfigSolution extends AbstractConfigSolution implements UniqueConfigSolutionInterface +{ + public function solve($value, $config = null) + { + if (is_callable($value)) { + return $value; + } + + return function () use ($value) { + $args = func_get_args(); + $result = $this->solveUsingExpressionLanguageIfNeeded( + $value, + call_user_func_array([$this, 'solveResolveCallbackArgs'], $args) + ); + + return $result; + }; + } +} diff --git a/Resolver/Config/TypeConfigSolution.php b/Resolver/Config/TypeConfigSolution.php new file mode 100644 index 000000000..c8964275d --- /dev/null +++ b/Resolver/Config/TypeConfigSolution.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Error\UserError; +use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; +use Overblog\GraphQLBundle\Relay\Connection\Output\Edge; + +class TypeConfigSolution extends AbstractConfigSolution +{ + const TYPE_CLASS = 'GraphQL\\Type\\Definition\\Type'; + const INTERFACE_CLASS = 'GraphQL\\Type\\Definition\\InterfaceType'; + + public function solveTypeCallback($values) + { + return function () use ($values) { + return $this->solveType($values); + }; + } + + public function solveType($expr, $parentClass = self::TYPE_CLASS) + { + $type = $this->configResolver->getTypeResolver()->resolve($expr); + + if (class_exists($parentClass) && !$type instanceof $parentClass) { + throw new \InvalidArgumentException( + sprintf('Invalid type! Must be instance of "%s"', $parentClass) + ); + } + + return $type; + } + + public function solveTypes(array $rawTypes, $parentClass = self::TYPE_CLASS) + { + $types = []; + + foreach ($rawTypes as $alias) { + $types[] = $this->solveType($alias, $parentClass); + } + + return $types; + } + + public function solveInterfaces(array $rawInterfaces) + { + return $this->solveTypes($rawInterfaces, self::INTERFACE_CLASS); + } +} diff --git a/Resolver/Config/UniqueConfigSolutionInterface.php b/Resolver/Config/UniqueConfigSolutionInterface.php new file mode 100644 index 000000000..c31a267c3 --- /dev/null +++ b/Resolver/Config/UniqueConfigSolutionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +interface UniqueConfigSolutionInterface extends ConfigSolutionInterface +{ + /** + * @param mixed $values + * @param null|array|\ArrayAccess $config + * @return mixed $value + */ + public function solve($values, $config = null); +} diff --git a/Resolver/Config/ValuesConfigSolution.php b/Resolver/Config/ValuesConfigSolution.php new file mode 100644 index 000000000..9ca9f49cc --- /dev/null +++ b/Resolver/Config/ValuesConfigSolution.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Resolver\Config; + +class ValuesConfigSolution extends AbstractConfigSolution implements UniqueConfigSolutionInterface +{ + public function solve($values, $config = null) + { + foreach ($values as $name => &$options) { + if (isset($options['value'])) { + $options['value'] = $this->solveUsingExpressionLanguageIfNeeded($options['value']); + } + } + + return $values; + } +} diff --git a/Resolver/ConfigResolver.php b/Resolver/ConfigResolver.php index 5beeb80ea..720701e16 100644 --- a/Resolver/ConfigResolver.php +++ b/Resolver/ConfigResolver.php @@ -12,60 +12,16 @@ namespace Overblog\GraphQLBundle\Resolver; use GraphQL\Executor\Executor; -use GraphQL\Type\Definition\ResolveInfo; -use Overblog\GraphQLBundle\Definition\ArgsInterface; -use Overblog\GraphQLBundle\Definition\Argument; -use Overblog\GraphQLBundle\Definition\FieldInterface; -use Overblog\GraphQLBundle\Error\UserError; -use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; -use Overblog\GraphQLBundle\Relay\Connection\Output\Edge; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\OptionsResolver\OptionsResolver; -class ConfigResolver implements ResolverInterface +class ConfigResolver extends AbstractResolver { - /** - * @var ExpressionLanguage - */ - private $expressionLanguage; - - /** - * @var TypeResolver - */ - private $typeResolver; - - /** - * @var FieldResolver - */ - private $fieldResolver; - - /** - * @var ArgResolver - */ - private $argResolver; - /** * @var callable */ private $defaultResolveFn = ['GraphQL\Executor\Executor', 'defaultResolveFn']; - /** - * @var array - * [name => callable] - */ - private $resolverMap = []; - - public function __construct( - ResolverInterface $typeResolver, - ResolverInterface $fieldResolver, - ResolverInterface $argResolver, - ExpressionLanguage $expressionLanguage - ) { - $this->typeResolver = $typeResolver; - $this->fieldResolver = $fieldResolver; - $this->argResolver = $argResolver; - $this->expressionLanguage = $expressionLanguage; - $this->resolverMap = [ + public function __construct() { + $solutionsMapping = [ 'fields' => [$this, 'resolveFields'], 'isTypeOf' => [$this, 'resolveResolveCallback'], 'interfaces' => [$this, 'resolveInterfaces'], @@ -85,6 +41,12 @@ public function __construct( 'payloadType' => [$this, 'resolveTypeCallback'], 'resolveSingleInput' => [$this, 'resolveResolveCallback'], ]; + + foreach ($solutionsMapping as $name => $solution) { + $this->addSolution($name, $solution); + } + + parent::__construct(); } public function setDefaultResolveFn(callable $defaultResolveFn) @@ -94,11 +56,6 @@ public function setDefaultResolveFn(callable $defaultResolveFn) $this->defaultResolveFn = $defaultResolveFn; } - public function addResolverMap($name, callable $resolver) - { - $this->resolverMap[$name] = $resolver; - } - public function resolve($config) { if (!is_array($config) || $config instanceof \ArrayAccess) { @@ -106,276 +63,18 @@ public function resolve($config) } foreach ($config as $name => &$values) { - if (!isset($this->resolverMap[$name]) || empty($values)) { + if ((!$solution = $this->getSolution($name)) || empty($values)) { continue; } - $values = call_user_func_array($this->resolverMap[$name], [$values]); + $values = call_user_func_array($solution, [$values]); } return $config; } - private function resolveFields(array $fields) - { - foreach ($fields as $field => &$options) { - if (isset($options['builder']) && is_string($options['builder'])) { - $alias = $options['builder']; - - $fieldBuilder = $this->fieldResolver->resolve($alias); - $builderConfig = []; - if (isset($options['builderConfig'])) { - if (!is_array($options['builderConfig'])) { - $options['builderConfig'] = [$options['builderConfig']]; - } - $builderConfig = $this->resolve($options['builderConfig']); - } - $builderConfig['name'] = $field; - - $access = isset($options['access']) ? $options['access'] : null; - - if ($fieldBuilder instanceof FieldInterface) { - $options = $fieldBuilder->toFieldDefinition($builderConfig); - } elseif (is_callable($fieldBuilder)) { - $options = call_user_func_array($fieldBuilder, [$builderConfig]); - } elseif (is_object($fieldBuilder)) { - $options = get_object_vars($fieldBuilder); - } else { - throw new \RuntimeException(sprintf('Could not build field "%s".', $alias)); - } - - $options['access'] = $access; - $options = $this->resolveResolveAndAccessIfNeeded($options); - - unset($options['builderConfig'], $options['builder']); - - continue; - } - - if (isset($options['type'])) { - $options['type'] = $this->resolveTypeCallback($options['type']); - } - - if (isset($options['args'])) { - foreach ($options['args'] as &$argsOptions) { - $argsOptions['type'] = $this->resolveTypeCallback($argsOptions['type']); - if (isset($argsOptions['defaultValue'])) { - $argsOptions['defaultValue'] = $this->resolveUsingExpressionLanguageIfNeeded($argsOptions['defaultValue']); - } - } - } - - if (isset($options['argsBuilder'])) { - $alias = $options['argsBuilder']['name']; - - $argsBuilder = $this->argResolver->resolve($alias); - $argsBuilderConfig = []; - if (isset($options['argsBuilder']['config'])) { - if (!is_array($options['argsBuilder']['config'])) { - $options['argsBuilder']['config'] = [$options['argsBuilder']['config']]; - } - $argsBuilderConfig = $this->resolve($options['argsBuilder']['config']); - } - - $options['args'] = isset($options['args']) ? $options['args'] : []; - - if ($argsBuilder instanceof ArgsInterface) { - $options['args'] = array_merge($argsBuilder->toArgsDefinition($argsBuilderConfig), $options['args']); - } elseif (is_callable($argsBuilder)) { - $options['args'] = array_merge(call_user_func_array($argsBuilder, [$argsBuilderConfig]), $options['args']); - } elseif (is_object($argsBuilder)) { - $options['args'] = array_merge(get_object_vars($argsBuilder), $options['args']); - } else { - throw new \RuntimeException(sprintf('Could not build args "%s".', $alias)); - } - - unset($options['argsBuilder']); - } - - $options = $this->resolveResolveAndAccessIfNeeded($options); - - if (isset($options['deprecationReason'])) { - $options['deprecationReason'] = $this->resolveUsingExpressionLanguageIfNeeded($options['deprecationReason']); - } - } - - return $fields; - } - - private function resolveResolveAndAccessIfNeeded(array $options) - { - $treatedOptions = $options; - - if (isset($treatedOptions['resolve'])) { - $treatedOptions['resolve'] = $this->resolveResolveCallback($treatedOptions['resolve']); - } - - if (isset($treatedOptions['access'])) { - $resolveCallback = $this->defaultResolveFn; - - if (isset($treatedOptions['resolve'])) { - $resolveCallback = $treatedOptions['resolve']; - } - - $treatedOptions['resolve'] = $this->resolveAccessAndWrapResolveCallback($treatedOptions['access'], $resolveCallback); - } - unset($treatedOptions['access']); - - return $treatedOptions; - } - - private function resolveTypeCallback($expr) - { - return function () use ($expr) { - return $this->resolveType($expr); - }; - } - - private function resolveInterfaces(array $rawInterfaces) - { - return $this->resolveTypes($rawInterfaces, 'GraphQL\\Type\\Definition\\InterfaceType'); - } - - private function resolveTypes(array $rawTypes, $parentClass = 'GraphQL\\Type\\Definition\\Type') - { - $types = []; - - foreach ($rawTypes as $alias) { - $types[] = $this->resolveType($alias, $parentClass); - } - - return $types; - } - - private function resolveType($expr, $parentClass = 'GraphQL\\Type\\Definition\\Type') - { - $type = $this->typeResolver->resolve($expr); - - if (class_exists($parentClass) && !$type instanceof $parentClass) { - throw new \InvalidArgumentException( - sprintf('Invalid type! Must be instance of "%s"', $parentClass) - ); - } - - return $type; - } - - private function resolveAccessAndWrapResolveCallback($expression, callable $resolveCallback = null) - { - return function () use ($expression, $resolveCallback) { - $args = func_get_args(); - - $result = null !== $resolveCallback ? call_user_func_array($resolveCallback, $args) : null; - - $values = call_user_func_array([$this, 'resolveResolveCallbackArgs'], $args); - - $checkAccess = function ($object, $throwException = false) use ($expression, $values) { - try { - $access = $this->resolveUsingExpressionLanguageIfNeeded( - $expression, - array_merge($values, ['object' => $object]) - ); - } catch (\Exception $e) { - $access = false; - } - - if ($throwException && !$access) { - throw new UserError('Access denied to this field.'); - } - - return $access; - }; - - switch (true) { - case is_array($result) || $result instanceof \ArrayAccess: - $result = array_filter( - array_map( - function ($object) use ($checkAccess) { - return $checkAccess($object) ? $object : null; - }, - $result - ) - ); - break; - - case $result instanceof Connection: - $result->edges = array_map( - function (Edge $edge) use ($checkAccess) { - $edge->node = $checkAccess($edge->node) ? $edge->node : null; - - return $edge; - }, - $result->edges - ); - break; - - default: - $checkAccess($result, true); - break; - } - - return $result; - }; - } - - private function resolveResolveCallback($value) - { - if (is_callable($value)) { - return $value; - } - - return function () use ($value) { - $args = func_get_args(); - $result = $this->resolveUsingExpressionLanguageIfNeeded( - $value, - call_user_func_array([$this, 'resolveResolveCallbackArgs'], $args) - ); - - return $result; - }; - } - - private function resolveResolveCallbackArgs() - { - $args = func_get_args(); - $optionResolver = new OptionsResolver(); - $optionResolver->setDefaults([null, null, null]); - - $args = $optionResolver->resolve($args); - - $arg1IsResolveInfo = $args[1] instanceof ResolveInfo; - - $value = $args[0]; - /** @var ResolveInfo $info */ - $info = $arg1IsResolveInfo ? $args[1] : $args[2]; - /** @var Argument $resolverArgs */ - $resolverArgs = new Argument(!$arg1IsResolveInfo ? $args[1] : []); - - return [ - 'value' => $value, - 'args' => $resolverArgs, - 'info' => $info, - ]; - } - - private function resolveValues(array $rawValues) + protected function supportedSolutionClass() { - $values = $rawValues; - - foreach ($values as $name => &$options) { - if (isset($options['value'])) { - $options['value'] = $this->resolveUsingExpressionLanguageIfNeeded($options['value']); - } - } - - return $values; + return 'Overblog\\GraphQLBundle\\Resolver\\Config\\ConfigSolutionInterface'; } - private function resolveUsingExpressionLanguageIfNeeded($expression, array $values = []) - { - if (is_string($expression) && 0 === strpos($expression, '@=')) { - return $this->expressionLanguage->evaluate(substr($expression, 2), $values); - } - - return $expression; - } } diff --git a/Resolver/ResolverInterface.php b/Resolver/ResolverInterface.php index f3d8b793f..0a7ef6911 100644 --- a/Resolver/ResolverInterface.php +++ b/Resolver/ResolverInterface.php @@ -14,4 +14,12 @@ interface ResolverInterface { public function resolve($input); + + public function addSolution($name, $solution, $extraOptions = []); + + public function getSolutions(); + + public function getSolution($name); + + public function getSolutionOptions($name); } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 047c115d6..029d84c3f 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -43,7 +43,7 @@ services: arguments: - "@overblog_graphql.type_resolver" - "@overblog_graphql.field_resolver" - - "@overblog_graphql.arg_resolver" + - - "@overblog_graphql.expression_language" calls: - ["setDefaultResolveFn", [[Overblog\GraphQLBundle\Resolver\Resolver, defaultResolveFn]]] @@ -96,3 +96,13 @@ services: - "@request_stack" tags: - { name: kernel.event_listener, event: graphql.executor.context, method: onExecutorContextEvent } + + #ConfigSolution + overblog_graphql.abstract_config_solution: + class: Overblog\GraphQLBundle\Resolver\Config\AbstractConfigSolution + abstract: true + calls: + - ["setArgResolver", ["@overblog_graphql.arg_resolver"]] + - ["setTypeResolver", ["@overblog_graphql.type_resolver"]] + - ["setExpressionLanguage", ["@overblog_graphql.expression_language"]] + - ["setFieldResolver", ["@overblog_graphql.field_resolver"]] From acc4110c0ae565719372fd7cd63aeb0c5bd7dbe3 Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Tue, 15 Mar 2016 18:25:21 +0100 Subject: [PATCH 3/3] refactor tests --- Definition/Argument.php | 21 +- .../MappingInterface.php} | 6 +- Definition/Builder/TypeBuilder.php | 2 +- Definition/FieldInterface.php | 22 -- Definition/MergeFieldTrait.php | 2 +- DependencyInjection/TypesConfiguration.php | 4 +- Error/ErrorHandler.php | 2 +- Relay/Connection/BackwardConnectionArgs.php | 6 +- Relay/Connection/ConnectionArgs.php | 6 +- Relay/Connection/ForwardConnectionArgs.php | 6 +- Relay/Mutation/MutationField.php | 6 +- Relay/Node/GlobalIdField.php | 6 +- Relay/Node/NodeField.php | 12 +- Relay/Node/PluralIdentifyingRootField.php | 6 +- Relay/Node/RawIdField.php | 40 --- Request/Executor.php | 31 +-- Resolver/AbstractProxyResolver.php | 17 +- Resolver/AbstractResolver.php | 21 -- Resolver/AbstractSimpleResolver.php | 10 +- Resolver/Cache/ArrayCache.php | 16 -- Resolver/Cache/CacheInterface.php | 14 - Resolver/Config/AbstractConfigSolution.php | 33 ++- Resolver/Config/FieldsConfigSolution.php | 240 ++++++++++-------- Resolver/Config/TypeConfigSolution.php | 10 +- .../Config/UniqueConfigSolutionInterface.php | 6 - Resolver/Config/ValuesConfigSolution.php | 8 +- Resolver/ConfigResolver.php | 39 +-- Resolver/ResolverInterface.php | 2 - Resolver/TypeResolver.php | 12 + Resources/config/graphql_fields.yml | 5 - Resources/config/services.yml | 52 +++- Tests/Definition/ArgumentTest.php | 67 +++++ Tests/Definition/Builder/TypeBuilderTest.php | 73 ++++++ Tests/Definition/MergeFieldTraitTest.php | 71 ++++++ .../Command/GraphDumpSchemaCommandTest.php | 43 ++++ Tests/Functional/Command/schema.yml | 1 + Tests/Resolver/AbstractSimpleResolverTest.php | 4 +- .../Config/AbstractConfigSolutionTest.php | 72 ++++++ .../Config/FieldsConfigSolutionTest.php | 131 ++++++++++ .../Config/TypeConfigSolutionTest.php | 34 +++ .../Config/ValuesConfigSolutionTest.php | 48 ++++ Tests/Resolver/ConfigResolverTest.php | 167 +----------- Tests/Resolver/TypeResolverTest.php | 17 ++ 43 files changed, 841 insertions(+), 550 deletions(-) rename Definition/{ArgsInterface.php => Builder/MappingInterface.php} (70%) delete mode 100644 Definition/FieldInterface.php delete mode 100644 Relay/Node/RawIdField.php create mode 100644 Tests/Definition/ArgumentTest.php create mode 100644 Tests/Definition/Builder/TypeBuilderTest.php create mode 100644 Tests/Definition/MergeFieldTraitTest.php create mode 100644 Tests/Functional/Command/GraphDumpSchemaCommandTest.php create mode 100644 Tests/Functional/Command/schema.yml create mode 100644 Tests/Resolver/Config/AbstractConfigSolutionTest.php create mode 100644 Tests/Resolver/Config/FieldsConfigSolutionTest.php create mode 100644 Tests/Resolver/Config/TypeConfigSolutionTest.php create mode 100644 Tests/Resolver/Config/ValuesConfigSolutionTest.php diff --git a/Definition/Argument.php b/Definition/Argument.php index 9058fb3fe..ed0813721 100644 --- a/Definition/Argument.php +++ b/Definition/Argument.php @@ -14,16 +14,13 @@ class Argument implements \ArrayAccess, \Countable { /** - * @var array|\ArrayAccess + * @var array */ - private $arguments = []; + private $arguments; - public function __construct($arguments) + public function __construct(array $arguments = null) { - if (!is_array($arguments) && !$arguments instanceof \ArrayAccess) { - $arguments = [$arguments]; - } - $this->arguments = $arguments; + $this->arguments = null === $arguments ? [] : $arguments; } public function offsetExists($offset) @@ -46,16 +43,6 @@ public function offsetUnset($offset) unset($this->arguments[$offset]); } - public function __get($key) - { - return $this->offsetGet($key); - } - - public function __set($key, $value) - { - $this->offsetSet($key, $value); - } - public function getRawArguments() { return $this->arguments; diff --git a/Definition/ArgsInterface.php b/Definition/Builder/MappingInterface.php similarity index 70% rename from Definition/ArgsInterface.php rename to Definition/Builder/MappingInterface.php index e80b84683..41077a8aa 100644 --- a/Definition/ArgsInterface.php +++ b/Definition/Builder/MappingInterface.php @@ -9,14 +9,14 @@ * file that was distributed with this source code. */ -namespace Overblog\GraphQLBundle\Definition; +namespace Overblog\GraphQLBundle\Definition\Builder; -interface ArgsInterface +interface MappingInterface { /** * @param array $config * * @return array */ - public function toArgsDefinition(array $config); + public function toMappingDefinition(array $config); } diff --git a/Definition/Builder/TypeBuilder.php b/Definition/Builder/TypeBuilder.php index 89883d994..13c1da74d 100644 --- a/Definition/Builder/TypeBuilder.php +++ b/Definition/Builder/TypeBuilder.php @@ -67,7 +67,7 @@ private function getBaseClassName($type) break; default: - throw new \RuntimeException(sprintf('Type "%s" is not managed.'), $type); + throw new \RuntimeException(sprintf('Type "%s" is not managed.', $type)); } return $class; diff --git a/Definition/FieldInterface.php b/Definition/FieldInterface.php deleted file mode 100644 index e40295201..000000000 --- a/Definition/FieldInterface.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Overblog\GraphQLBundle\Definition; - -interface FieldInterface -{ - /** - * @param array $config - * - * @return array - */ - public function toFieldDefinition(array $config); -} diff --git a/Definition/MergeFieldTrait.php b/Definition/MergeFieldTrait.php index a9ae605bf..b0c97a588 100644 --- a/Definition/MergeFieldTrait.php +++ b/Definition/MergeFieldTrait.php @@ -25,7 +25,7 @@ protected function getFieldsWithDefaults($fields, array $defaultFields, $forceAr } if (!is_array($fields)) { - $fields = [$fields]; + $fields = (array) $fields; } return array_merge($fields, $defaultFields); diff --git a/DependencyInjection/TypesConfiguration.php b/DependencyInjection/TypesConfiguration.php index 3d75457ec..377ec4bfc 100644 --- a/DependencyInjection/TypesConfiguration.php +++ b/DependencyInjection/TypesConfiguration.php @@ -138,10 +138,10 @@ private function addFieldsSelection($name, $enabledBuilder = true) ->info('Use to build dynamic args. Can be combine with args.') ->beforeNormalization() ->ifString() - ->then(function ($v) { return ['name' => $v]; }) + ->then(function ($v) { return ['builder' => $v]; }) ->end() ->children() - ->scalarNode('name') + ->scalarNode('builder') ->info('Service alias tagged with "overblog_graphql.arg"') ->isRequired() ->end() diff --git a/Error/ErrorHandler.php b/Error/ErrorHandler.php index 6edecae8e..58f863450 100644 --- a/Error/ErrorHandler.php +++ b/Error/ErrorHandler.php @@ -37,7 +37,7 @@ public function __construct($internalErrorMessage = null, LoggerInterface $logge /** * @param Error[] $errors - * @param boolean $throwRawException + * @param bool $throwRawException * * @return Error[] * diff --git a/Relay/Connection/BackwardConnectionArgs.php b/Relay/Connection/BackwardConnectionArgs.php index 2991f8991..2fc10da92 100644 --- a/Relay/Connection/BackwardConnectionArgs.php +++ b/Relay/Connection/BackwardConnectionArgs.php @@ -12,16 +12,16 @@ namespace Overblog\GraphQLBundle\Relay\Connection; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class BackwardConnectionArgs implements ArgsInterface +class BackwardConnectionArgs implements MappingInterface { /** * @param array $config * * @return array */ - public function toArgsDefinition(array $config) + public function toMappingDefinition(array $config) { return [ 'before' => [ diff --git a/Relay/Connection/ConnectionArgs.php b/Relay/Connection/ConnectionArgs.php index 645b087a7..789122554 100644 --- a/Relay/Connection/ConnectionArgs.php +++ b/Relay/Connection/ConnectionArgs.php @@ -12,16 +12,16 @@ namespace Overblog\GraphQLBundle\Relay\Connection; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class ConnectionArgs implements ArgsInterface +class ConnectionArgs implements MappingInterface { /** * @param array $config * * @return array */ - public function toArgsDefinition(array $config) + public function toMappingDefinition(array $config) { return [ 'after' => [ diff --git a/Relay/Connection/ForwardConnectionArgs.php b/Relay/Connection/ForwardConnectionArgs.php index 4a9078b75..77f9bf48a 100644 --- a/Relay/Connection/ForwardConnectionArgs.php +++ b/Relay/Connection/ForwardConnectionArgs.php @@ -12,16 +12,16 @@ namespace Overblog\GraphQLBundle\Relay\Connection; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\ArgsInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class ForwardConnectionArgs implements ArgsInterface +class ForwardConnectionArgs implements MappingInterface { /** * @param array $config * * @return array */ - public function toArgsDefinition(array $config) + public function toMappingDefinition(array $config) { return [ 'after' => [ diff --git a/Relay/Mutation/MutationField.php b/Relay/Mutation/MutationField.php index 0311b7cdc..1f37cef09 100644 --- a/Relay/Mutation/MutationField.php +++ b/Relay/Mutation/MutationField.php @@ -14,14 +14,14 @@ use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\Type; use GraphQL\Utils; -use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; use Overblog\GraphQLBundle\Definition\MergeFieldTrait; -class MutationField implements FieldInterface +class MutationField implements MappingInterface { use MergeFieldTrait; - public function toFieldDefinition(array $config) + public function toMappingDefinition(array $config) { Utils::invariant(!empty($config['name']), 'Every type is expected to have name'); diff --git a/Relay/Node/GlobalIdField.php b/Relay/Node/GlobalIdField.php index 9f3b8cb96..aec620936 100644 --- a/Relay/Node/GlobalIdField.php +++ b/Relay/Node/GlobalIdField.php @@ -14,11 +14,11 @@ use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class GlobalIdField implements FieldInterface +class GlobalIdField implements MappingInterface { - public function toFieldDefinition(array $config) + public function toMappingDefinition(array $config) { Config::validate($config, [ 'name' => Config::STRING | Config::REQUIRED, diff --git a/Relay/Node/NodeField.php b/Relay/Node/NodeField.php index aab3daa7b..2f595ece4 100644 --- a/Relay/Node/NodeField.php +++ b/Relay/Node/NodeField.php @@ -13,11 +13,11 @@ use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class NodeField implements FieldInterface +class NodeField implements MappingInterface { - public function toFieldDefinition(array $config) + public function toMappingDefinition(array $config) { Config::validate($config, [ 'name' => Config::STRING | Config::REQUIRED, @@ -37,12 +37,6 @@ public function toFieldDefinition(array $config) 'id' => ['type' => Type::nonNull(Type::id()), 'description' => 'The ID of an object'], ], 'resolve' => function ($obj, $args, $info) use ($idFetcher) { - if (empty($args['id'])) { - throw new \InvalidArgumentException( - 'Argument "id" is required but not provided.' - ); - } - return call_user_func_array($idFetcher, [$args['id'], $info]); }, ]; diff --git a/Relay/Node/PluralIdentifyingRootField.php b/Relay/Node/PluralIdentifyingRootField.php index 516ebbb90..67d1bc09b 100644 --- a/Relay/Node/PluralIdentifyingRootField.php +++ b/Relay/Node/PluralIdentifyingRootField.php @@ -13,11 +13,11 @@ use GraphQL\Type\Definition\Config; use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; -class PluralIdentifyingRootField implements FieldInterface +class PluralIdentifyingRootField implements MappingInterface { - public function toFieldDefinition(array $config) + public function toMappingDefinition(array $config) { Config::validate($config, [ 'name' => Config::STRING, diff --git a/Relay/Node/RawIdField.php b/Relay/Node/RawIdField.php deleted file mode 100644 index 056fc8a1a..000000000 --- a/Relay/Node/RawIdField.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Overblog\GraphQLBundle\Relay\Node; - -use GraphQL\Type\Definition\Config; -use GraphQL\Type\Definition\ResolveInfo; -use GraphQL\Type\Definition\Type; -use Overblog\GraphQLBundle\Definition\FieldInterface; - -class RawIdField implements FieldInterface -{ - public function toFieldDefinition(array $config) - { - Config::validate($config, [ - 'name' => Config::STRING | Config::REQUIRED, - 'idFetcher' => Config::CALLBACK, - ]); - - $name = $config['name']; - $idFetcher = isset($config['idFetcher']) ? $config['idFetcher'] : null; - - return [ - 'name' => $name, - 'description' => 'The raw ID of an object', - 'type' => Type::nonNull(Type::int()), - 'resolve' => function ($obj, $args, ResolveInfo $info) use ($idFetcher) { - return is_callable($idFetcher) ? $idFetcher($obj, $info) : $obj->id; - }, - ]; - } -} diff --git a/Request/Executor.php b/Request/Executor.php index 85aa84e6c..f3760b0e5 100644 --- a/Request/Executor.php +++ b/Request/Executor.php @@ -11,8 +11,6 @@ namespace Overblog\GraphQLBundle\Request; -use GraphQL\Error; -use GraphQL\Executor\ExecutionResult; use GraphQL\GraphQL; use GraphQL\Schema; use Overblog\GraphQLBundle\Error\ErrorHandler; @@ -43,14 +41,6 @@ public function __construct(Schema $schema, EventDispatcherInterface $dispatcher $this->errorHandler = $errorHandler; } - /** - * @return bool - */ - public function getThrowException() - { - return $this->throwException; - } - /** * @param bool $throwException * @@ -68,20 +58,13 @@ public function execute(array $data, array $context = []) $event = new ExecutorContextEvent($context); $this->dispatcher->dispatch(Events::EXECUTOR_CONTEXT, $event); - try { - $executionResult = GraphQL::executeAndReturnResult( - $this->schema, - isset($data['query']) ? $data['query'] : null, - $event->getExecutorContext(), - $data['variables'], - $data['operationName'] - ); - } catch (\Exception $exception) { - $executionResult = new ExecutionResult( - null, - [new Error('An errors occurred while processing query.', null, $exception)] - ); - } + $executionResult = GraphQL::executeAndReturnResult( + $this->schema, + isset($data['query']) ? $data['query'] : null, + $event->getExecutorContext(), + $data['variables'], + $data['operationName'] + ); $this->errorHandler->handleErrors($executionResult, $this->throwException); diff --git a/Resolver/AbstractProxyResolver.php b/Resolver/AbstractProxyResolver.php index 61a63ca00..7d4e30279 100644 --- a/Resolver/AbstractProxyResolver.php +++ b/Resolver/AbstractProxyResolver.php @@ -35,20 +35,15 @@ public function resolve($input) $alias = $input[0]; $funcArgs = $input[1]; - if (null === $func = $this->cache->fetch($alias)) { - $solution = $this->getSolution($alias); + $solution = $this->getSolution($alias); - if (null === $solution) { - throw new UnresolvableException($this->unresolvableMessage($alias)); - } - - $options = $this->getSolutionOptions($alias); - - $func = [$solution, $options['method']]; - - $this->cache->save($alias, $func); + if (null === $solution) { + throw new UnresolvableException($this->unresolvableMessage($alias)); } + $options = $this->getSolutionOptions($alias); + $func = [$solution, $options['method']]; + return call_user_func_array($func, $funcArgs); } diff --git a/Resolver/AbstractResolver.php b/Resolver/AbstractResolver.php index 076cf0915..03fbfa2e6 100644 --- a/Resolver/AbstractResolver.php +++ b/Resolver/AbstractResolver.php @@ -11,9 +11,6 @@ namespace Overblog\GraphQLBundle\Resolver; -use Overblog\GraphQLBundle\Resolver\Cache\ArrayCache; -use Overblog\GraphQLBundle\Resolver\Cache\CacheInterface; - abstract class AbstractResolver implements ResolverInterface { /** @@ -26,16 +23,6 @@ abstract class AbstractResolver implements ResolverInterface */ private $solutionOptions = []; - /** - * @var CacheInterface - */ - protected $cache; - - public function __construct(CacheInterface $cache = null) - { - $this->cache = null !== $cache ? $cache : new ArrayCache(); - } - public function addSolution($name, $solution, $options = []) { if (!$this->supportsSolution($solution)) { @@ -50,14 +37,6 @@ public function addSolution($name, $solution, $options = []) return $this; } - /** - * @return array - */ - public function getSolutions() - { - return $this->solutions; - } - /** * @param $name * diff --git a/Resolver/AbstractSimpleResolver.php b/Resolver/AbstractSimpleResolver.php index 096f37520..7493d5954 100644 --- a/Resolver/AbstractSimpleResolver.php +++ b/Resolver/AbstractSimpleResolver.php @@ -20,18 +20,18 @@ abstract class AbstractSimpleResolver extends AbstractResolver */ public function resolve($alias) { - if (null !== $solution = $this->cache->fetch($alias)) { - return $solution; - } $solution = $this->getSolution($alias); if (null === $solution) { throw new UnresolvableException($this->unresolvableMessage($alias)); } - $this->cache->save($alias, $solution); - return $solution; } + protected function supportedSolutionClass() + { + return 'Overblog\\GraphQLBundle\\Definition\\Builder\\MappingInterface'; + } + abstract protected function unresolvableMessage($alias); } diff --git a/Resolver/Cache/ArrayCache.php b/Resolver/Cache/ArrayCache.php index 62457214b..ca3dc18c4 100644 --- a/Resolver/Cache/ArrayCache.php +++ b/Resolver/Cache/ArrayCache.php @@ -33,20 +33,4 @@ public function save($key, $result) { $this->cache[$key] = $result; } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - unset($this->cache[$key]); - } - - /** - * reset cache. - */ - public function clear() - { - $this->cache = []; - } } diff --git a/Resolver/Cache/CacheInterface.php b/Resolver/Cache/CacheInterface.php index ceb5a17e1..db76c5242 100644 --- a/Resolver/Cache/CacheInterface.php +++ b/Resolver/Cache/CacheInterface.php @@ -29,18 +29,4 @@ public function save($key, $result); * @return mixed|null */ public function fetch($key); - - /** - * Delete an result from the cache. - * - * @param string $key The cache key - * - * @return mixed|null - */ - public function delete($key); - - /** - * reset cache. - */ - public function clear(); } diff --git a/Resolver/Config/AbstractConfigSolution.php b/Resolver/Config/AbstractConfigSolution.php index fbd76dc9d..639e94fcc 100644 --- a/Resolver/Config/AbstractConfigSolution.php +++ b/Resolver/Config/AbstractConfigSolution.php @@ -14,6 +14,7 @@ use GraphQL\Type\Definition\ResolveInfo; use Overblog\GraphQLBundle\Definition\Argument; use Overblog\GraphQLBundle\Resolver\ArgResolver; +use Overblog\GraphQLBundle\Resolver\ConfigResolver; use Overblog\GraphQLBundle\Resolver\FieldResolver; use Overblog\GraphQLBundle\Resolver\TypeResolver; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -24,60 +25,84 @@ abstract class AbstractConfigSolution implements ConfigSolutionInterface /** * @var ExpressionLanguage */ - private $expressionLanguage; + protected $expressionLanguage; /** * @var TypeResolver */ - private $typeResolver; + protected $typeResolver; /** * @var FieldResolver */ - private $fieldResolver; + protected $fieldResolver; /** * @var ArgResolver */ - private $argResolver; + protected $argResolver; + /** + * @var ConfigResolver + */ + protected $configResolver; /** * @param ExpressionLanguage $expressionLanguage + * * @return AbstractConfigSolution */ public function setExpressionLanguage($expressionLanguage) { $this->expressionLanguage = $expressionLanguage; + return $this; } /** * @param TypeResolver $typeResolver + * * @return AbstractConfigSolution */ public function setTypeResolver($typeResolver) { $this->typeResolver = $typeResolver; + return $this; } /** * @param FieldResolver $fieldResolver + * * @return AbstractConfigSolution */ public function setFieldResolver($fieldResolver) { $this->fieldResolver = $fieldResolver; + return $this; } /** * @param ArgResolver $argResolver + * * @return AbstractConfigSolution */ public function setArgResolver($argResolver) { $this->argResolver = $argResolver; + + return $this; + } + + /** + * @param ConfigResolver $configResolver + * + * @return AbstractConfigSolution + */ + public function setConfigResolver($configResolver) + { + $this->configResolver = $configResolver; + return $this; } diff --git a/Resolver/Config/FieldsConfigSolution.php b/Resolver/Config/FieldsConfigSolution.php index b016e0aa7..3191042c5 100644 --- a/Resolver/Config/FieldsConfigSolution.php +++ b/Resolver/Config/FieldsConfigSolution.php @@ -11,11 +11,11 @@ namespace Overblog\GraphQLBundle\Resolver\Config; -use Overblog\GraphQLBundle\Definition\ArgsInterface; -use Overblog\GraphQLBundle\Definition\FieldInterface; +use Overblog\GraphQLBundle\Definition\Builder\MappingInterface; use Overblog\GraphQLBundle\Error\UserError; use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; use Overblog\GraphQLBundle\Relay\Connection\Output\Edge; +use Overblog\GraphQLBundle\Resolver\ResolverInterface; class FieldsConfigSolution extends AbstractConfigSolution implements UniqueConfigSolutionInterface { @@ -29,96 +29,102 @@ class FieldsConfigSolution extends AbstractConfigSolution implements UniqueConfi */ private $resolveCallbackConfigSolution; - public function __construct(TypeConfigSolution $typeConfigSolution, ResolveCallbackConfigSolution $resolveCallbackConfigSolution) - { + public function __construct( + TypeConfigSolution $typeConfigSolution, + ResolveCallbackConfigSolution $resolveCallbackConfigSolution + ) { $this->typeConfigSolution = $typeConfigSolution; $this->resolveCallbackConfigSolution = $resolveCallbackConfigSolution; } - public function solve($values, $config) + public function solve($values, $config = null) { + // builder must be last + $fieldsTreated = ['type', 'args', 'argsBuilder', 'deprecationReason', 'builder']; + foreach ($values as $field => &$options) { - if (isset($options['builder']) && is_string($options['builder'])) { - $alias = $options['builder']; - - $fieldBuilder = $this->configResolver->getFieldResolver()->resolve($alias); - $builderConfig = []; - if (isset($options['builderConfig'])) { - if (!is_array($options['builderConfig'])) { - $options['builderConfig'] = [$options['builderConfig']]; - } - $builderConfig = $this->configResolver->resolve($options['builderConfig']); - } - $builderConfig['name'] = $field; - - $access = isset($options['access']) ? $options['access'] : null; - - if ($fieldBuilder instanceof FieldInterface) { - $options = $fieldBuilder->toFieldDefinition($builderConfig); - } elseif (is_callable($fieldBuilder)) { - $options = call_user_func_array($fieldBuilder, [$builderConfig]); - } elseif (is_object($fieldBuilder)) { - $options = get_object_vars($fieldBuilder); - } else { - throw new \RuntimeException(sprintf('Could not build field "%s".', $alias)); + foreach ($fieldsTreated as $fieldTreated) { + if (isset($options[$fieldTreated])) { + $method = 'solve'.ucfirst($fieldTreated); + $options = $this->$method($options, $field); } + } - $options['access'] = $access; - $options = $this->resolveResolveAndAccessIfNeeded($options); + $options = $this->resolveResolveAndAccessIfNeeded($options); + } - unset($options['builderConfig'], $options['builder']); + return $values; + } - continue; - } + private function solveBuilder($options, $field) + { + $builderConfig = isset($options['builderConfig']) ? $options['builderConfig'] : []; - if (isset($options['type'])) { - $options['type'] = $this->typeConfigSolution->solveTypeCallback($options['type']); - } + $access = isset($options['access']) ? $options['access'] : null; + $options = $this->builderToMappingDefinition($options['builder'], $builderConfig, $this->fieldResolver, $field); + $options['access'] = $access; + $options = $this->resolveResolveAndAccessIfNeeded($options); - if (isset($options['args'])) { - foreach ($options['args'] as &$argsOptions) { - $argsOptions['type'] = $this->typeConfigSolution->solveTypeCallback($argsOptions['type']); - if (isset($argsOptions['defaultValue'])) { - $argsOptions['defaultValue'] = $this->solveUsingExpressionLanguageIfNeeded($argsOptions['defaultValue']); - } - } + unset($options['builderConfig'], $options['builder']); + + return $options; + } + + private function solveType($options) + { + $options['type'] = $this->typeConfigSolution->solveTypeCallback($options['type']); + + return $options; + } + + private function solveArgs($options) + { + foreach ($options['args'] as &$argsOptions) { + $argsOptions['type'] = $this->typeConfigSolution->solveTypeCallback($argsOptions['type']); + if (isset($argsOptions['defaultValue'])) { + $argsOptions['defaultValue'] = $this->solveUsingExpressionLanguageIfNeeded($argsOptions['defaultValue']); } + } - if (isset($options['argsBuilder'])) { - $alias = $options['argsBuilder']['name']; + return $options; + } - $argsBuilder = $this->configResolver->getArgResolver()->resolve($alias); - $argsBuilderConfig = []; - if (isset($options['argsBuilder']['config'])) { - if (!is_array($options['argsBuilder']['config'])) { - $options['argsBuilder']['config'] = [$options['argsBuilder']['config']]; - } - $argsBuilderConfig = $this->configResolver->resolve($options['argsBuilder']['config']); - } + private function solveArgsBuilder($options) + { + $argsBuilderConfig = isset($options['argsBuilder']['config']) ? $options['argsBuilder']['config'] : []; - $options['args'] = isset($options['args']) ? $options['args'] : []; + $options['args'] = array_merge( + $this->builderToMappingDefinition($options['argsBuilder']['builder'], $argsBuilderConfig, $this->argResolver), + isset($options['args']) ? $options['args'] : [] + ); - if ($argsBuilder instanceof ArgsInterface) { - $options['args'] = array_merge($argsBuilder->toArgsDefinition($argsBuilderConfig), $options['args']); - } elseif (is_callable($argsBuilder)) { - $options['args'] = array_merge(call_user_func_array($argsBuilder, [$argsBuilderConfig]), $options['args']); - } elseif (is_object($argsBuilder)) { - $options['args'] = array_merge(get_object_vars($argsBuilder), $options['args']); - } else { - throw new \RuntimeException(sprintf('Could not build args "%s".', $alias)); - } + unset($options['argsBuilder']); - unset($options['argsBuilder']); - } + return $options; + } - $options = $this->resolveResolveAndAccessIfNeeded($options); + private function solveDeprecationReason($options) + { + $options['deprecationReason'] = $this->solveUsingExpressionLanguageIfNeeded($options['deprecationReason']); - if (isset($options['deprecationReason'])) { - $options['deprecationReason'] = $this->solveUsingExpressionLanguageIfNeeded($options['deprecationReason']); - } + return $options; + } + + private function builderToMappingDefinition($rawBuilder, array $rawBuilderConfig, ResolverInterface $builderResolver, $name = null) + { + /** @var MappingInterface $builder */ + $builder = $builderResolver->resolve($rawBuilder); + $builderConfig = []; + if (!empty($rawBuilderConfig)) { + $builderConfig = $rawBuilderConfig; + $builderConfig = $this->configResolver->resolve($builderConfig); } - return $values; + if (null !== $name) { + $builderConfig['name'] = $name; + } + + return $builder->toMappingDefinition($builderConfig); } private function resolveResolveAndAccessIfNeeded(array $options) @@ -150,54 +156,64 @@ private function resolveAccessAndWrapResolveCallback($expression, callable $reso $result = null !== $resolveCallback ? call_user_func_array($resolveCallback, $args) : null; - $values = call_user_func_array([$this, 'resolveResolveCallbackArgs'], $args); + $values = call_user_func_array([$this, 'solveResolveCallbackArgs'], $args); - $checkAccess = function ($object, $throwException = false) use ($expression, $values) { - try { - $access = $this->solveUsingExpressionLanguageIfNeeded( - $expression, - array_merge($values, ['object' => $object]) - ); - } catch (\Exception $e) { - $access = false; - } - - if ($throwException && !$access) { - throw new UserError('Access denied to this field.'); - } + return $this->filterResultUsingAccess($result, $expression, $values); + }; + } - return $access; - }; - - switch (true) { - case is_array($result) || $result instanceof \ArrayAccess: - $result = array_filter( - array_map( - function ($object) use ($checkAccess) { - return $checkAccess($object) ? $object : null; - }, - $result - ) - ); - break; - - case $result instanceof Connection: - $result->edges = array_map( - function (Edge $edge) use ($checkAccess) { - $edge->node = $checkAccess($edge->node) ? $edge->node : null; - - return $edge; + private function filterResultUsingAccess($result, $expression, $values) + { + $checkAccess = $this->checkAccessCallback($expression, $values); + + switch (true) { + case is_array($result) || $result instanceof \ArrayAccess: + $result = array_filter( + array_map( + function ($object) use ($checkAccess) { + return $checkAccess($object) ? $object : null; }, - $result->edges - ); - break; + $result + ) + ); + break; + + case $result instanceof Connection: + $result->edges = array_map( + function (Edge $edge) use ($checkAccess) { + $edge->node = $checkAccess($edge->node) ? $edge->node : null; + + return $edge; + }, + $result->edges + ); + break; + + default: + $checkAccess($result, true); + break; + } + + return $result; + } + + private function checkAccessCallback($expression, $values) + { + return function ($object, $throwException = false) use ($expression, $values) { + try { + $access = $this->solveUsingExpressionLanguageIfNeeded( + $expression, + array_merge($values, ['object' => $object]) + ); + } catch (\Exception $e) { + $access = false; + } - default: - $checkAccess($result, true); - break; + if ($throwException && !$access) { + throw new UserError('Access denied to this field.'); } - return $result; + return $access; }; } } diff --git a/Resolver/Config/TypeConfigSolution.php b/Resolver/Config/TypeConfigSolution.php index c8964275d..65250189c 100644 --- a/Resolver/Config/TypeConfigSolution.php +++ b/Resolver/Config/TypeConfigSolution.php @@ -11,12 +11,6 @@ namespace Overblog\GraphQLBundle\Resolver\Config; -use Overblog\GraphQLBundle\Definition\ArgsInterface; -use Overblog\GraphQLBundle\Definition\FieldInterface; -use Overblog\GraphQLBundle\Error\UserError; -use Overblog\GraphQLBundle\Relay\Connection\Output\Connection; -use Overblog\GraphQLBundle\Relay\Connection\Output\Edge; - class TypeConfigSolution extends AbstractConfigSolution { const TYPE_CLASS = 'GraphQL\\Type\\Definition\\Type'; @@ -31,9 +25,9 @@ public function solveTypeCallback($values) public function solveType($expr, $parentClass = self::TYPE_CLASS) { - $type = $this->configResolver->getTypeResolver()->resolve($expr); + $type = $this->typeResolver->resolve($expr); - if (class_exists($parentClass) && !$type instanceof $parentClass) { + if (null !== $parentClass && !$type instanceof $parentClass) { throw new \InvalidArgumentException( sprintf('Invalid type! Must be instance of "%s"', $parentClass) ); diff --git a/Resolver/Config/UniqueConfigSolutionInterface.php b/Resolver/Config/UniqueConfigSolutionInterface.php index c31a267c3..865dd212e 100644 --- a/Resolver/Config/UniqueConfigSolutionInterface.php +++ b/Resolver/Config/UniqueConfigSolutionInterface.php @@ -13,10 +13,4 @@ interface UniqueConfigSolutionInterface extends ConfigSolutionInterface { - /** - * @param mixed $values - * @param null|array|\ArrayAccess $config - * @return mixed $value - */ - public function solve($values, $config = null); } diff --git a/Resolver/Config/ValuesConfigSolution.php b/Resolver/Config/ValuesConfigSolution.php index 9ca9f49cc..871d786c0 100644 --- a/Resolver/Config/ValuesConfigSolution.php +++ b/Resolver/Config/ValuesConfigSolution.php @@ -15,9 +15,11 @@ class ValuesConfigSolution extends AbstractConfigSolution implements UniqueConfi { public function solve($values, $config = null) { - foreach ($values as $name => &$options) { - if (isset($options['value'])) { - $options['value'] = $this->solveUsingExpressionLanguageIfNeeded($options['value']); + if (!empty($values['values'])) { + foreach ($values['values'] as $name => &$options) { + if (isset($options['value'])) { + $options['value'] = $this->solveUsingExpressionLanguageIfNeeded($options['value']); + } } } diff --git a/Resolver/ConfigResolver.php b/Resolver/ConfigResolver.php index 720701e16..3f5f6341e 100644 --- a/Resolver/ConfigResolver.php +++ b/Resolver/ConfigResolver.php @@ -20,35 +20,6 @@ class ConfigResolver extends AbstractResolver */ private $defaultResolveFn = ['GraphQL\Executor\Executor', 'defaultResolveFn']; - public function __construct() { - $solutionsMapping = [ - 'fields' => [$this, 'resolveFields'], - 'isTypeOf' => [$this, 'resolveResolveCallback'], - 'interfaces' => [$this, 'resolveInterfaces'], - 'types' => [$this, 'resolveTypes'], - 'values' => [$this, 'resolveValues'], - 'resolveType' => [$this, 'resolveResolveCallback'], - 'resolveCursor' => [$this, 'resolveResolveCallback'], - 'resolveNode' => [$this, 'resolveResolveCallback'], - 'nodeType' => [$this, 'resolveTypeCallback'], - 'connectionFields' => [$this, 'resolveFields'], - 'edgeFields' => [$this, 'resolveFields'], - 'mutateAndGetPayload' => [$this, 'resolveResolveCallback'], - 'idFetcher' => [$this, 'resolveResolveCallback'], - 'nodeInterfaceType' => [$this, 'resolveTypeCallback'], - 'inputType' => [$this, 'resolveTypeCallback'], - 'outputType' => [$this, 'resolveTypeCallback'], - 'payloadType' => [$this, 'resolveTypeCallback'], - 'resolveSingleInput' => [$this, 'resolveResolveCallback'], - ]; - - foreach ($solutionsMapping as $name => $solution) { - $this->addSolution($name, $solution); - } - - parent::__construct(); - } - public function setDefaultResolveFn(callable $defaultResolveFn) { Executor::setDefaultResolveFn($defaultResolveFn); @@ -56,6 +27,11 @@ public function setDefaultResolveFn(callable $defaultResolveFn) $this->defaultResolveFn = $defaultResolveFn; } + public function getDefaultResolveFn() + { + return $this->defaultResolveFn; + } + public function resolve($config) { if (!is_array($config) || $config instanceof \ArrayAccess) { @@ -66,7 +42,9 @@ public function resolve($config) if ((!$solution = $this->getSolution($name)) || empty($values)) { continue; } - $values = call_user_func_array($solution, [$values]); + $options = $this->getSolutionOptions($name); + + $values = call_user_func_array([$solution, $options['method']], [$values]); } return $config; @@ -76,5 +54,4 @@ protected function supportedSolutionClass() { return 'Overblog\\GraphQLBundle\\Resolver\\Config\\ConfigSolutionInterface'; } - } diff --git a/Resolver/ResolverInterface.php b/Resolver/ResolverInterface.php index 0a7ef6911..64cbe79b0 100644 --- a/Resolver/ResolverInterface.php +++ b/Resolver/ResolverInterface.php @@ -17,8 +17,6 @@ public function resolve($input); public function addSolution($name, $solution, $extraOptions = []); - public function getSolutions(); - public function getSolution($name); public function getSolutionOptions($name); diff --git a/Resolver/TypeResolver.php b/Resolver/TypeResolver.php index 3d1ef23fb..11bbf5d57 100644 --- a/Resolver/TypeResolver.php +++ b/Resolver/TypeResolver.php @@ -12,9 +12,21 @@ namespace Overblog\GraphQLBundle\Resolver; use GraphQL\Type\Definition\Type; +use Overblog\GraphQLBundle\Resolver\Cache\ArrayCache; +use Overblog\GraphQLBundle\Resolver\Cache\CacheInterface; class TypeResolver extends AbstractResolver { + /** + * @var CacheInterface + */ + protected $cache; + + public function __construct(CacheInterface $cache = null) + { + $this->cache = null !== $cache ? $cache : new ArrayCache(); + } + /** * @param string $alias * diff --git a/Resources/config/graphql_fields.yml b/Resources/config/graphql_fields.yml index 67f6cfd59..5fde2396b 100644 --- a/Resources/config/graphql_fields.yml +++ b/Resources/config/graphql_fields.yml @@ -10,11 +10,6 @@ services: tags: - { name: overblog_graphql.field, alias: GlobalId } - overblog_graphql.definition.relay_node_raw_id_field: - class: Overblog\GraphQLBundle\Relay\Node\RawIdField - tags: - - { name: overblog_graphql.field, alias: RawId } - overblog_graphql.definition.relay_node_node_field: class: Overblog\GraphQLBundle\Relay\Node\NodeField tags: diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 029d84c3f..581c290e0 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -1,3 +1,26 @@ +parameters: + overblog_graphql.default_resolver: [Overblog\GraphQLBundle\Resolver\Resolver, defaultResolveFn] + + overblog_graphql.configs_mapping: + fields: {id: "overblog_graphql.fields_config_solution", method: "solve"} + isTypeOf: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + interfaces: {id: "overblog_graphql.type_config_solution", method: "solveInterfaces"} + types: {id: "overblog_graphql.type_config_solution", method: "solveTypes"} + values: {id: "overblog_graphql.values_config_solution", method: "solve"} + resolveType: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + resolveCursor: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + resolveNode: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + nodeType: {id: "overblog_graphql.type_config_solution", method: "solveTypeCallback"} + connectionFields: {id: "overblog_graphql.fields_config_solution", method: "solve"} + edgeFields: {id: "overblog_graphql.fields_config_solution", method: "solve"} + mutateAndGetPayload: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + idFetcher: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + nodeInterfaceType: {id: "overblog_graphql.type_config_solution", method: "solveTypeCallback"} + inputType: {id: "overblog_graphql.type_config_solution", method: "solveTypeCallback"} + outputType: {id: "overblog_graphql.type_config_solution", method: "solveTypeCallback"} + payloadType: {id: "overblog_graphql.type_config_solution", method: "solveTypeCallback"} + resolveSingleInput: {id: "overblog_graphql.resolve_callback_config_solution", method: "solve"} + services: overblog_graphql.error_handler: class: Overblog\GraphQLBundle\Error\ErrorHandler @@ -40,13 +63,9 @@ services: overblog_graphql.config_resolver: class: Overblog\GraphQLBundle\Resolver\ConfigResolver - arguments: - - "@overblog_graphql.type_resolver" - - "@overblog_graphql.field_resolver" - - - - "@overblog_graphql.expression_language" + configurator: ["@overblog_graphql.resolver_configurator", "configureConfig"] calls: - - ["setDefaultResolveFn", [[Overblog\GraphQLBundle\Resolver\Resolver, defaultResolveFn]]] + - ["setDefaultResolveFn", [%overblog_graphql.default_resolver%]] overblog_graphql.type_resolver: class: Overblog\GraphQLBundle\Resolver\TypeResolver @@ -77,6 +96,7 @@ services: - ["addMapping", ["arg", %overblog_graphql.args_mapping%]] - ["addMapping", ["resolver", %overblog_graphql.resolvers_mapping%]] - ["addMapping", ["mutation", %overblog_graphql.mutations_mapping%]] + - ["addMapping", ["config", %overblog_graphql.configs_mapping%]] overblog_graphql.cache_expression_language_parser.default: class: Symfony\Component\ExpressionLanguage\ParserCache\ArrayParserCache @@ -106,3 +126,23 @@ services: - ["setTypeResolver", ["@overblog_graphql.type_resolver"]] - ["setExpressionLanguage", ["@overblog_graphql.expression_language"]] - ["setFieldResolver", ["@overblog_graphql.field_resolver"]] + - ["setConfigResolver", ["@overblog_graphql.config_resolver"]] + + overblog_graphql.fields_config_solution: + class: Overblog\GraphQLBundle\Resolver\Config\FieldsConfigSolution + parent: overblog_graphql.abstract_config_solution + arguments: + - "@overblog_graphql.type_config_solution" + - "@overblog_graphql.resolve_callback_config_solution" + + overblog_graphql.type_config_solution: + class: Overblog\GraphQLBundle\Resolver\Config\TypeConfigSolution + parent: overblog_graphql.abstract_config_solution + + overblog_graphql.resolve_callback_config_solution: + class: Overblog\GraphQLBundle\Resolver\Config\ResolveCallbackConfigSolution + parent: overblog_graphql.abstract_config_solution + + overblog_graphql.values_config_solution: + class: Overblog\GraphQLBundle\Resolver\Config\ValuesConfigSolution + parent: overblog_graphql.abstract_config_solution diff --git a/Tests/Definition/ArgumentTest.php b/Tests/Definition/ArgumentTest.php new file mode 100644 index 000000000..17c815155 --- /dev/null +++ b/Tests/Definition/ArgumentTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Definition; + +use Overblog\GraphQLBundle\Definition\Argument; + +class ArgumentTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var array + */ + private $rawArgs; + + /** + * @var Argument + */ + private $argument; + + public function setUp() + { + $this->rawArgs = ['toto' => 'tata']; + + $this->argument = new Argument($this->rawArgs); + } + + public function testOffsetGet() + { + $this->assertEquals($this->argument['toto'], 'tata'); + $this->assertNull($this->argument['fake']); + } + + public function testOffsetSet() + { + $this->argument['foo'] = 'bar'; + $this->assertEquals($this->argument['foo'], 'bar'); + } + + public function testOffsetExists() + { + unset($this->argument['toto']); + $this->assertNull($this->argument['toto']); + } + + public function testOffsetUnset() + { + $this->assertTrue(isset($this->argument['toto'])); + } + + public function testCount() + { + $this->assertCount(1, $this->argument); + } + + public function testGetRawArgs() + { + $this->assertEquals($this->rawArgs, $this->argument->getRawArguments()); + } +} diff --git a/Tests/Definition/Builder/TypeBuilderTest.php b/Tests/Definition/Builder/TypeBuilderTest.php new file mode 100644 index 000000000..a32daa63a --- /dev/null +++ b/Tests/Definition/Builder/TypeBuilderTest.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Definition\Builder; + +use GraphQL\Type\Definition\ObjectType; +use Overblog\GraphQLBundle\Definition\Builder\TypeBuilder; +use Overblog\GraphQLBundle\Resolver\ConfigResolver; + +class TypeBuilderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var TypeBuilder + */ + private $typeBuilder; + + public function setUp() + { + $this->typeBuilder = new TypeBuilder(new ConfigResolver()); + } + + /** + * @param $type + * @param array $config + * @param $expectedInstanceOf + * + * @dataProvider getCreateDataProvider + */ + public function testCreate($type, array $config, $expectedInstanceOf) + { + $type = $this->typeBuilder->create($type, $config); + + $this->assertInstanceOf($expectedInstanceOf, $type); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Type "toto" is not managed. + */ + public function testCreateInvalidType() + { + $this->typeBuilder->create('toto', []); + } + + public function getCreateDataProvider() + { + return [ + [ + 'object', ['name' => 'object'], 'GraphQL\\Type\\Definition\\ObjectType', + ], + [ + 'enum', ['name' => 'enum'], 'GraphQL\\Type\\Definition\\EnumType', + ], + [ + 'interface', ['name' => 'interface'], 'GraphQL\\Type\\Definition\\InterfaceType', + ], + [ + 'union', ['name' => 'union', 'types' => [new ObjectType(['name' => 'toto'])]], 'GraphQL\\Type\\Definition\\UnionType', + ], + [ + 'input-object', ['name' => 'input'], 'GraphQL\\Type\\Definition\\InputObjectType', + ], + ]; + } +} diff --git a/Tests/Definition/MergeFieldTraitTest.php b/Tests/Definition/MergeFieldTraitTest.php new file mode 100644 index 000000000..0c961923c --- /dev/null +++ b/Tests/Definition/MergeFieldTraitTest.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Definition; + +use Overblog\GraphQLBundle\Definition\MergeFieldTrait; + +class MergeFieldTraitTest extends \PHPUnit_Framework_TestCase +{ + use MergeFieldTrait; + + /** + * @param $fields + * @param array $defaultFields + * @param $forceArray + * @param $expectedFields + * + * @dataProvider getFieldsDataProvider + */ + public function testGetFieldsWithDefaults($fields, array $defaultFields, $forceArray, $expectedFields) + { + $this->assertEquals($expectedFields, $this->getFieldsWithDefaults($fields, $defaultFields, $forceArray)); + } + + public function getFieldsDataProvider() + { + return [ + [ + ['toto', 'tata', 'titi'], + ['foo', 'bar'], + true, + ['toto', 'tata', 'titi', 'foo', 'bar'], + ], + [ + [], + ['foo'], + true, + ['foo'], + ], + [ + function () { + return ['test']; + }, + ['bar'], + true, + ['test', 'bar'], + ], + [ + 'toto', + ['tata'], + true, + ['toto', 'tata'], + ], + ]; + } + + public function testGetFieldsWithDefaultsForceArray() + { + $fields = $this->getFieldsWithDefaults(function () { return ['bar']; }, ['toto'], false); + $this->assertInstanceOf('Closure', $fields); + $this->assertEquals(['bar', 'toto'], $fields()); + } +} diff --git a/Tests/Functional/Command/GraphDumpSchemaCommandTest.php b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php new file mode 100644 index 000000000..995821e44 --- /dev/null +++ b/Tests/Functional/Command/GraphDumpSchemaCommandTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Functional\Command; + +use Overblog\GraphQLBundle\Command\GraphDumpSchemaCommand; +use Overblog\GraphQLBundle\Tests\Functional\TestCase; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; + +class GraphDumpSchemaCommandTest extends TestCase +{ + public function testExecute() + { + $client = static::createClient(['test_case' => 'connection']); + $kernel = $client->getKernel(); + + $application = new Application($kernel); + $application->add(new GraphDumpSchemaCommand()); + + $command = $application->find('graph:dump-schema'); + $file = $kernel->getCacheDir().'/schema.yml'; + + $commandTester = new CommandTester($command); + $commandTester->execute( + [ + 'command' => $command->getName(), + '--file' => $file, + ] + ); + + $this->assertEquals(0, $commandTester->getStatusCode()); + $this->assertEquals(file_get_contents(__DIR__.'/schema.yml'), file_get_contents($file)); + } +} diff --git a/Tests/Functional/Command/schema.yml b/Tests/Functional/Command/schema.yml new file mode 100644 index 000000000..47d68938e --- /dev/null +++ b/Tests/Functional/Command/schema.yml @@ -0,0 +1 @@ +{"__schema":{"queryType":{"name":"Query"},"mutationType":null,"types":[{"kind":"OBJECT","name":"Query","fields":[{"name":"user","args":[],"type":{"kind":"OBJECT","name":"User","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"User","fields":[{"name":"name","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"friends","args":[{"name":"after","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null},{"name":"before","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"last","type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"friendConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"friendsForward","args":[{"name":"after","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"first","type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"userConnection","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"friendsBackward","args":[{"name":"before","type":{"kind":"SCALAR","name":"String","ofType":null},"defaultValue":null},{"name":"last","type":{"kind":"SCALAR","name":"Int","ofType":null},"defaultValue":null}],"type":{"kind":"OBJECT","name":"userConnection","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"String","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Int","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"friendConnection","fields":[{"name":"totalCount","args":[],"type":{"kind":"SCALAR","name":"Int","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"pageInfo","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"friendEdge","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"PageInfo","fields":[{"name":"hasNextPage","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"hasPreviousPage","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"startCursor","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"endCursor","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Boolean","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"friendEdge","fields":[{"name":"friendshipTime","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"node","args":[],"type":{"kind":"OBJECT","name":"User","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"userConnection","fields":[{"name":"pageInfo","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"PageInfo","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"edges","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"userEdge","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"userEdge","fields":[{"name":"node","args":[],"type":{"kind":"OBJECT","name":"User","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"cursor","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Schema","fields":[{"name":"types","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type"}}}},"isDeprecated":false,"deprecationReason":null},{"name":"queryType","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"mutationType","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"subscriptionType","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"directives","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Directive"}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Type","fields":[{"name":"kind","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"__TypeKind","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"name","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"description","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"fields","args":[{"name":"includeDeprecated","type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Field","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"interfaces","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"possibleTypes","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"enumValues","args":[{"name":"includeDeprecated","type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":"false"}],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__EnumValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"inputFields","args":[],"type":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue","ofType":null}}},"isDeprecated":false,"deprecationReason":null},{"name":"ofType","args":[],"type":{"kind":"OBJECT","name":"__Type","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"__TypeKind","fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"SCALAR","isDeprecated":false,"deprecationReason":null},{"name":"OBJECT","isDeprecated":false,"deprecationReason":null},{"name":"INTERFACE","isDeprecated":false,"deprecationReason":null},{"name":"UNION","isDeprecated":false,"deprecationReason":null},{"name":"ENUM","isDeprecated":false,"deprecationReason":null},{"name":"INPUT_OBJECT","isDeprecated":false,"deprecationReason":null},{"name":"LIST","isDeprecated":false,"deprecationReason":null},{"name":"NON_NULL","isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"__Field","fields":[{"name":"name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue"}}}},"isDeprecated":false,"deprecationReason":null},{"name":"type","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__InputValue","fields":[{"name":"name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"type","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__Type","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"defaultValue","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__EnumValue","fields":[{"name":"name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"isDeprecated","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"deprecationReason","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"__Directive","fields":[{"name":"name","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"description","args":[],"type":{"kind":"SCALAR","name":"String","ofType":null},"isDeprecated":false,"deprecationReason":null},{"name":"args","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"__InputValue"}}}},"isDeprecated":false,"deprecationReason":null},{"name":"onOperation","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"onFragment","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"onField","args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"ID","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"Float","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"include","args":[{"name":"if","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}],"onOperation":false,"onFragment":true,"onField":true},{"name":"skip","args":[{"name":"if","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}],"onOperation":false,"onFragment":true,"onField":true}]}} \ No newline at end of file diff --git a/Tests/Resolver/AbstractSimpleResolverTest.php b/Tests/Resolver/AbstractSimpleResolverTest.php index d3236c64b..5e7fe081e 100644 --- a/Tests/Resolver/AbstractSimpleResolverTest.php +++ b/Tests/Resolver/AbstractSimpleResolverTest.php @@ -15,7 +15,7 @@ abstract class AbstractSimpleResolverTest extends AbstractResolverTest { protected function getResolverSolutionsMapping() { - $totoSolution = new \stdClass(); + $totoSolution = $this->getMock('Overblog\GraphQLBundle\Definition\Builder\MappingInterface'); $totoSolution->name = 'Toto'; return [ @@ -27,7 +27,7 @@ public function testResolveKnownArg() { $arg = $this->resolver->resolve('Toto'); - $this->assertInstanceOf('stdClass', $arg); + $this->assertInstanceOf('Overblog\GraphQLBundle\Definition\Builder\MappingInterface', $arg); $this->assertEquals('Toto', $arg->name); } diff --git a/Tests/Resolver/Config/AbstractConfigSolutionTest.php b/Tests/Resolver/Config/AbstractConfigSolutionTest.php new file mode 100644 index 000000000..3dd07c969 --- /dev/null +++ b/Tests/Resolver/Config/AbstractConfigSolutionTest.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Resolver\Config; + +use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage; +use Overblog\GraphQLBundle\Resolver\Config\AbstractConfigSolution; +use Overblog\GraphQLBundle\Resolver\ConfigResolver; +use Overblog\GraphQLBundle\Tests\DIContainerMockTrait; + +abstract class AbstractConfigSolutionTest extends \PHPUnit_Framework_TestCase +{ + use DIContainerMockTrait; + + /** + * @var AbstractConfigSolution + */ + protected $configSolution; + + /** + * @return AbstractConfigSolution + */ + abstract protected function createConfigSolution(); + + public function setUp() + { + $container = $this->getDIContainerMock(); + $container + ->method('get') + ->will($this->returnValue(new \stdClass())); + + $expressionLanguage = new ExpressionLanguage(); + $expressionLanguage->setContainer($container); + + $typeResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\TypeResolver') + ->disableOriginalConstructor() + ->getMock(); + $typeResolver + ->method('resolve') + ->will($this->returnValue(new \stdClass())); + + $fieldResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\FieldResolver') + ->disableOriginalConstructor() + ->getMock(); + $fieldResolver + ->method('resolve') + ->will($this->returnValue(new \stdClass())); + + $argResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\ArgResolver') + ->disableOriginalConstructor() + ->getMock(); + $argResolver + ->method('resolve') + ->will($this->returnValue(new \stdClass())); + + $this->configSolution = $this->createConfigSolution(); + + $this->configSolution->setConfigResolver(new ConfigResolver()) + ->setArgResolver($argResolver) + ->setFieldResolver($fieldResolver) + ->setTypeResolver($typeResolver) + ->setExpressionLanguage($expressionLanguage); + } +} diff --git a/Tests/Resolver/Config/FieldsConfigSolutionTest.php b/Tests/Resolver/Config/FieldsConfigSolutionTest.php new file mode 100644 index 000000000..2d5f8af4d --- /dev/null +++ b/Tests/Resolver/Config/FieldsConfigSolutionTest.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Resolver\Config; + +use Overblog\GraphQLBundle\Relay\Connection\Output\ConnectionBuilder; +use Overblog\GraphQLBundle\Resolver\Config\FieldsConfigSolution; + +/** + * @property FieldsConfigSolution $configSolution + */ +class FieldsConfigSolutionTest extends AbstractConfigSolutionTest +{ + protected function createConfigSolution() + { + $typeConfigSolution = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\Config\TypeConfigSolution')->getMock(); + $resolveCallbackConfigSolution = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\Config\ResolveCallbackConfigSolution')->getMock(); + + return new FieldsConfigSolution($typeConfigSolution, $resolveCallbackConfigSolution); + } + + /** + * @expectedException \Overblog\GraphQLBundle\Error\UserError + * @expectedExceptionMessage Access denied to this field + */ + public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDenied() + { + $callback = $this->invokeResolveAccessAndWrapResolveCallback(false); + $callback('toto'); + } + + /** + * @expectedException \Overblog\GraphQLBundle\Error\UserError + * @expectedExceptionMessage Access denied to this field + */ + public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndExpressionEvalThrowingException() + { + $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=oups'); + $callback('titi'); + } + + public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDeniedGranted() + { + $callback = $this->invokeResolveAccessAndWrapResolveCallback(true); + $this->assertEquals('toto', $callback('toto')); + } + + public function testResolveAccessAndWrapResolveCallbackWithArrayAndAccessDeniedToEveryItemStartingByTo() + { + $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=not(object matches "/^to.*/i")'); + $this->assertEquals( + [ + 'tata', + 'titi', + 'tata', + ], + $callback( + [ + 'tata', + 'titi', + 'tata', + 'toto', + 'tota', + ] + ) + ); + } + + public function testResolveAccessAndWrapResolveCallbackWithRelayConnectionAndAccessGrantedToEveryNodeStartingByTo() + { + $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=object matches "/^to.*/i"'); + $this->assertEquals( + ConnectionBuilder::connectionFromArray(['toto', 'toti', null, null, null]), + $callback( + ConnectionBuilder::connectionFromArray(['toto', 'toti', 'coco', 'foo', 'bar']) + ) + ); + } + + /** + * @param bool|string $hasAccess + * @param callable|null $callback + * + * @return callback + */ + private function invokeResolveAccessAndWrapResolveCallback($hasAccess, callable $callback = null) + { + if (null === $callback) { + $callback = function ($value) { + return $value; + }; + } + + return $this->invokeMethod( + $this->configSolution, + 'resolveAccessAndWrapResolveCallback', + [ + $hasAccess, + $callback, + ] + ); + } + + /** + * Call protected/private method of a class. + * + * @see https://jtreminio.com/2013/03/unit-testing-tutorial-part-3-testing-protected-private-methods-coverage-reports-and-crap/ + * + * @param object $object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed Method return. + */ + private function invokeMethod($object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} diff --git a/Tests/Resolver/Config/TypeConfigSolutionTest.php b/Tests/Resolver/Config/TypeConfigSolutionTest.php new file mode 100644 index 000000000..e12fada4d --- /dev/null +++ b/Tests/Resolver/Config/TypeConfigSolutionTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Resolver\Config; + +use Overblog\GraphQLBundle\Resolver\Config\TypeConfigSolution; + +/** + * @property TypeConfigSolution $configSolution + */ +class TypeConfigSolutionTest extends AbstractConfigSolutionTest +{ + protected function createConfigSolution() + { + return new TypeConfigSolution(); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid type! Must be instance of "GraphQL\Type\Definition\Type" + */ + public function testSolveType() + { + $this->configSolution->solveType('toto'); + } +} diff --git a/Tests/Resolver/Config/ValuesConfigSolutionTest.php b/Tests/Resolver/Config/ValuesConfigSolutionTest.php new file mode 100644 index 000000000..47e251b3a --- /dev/null +++ b/Tests/Resolver/Config/ValuesConfigSolutionTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Overblog\GraphQLBundle\Tests\Resolver\Config; + +use Overblog\GraphQLBundle\Resolver\Config\ValuesConfigSolution; + +/** + * @property ValuesConfigSolution $configSolution + */ +class ValuesConfigSolutionTest extends AbstractConfigSolutionTest +{ + protected function createConfigSolution() + { + return new ValuesConfigSolution(); + } + + public function testSolve() + { + $config = $this->configSolution->solve( + [ + 'values' => [ + 'test' => ['value' => 'my test value'], + 'toto' => ['value' => 'my toto value'], + 'expression-language-test' => ['value' => '@=["my", "test"]'], + ], + ] + ); + + $expected = [ + 'values' => [ + 'test' => ['value' => 'my test value'], + 'toto' => ['value' => 'my toto value'], + 'expression-language-test' => ['value' => ['my', 'test']], + ], + ]; + + $this->assertEquals($expected, $config); + } +} diff --git a/Tests/Resolver/ConfigResolverTest.php b/Tests/Resolver/ConfigResolverTest.php index 82ab4a8d8..3a3cfa4f0 100644 --- a/Tests/Resolver/ConfigResolverTest.php +++ b/Tests/Resolver/ConfigResolverTest.php @@ -11,56 +11,16 @@ namespace Overblog\GraphQLBundle\Tests\Resolver; -use Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage; -use Overblog\GraphQLBundle\Relay\Connection\Output\ConnectionBuilder; use Overblog\GraphQLBundle\Resolver\ConfigResolver; -use Overblog\GraphQLBundle\Tests\DIContainerMockTrait; class ConfigResolverTest extends \PHPUnit_Framework_TestCase { - use DIContainerMockTrait; - /** @var ConfigResolver */ private $configResolver; public function setUp() { - $container = $this->getDIContainerMock(); - $container - ->method('get') - ->will($this->returnValue(new \stdClass())); - - $expressionLanguage = new ExpressionLanguage(); - $expressionLanguage->setContainer($container); - - $typeResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\TypeResolver') - ->disableOriginalConstructor() - ->getMock(); - $typeResolver - ->method('resolve') - ->will($this->returnValue(new \stdClass())); - - $fieldResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\FieldResolver') - ->disableOriginalConstructor() - ->getMock(); - $fieldResolver - ->method('resolve') - ->will($this->returnValue(new \stdClass())); - - $argResolver = $this->getMockBuilder('Overblog\GraphQLBundle\Resolver\ArgResolver') - ->disableOriginalConstructor() - ->getMock(); - $argResolver - ->method('resolve') - ->will($this->returnValue(new \stdClass())); - - $this->configResolver = new ConfigResolver( - $typeResolver, - $fieldResolver, - $argResolver, - $expressionLanguage, - true - ); + $this->configResolver = new ConfigResolver(); } /** @@ -71,129 +31,4 @@ public function testConfigNotArrayOrImplementArrayAccess() { $this->configResolver->resolve('Not Array'); } - - public function testResolveValues() - { - $config = $this->configResolver->resolve( - [ - 'values' => [ - 'test' => ['value' => 'my test value'], - 'toto' => ['value' => 'my toto value'], - 'expression-language-test' => ['value' => '@=["my", "test"]'], - ], - ] - ); - - $expected = [ - 'values' => [ - 'test' => ['value' => 'my test value'], - 'toto' => ['value' => 'my toto value'], - 'expression-language-test' => ['value' => ['my', 'test']], - ], - ]; - - $this->assertEquals($expected, $config); - } - - /** - * @expectedException \Overblog\GraphQLBundle\Error\UserError - * @expectedExceptionMessage Access denied to this field - */ - public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDenied() - { - $callback = $this->invokeResolveAccessAndWrapResolveCallback(false); - $callback('toto'); - } - - /** - * @expectedException \Overblog\GraphQLBundle\Error\UserError - * @expectedExceptionMessage Access denied to this field - */ - public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndExpressionEvalThrowingException() - { - $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=oups'); - $callback('titi'); - } - - public function testResolveAccessAndWrapResolveCallbackWithScalarValueAndAccessDeniedGranted() - { - $callback = $this->invokeResolveAccessAndWrapResolveCallback(true); - $this->assertEquals('toto', $callback('toto')); - } - - public function testResolveAccessAndWrapResolveCallbackWithArrayAndAccessDeniedToEveryItemStartingByTo() - { - $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=not(object matches "/^to.*/i")'); - $this->assertEquals( - [ - 'tata', - 'titi', - 'tata', - ], - $callback( - [ - 'tata', - 'titi', - 'tata', - 'toto', - 'tota', - ] - ) - ); - } - - public function testResolveAccessAndWrapResolveCallbackWithRelayConnectionAndAccessGrantedToEveryNodeStartingByTo() - { - $callback = $this->invokeResolveAccessAndWrapResolveCallback('@=object matches "/^to.*/i"'); - $this->assertEquals( - ConnectionBuilder::connectionFromArray(['toto', 'toti', null, null, null]), - $callback( - ConnectionBuilder::connectionFromArray(['toto', 'toti', 'coco', 'foo', 'bar']) - ) - ); - } - - /** - * @param bool|string $hasAccess - * @param callable|null $callback - * - * @return callback - */ - private function invokeResolveAccessAndWrapResolveCallback($hasAccess, callable $callback = null) - { - if (null === $callback) { - $callback = function ($value) { - return $value; - }; - } - - return $this->invokeMethod( - $this->configResolver, - 'resolveAccessAndWrapResolveCallback', - [ - $hasAccess, - $callback, - ] - ); - } - - /** - * Call protected/private method of a class. - * - * @see https://jtreminio.com/2013/03/unit-testing-tutorial-part-3-testing-protected-private-methods-coverage-reports-and-crap/ - * - * @param object $object Instantiated object that we will run method on. - * @param string $methodName Method name to call - * @param array $parameters Array of parameters to pass into method. - * - * @return mixed Method return. - */ - private function invokeMethod($object, $methodName, array $parameters = []) - { - $reflection = new \ReflectionClass(get_class($object)); - $method = $reflection->getMethod($methodName); - $method->setAccessible(true); - - return $method->invokeArgs($object, $parameters); - } } diff --git a/Tests/Resolver/TypeResolverTest.php b/Tests/Resolver/TypeResolverTest.php index f8c81a798..64498e822 100644 --- a/Tests/Resolver/TypeResolverTest.php +++ b/Tests/Resolver/TypeResolverTest.php @@ -29,6 +29,15 @@ protected function getResolverSolutionsMapping() ]; } + /** + * @expectedException \Overblog\GraphQLBundle\Resolver\UnsupportedResolverException + * @expectedExceptionMessage Resolver "not-supported" must be "GraphQL\Type\Definition\Type" "stdClass" given. + */ + public function testAddNotSupportedSolution() + { + $this->resolver->addSolution('not-supported', new \stdClass()); + } + public function testResolveKnownType() { $type = $this->resolver->resolve('Toto'); @@ -45,6 +54,14 @@ public function testResolveUnknownType() $this->resolver->resolve('Fake'); } + /** + * @expectedException \Overblog\GraphQLBundle\Resolver\UnresolvableException + */ + public function testWrongListOfWrappingType() + { + $this->resolver->resolve('[Tata'); + } + public function testResolveWithListOfWrapper() { /** @var \GraphQL\Type\Definition\WrappingType $type */