From 71f69283372516686588f9a8c8a77a1135c91ef4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 8 Jul 2020 08:07:25 +0200 Subject: [PATCH] Profiler feature --- README.md | 1 + composer.json | 4 +- docs/profiler/index.md | 20 ++ phpstan-baseline.neon | 22 +- src/Controller/ProfilerController.php | 72 ++++++ src/DataCollector/GraphQLCollector.php | 169 ++++++++++++++ src/DependencyInjection/Configuration.php | 18 +- .../OverblogGraphQLExtension.php | 7 + src/Event/ExecutorArgumentsEvent.php | 34 ++- src/Event/ExecutorResultEvent.php | 11 +- src/Request/Executor.php | 11 +- src/Resources/config/profiler.yaml | 17 ++ .../views/profiler/graphql.html.twig | 215 ++++++++++++++++++ src/Resources/views/profiler/panel.html.twig | 26 +++ tests/Controller/ProfilerControllerTest.php | 93 ++++++++ tests/DataCollector/GraphQLCollectorTest.php | 69 ++++++ tests/Request/ExecutorTest.php | 21 +- 17 files changed, 794 insertions(+), 16 deletions(-) create mode 100644 docs/profiler/index.md create mode 100644 src/Controller/ProfilerController.php create mode 100644 src/DataCollector/GraphQLCollector.php create mode 100644 src/Resources/config/profiler.yaml create mode 100644 src/Resources/views/profiler/graphql.html.twig create mode 100644 src/Resources/views/profiler/panel.html.twig create mode 100644 tests/Controller/ProfilerControllerTest.php create mode 100644 tests/DataCollector/GraphQLCollectorTest.php diff --git a/README.md b/README.md index bb3b3a642..60a433065 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Documentation - [Disable introspection](docs/security/disable_introspection.md) - [Errors handling](docs/error-handling/index.md) - [Events](docs/events/index.md) +- [Profiler](docs/profiler/index.md) Talks and slides to help you start ---------------------------------- diff --git a/composer.json b/composer.json index 75b0794c9..ba1922ed9 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,9 @@ "symfony/process": "^4.3 || ^5.0", "symfony/security-bundle": "^4.3 || ^5.0", "symfony/validator": "^4.3 || ^5.0", - "symfony/yaml": "^4.3 || ^5.0" + "symfony/var-dumper": "^4.3 || ^5.0", + "symfony/yaml": "^4.3 || ^5.0", + "twig/twig": "^2.10|^3.0" }, "extra": { "branch-alias": { diff --git a/docs/profiler/index.md b/docs/profiler/index.md new file mode 100644 index 000000000..793ab6a72 --- /dev/null +++ b/docs/profiler/index.md @@ -0,0 +1,20 @@ +> _The profiler feature was introduced in the version **1.0**_ + +# Profiler + +This bundle provides a profiler to monitor your GraphQL queries and mutations. + +## Configuration + +In order to display only GraphQL related requests, the profiler will filter requests based on the requested url. +By default, it will display requests matching the configured endpoint url (ie. The route `overblog_graphql_endpoint`). + +If you need to change the behavior (for example if you have parameters in your endpoint url), you can change the matching with the following option: + +```yaml +overblog_graphql: + profiler: + query_match: my_string_to_match +``` + +In the example above, only the requests containing the string `my_string_to_match` will be displayed. \ No newline at end of file diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d11fda989..0a6d8f93c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -362,7 +362,7 @@ parameters: - message: "#^Cannot call method end\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#" - count: 1 + count: 2 path: src/DependencyInjection/Configuration.php - @@ -1224,3 +1224,23 @@ parameters: message: "#^Array \\(array\\) does not accept Symfony\\\\Component\\\\Validator\\\\Mapping\\\\MetadataInterface\\.$#" count: 1 path: src/Validator/InputValidator.php + + - + message: "#^Cannot call method loadProfile\\(\\) on Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler|null\\.$#" + count: 1 + path: src/Controller/ProfilerController.php + + - + message: "#^Parameter \\#1 \\$ip of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#" + count: 1 + path: src/Controller/ProfilerController.php + + - + message: "#^Parameter \\#5 \\$start of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#" + count: 1 + path: src/Controller/ProfilerController.php + + - + message: "#^Parameter \\#6 \\$end of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#" + count: 1 + path: src/Controller/ProfilerController.php \ No newline at end of file diff --git a/src/Controller/ProfilerController.php b/src/Controller/ProfilerController.php new file mode 100644 index 000000000..e21efdea5 --- /dev/null +++ b/src/Controller/ProfilerController.php @@ -0,0 +1,72 @@ +profiler = $profiler; + $this->twig = $twig; + $this->endpointUrl = $router->generate('overblog_graphql_endpoint'); + $this->requestExecutor = $requestExecutor; + $this->queryMatch = $queryMatch; + } + + /** + * @throws ServiceNotFoundException + */ + public function __invoke(Request $request, string $token): Response + { + if (null === $this->profiler) { + throw new ServiceNotFoundException('The profiler must be enabled.'); + } + + if (null === $this->twig) { + throw new ServiceNotFoundException('The GraphQL Profiler require twig'); + } + + $this->profiler->disable(); + + $profile = $this->profiler->loadProfile($token); + + $tokens = \array_map(function ($tokenData) { + $profile = $this->profiler->loadProfile($tokenData['token']); + $graphql = $profile ? $profile->getCollector('graphql') : null; + $tokenData['graphql'] = $graphql; + + return $tokenData; + }, $this->profiler->find(null, $this->queryMatch ?: $this->endpointUrl, '100', 'POST', null, null, null)); + + $schemas = []; + foreach ($this->requestExecutor->getSchemasNames() as $schemaName) { + $schemas[$schemaName] = SchemaPrinter::doPrint($this->requestExecutor->getSchema($schemaName)); + } + + return new Response($this->twig->render('@OverblogGraphQL/profiler/graphql.html.twig', [ + 'request' => $request, + 'profile' => $profile, + 'tokens' => $tokens, + 'token' => $token, + 'panel' => null, + 'schemas' => $schemas, + ]), 200, ['Content-Type' => 'text/html']); + } +} diff --git a/src/DataCollector/GraphQLCollector.php b/src/DataCollector/GraphQLCollector.php new file mode 100644 index 000000000..af2a5a34d --- /dev/null +++ b/src/DataCollector/GraphQLCollector.php @@ -0,0 +1,169 @@ +batches as $batch) { + if (isset($batch['error'])) { + $error = true; + } + $count += $batch['count']; + } + + $this->data = [ + 'schema' => $request->attributes->get('_route_params')['schemaName'] ?? 'default', + 'batches' => $this->batches, + 'count' => $count, + 'error' => $error, + ]; + } + + /** + * Check if we have an error. + */ + public function getError(): bool + { + return $this->data['error'] ?? false; + } + + /** + * Count the number of executed queries. + */ + public function getCount(): int + { + return $this->data['count'] ?? 0; + } + + /** + * Return the targeted schema. + */ + public function getSchema(): string + { + return $this->data['schema'] ?? 'default'; + } + + /** + * Return the list of executed batch. + */ + public function getBatches(): array + { + return $this->data['batches'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function reset(): void + { + $this->data = []; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'graphql'; + } + + /** + * Hook into the GraphQL events to populate the collector. + */ + public function onPostExecutor(ExecutorResultEvent $event): void + { + $executorArgument = $event->getExecutorArguments(); + $queryString = $executorArgument->getRequestString(); + $operationName = $executorArgument->getOperationName(); + $variables = $executorArgument->getVariableValue(); + $queryTime = \microtime(true) - $executorArgument->getStartTime(); + + $result = $event->getResult()->toArray(); + + $batch = [ + 'queryString' => $queryString, + 'queryTime' => $queryTime, + 'variables' => $this->cloneVar($variables), + 'result' => $this->cloneVar($result), + 'count' => 0, + ]; + + try { + $parsed = Parser::parse($queryString); + $batch['graphql'] = $this->extractGraphql($parsed, $operationName); + if (isset($batch['graphql']['fields'])) { + $batch['count'] += \count($batch['graphql']['fields']); + } + $error = $result['errors'][0] ?? false; + if ($error) { + $batch['error'] = [ + 'message' => $error['message'], + 'location' => $error['locations'][0] ?? false, + ]; + } + } catch (SyntaxError $error) { + $location = $error->getLocations()[0] ?? false; + $batch['error'] = ['message' => $error->getMessage(), 'location' => $location]; + } + + $this->batches[] = $batch; + } + + /** + * Extract GraphQL Information from the documentNode. + */ + protected function extractGraphql(DocumentNode $document, ?string $operationName): array + { + $operation = null; + $fields = []; + + foreach ($document->definitions as $definition) { + if ($definition instanceof OperationDefinitionNode) { + $definitionOperation = $definition->name->value ?? null; + if ($operationName != $definitionOperation) { + continue; + } + + $operation = $definition->operation; + foreach ($definition->selectionSet->selections as $selection) { + if ($selection instanceof FieldNode) { + $name = $selection->name->value; + $alias = $selection->alias ? $selection->alias->value : null; + + $fields[] = [ + 'name' => $name, + 'alias' => $alias, + ]; + } + } + } + } + + return [ + 'operation' => $operation, + 'operationName' => $operationName, + 'fields' => $fields, + ]; + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index ec6d4a564..ccd418e91 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -56,6 +56,7 @@ public function getConfigTreeBuilder() ->append($this->servicesSection()) ->append($this->securitySection()) ->append($this->doctrineSection()) + ->append($this->profilerSection()) ->end(); return $treeBuilder; @@ -210,7 +211,7 @@ private function definitionsSchemaSection() ->arrayNode('resolver_maps') ->defaultValue([]) ->prototype('scalar')->end() - ->setDeprecated('The "%path%.%node%" configuration is deprecated since version 0.13 and will be removed in 0.14. Add the "overblog_graphql.resolver_map" tag to the services instead.') + ->setDeprecated('The "%path%.%node%" configuration is deprecated since version 0.13 and will be removed in 0.14. Add the "overblog_graphql.resolver_map" tag to the services instead.', '0.13') ->end() ->arrayNode('types') ->defaultValue([]) @@ -290,6 +291,21 @@ private function doctrineSection() return $node; } + private function profilerSection() + { + $builder = new TreeBuilder('profiler'); + /** @var ArrayNodeDefinition $node */ + $node = self::getRootNodeWithoutDeprecation($builder, 'profiler'); + $node + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('query_match')->defaultNull()->end() + ->end() + ; + + return $node; + } + /** * @param string $name * diff --git a/src/DependencyInjection/OverblogGraphQLExtension.php b/src/DependencyInjection/OverblogGraphQLExtension.php index b7edc491d..a5d6062d1 100644 --- a/src/DependencyInjection/OverblogGraphQLExtension.php +++ b/src/DependencyInjection/OverblogGraphQLExtension.php @@ -48,6 +48,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->setConfigBuilders($config, $container); $this->setDebugListener($config, $container); $this->setDefinitionParameters($config, $container); + $this->setProfilerParameters($config, $container); $this->setClassLoaderListener($config, $container); $this->setCompilerCacheWarmer($config, $container); $this->registerForAutoconfiguration($container); @@ -82,6 +83,7 @@ private function loadConfigFiles(ContainerBuilder $container): void $loader->load('expression_language_functions.yaml'); $loader->load('definition_config_processors.yaml'); $loader->load('aliases.yaml'); + $loader->load('profiler.yaml'); } private function registerForAutoconfiguration(ContainerBuilder $container): void @@ -155,6 +157,11 @@ private function setDefinitionParameters(array $config, ContainerBuilder $contai $container->setParameter($this->getAlias().'.use_experimental_executor', $config['definitions']['use_experimental_executor']); } + private function setProfilerParameters(array $config, ContainerBuilder $container): void + { + $container->setParameter($this->getAlias().'.profiler.query_match', $config['profiler']['query_match']); + } + private function setBatchingMethod(array $config, ContainerBuilder $container): void { $container->setParameter($this->getAlias().'.batching_method', $config['batching_method']); diff --git a/src/Event/ExecutorArgumentsEvent.php b/src/Event/ExecutorArgumentsEvent.php index 3ef8ac176..05613e3fc 100644 --- a/src/Event/ExecutorArgumentsEvent.php +++ b/src/Event/ExecutorArgumentsEvent.php @@ -27,14 +27,17 @@ final class ExecutorArgumentsEvent extends Event /** @var string|null */ private $operationName; + /** @var float */ + private $startTime; + public static function create( - ExtensibleSchema $schema, - $requestString, - \ArrayObject $contextValue, - $rootValue = null, - array $variableValue = null, - $operationName = null - ) { + ExtensibleSchema $schema, + $requestString, + \ArrayObject $contextValue, + $rootValue = null, + array $variableValue = null, + $operationName = null + ) { $instance = new static(); $instance->setSchema($schema); $instance->setRequestString($requestString); @@ -42,6 +45,7 @@ public static function create( $instance->setRootValue($rootValue); $instance->setVariableValue($variableValue); $instance->setOperationName($operationName); + $instance->setStartTime(\microtime(true)); return $instance; } @@ -85,6 +89,14 @@ public function setSchema(ExtensibleSchema $schema): void $this->schema = $schema; } + public function setStartTime(float $startTime): void + { + $this->startTime = $startTime; + } + + /** + * @return ExtensibleSchema + */ public function getSchema(): ExtensibleSchema { return $this->schema; @@ -123,4 +135,12 @@ public function getOperationName() { return $this->operationName; } + + /** + * @return float|null + */ + public function getStartTime() + { + return $this->startTime; + } } diff --git a/src/Event/ExecutorResultEvent.php b/src/Event/ExecutorResultEvent.php index 8ae7c2691..f775236c9 100644 --- a/src/Event/ExecutorResultEvent.php +++ b/src/Event/ExecutorResultEvent.php @@ -12,13 +12,22 @@ final class ExecutorResultEvent extends Event /** @var ExecutionResult */ private $result; - public function __construct(ExecutionResult $result) + /** @var ExecutorArgumentsEvent */ + private $executorArguments; + + public function __construct(ExecutionResult $result, ExecutorArgumentsEvent $executorArguments) { $this->result = $result; + $this->executorArguments = $executorArguments; } public function getResult(): ExecutionResult { return $this->result; } + + public function getExecutorArguments(): ExecutorArgumentsEvent + { + return $this->executorArguments; + } } diff --git a/src/Request/Executor.php b/src/Request/Executor.php index 9417cf6e9..90ebea9b0 100644 --- a/src/Request/Executor.php +++ b/src/Request/Executor.php @@ -95,6 +95,11 @@ public function getSchema(?string $name = null): Schema return $schema; } + public function getSchemasNames(): array + { + return \array_keys($this->schemas); + } + public function setMaxQueryDepth($maxQueryDepth): void { /** @var QueryDepth $queryDepth */ @@ -148,7 +153,7 @@ public function execute(?string $schemaName, array $request, $rootValue = null): $this->defaultFieldResolver ); - $result = $this->postExecute($result); + $result = $this->postExecute($result, $executorArgumentsEvent); return $result; } @@ -172,10 +177,10 @@ private function preExecute( ); } - private function postExecute(ExecutionResult $result): ExecutionResult + private function postExecute(ExecutionResult $result, ExecutorArgumentsEvent $executorArguments): ExecutionResult { return $this->dispatcher->dispatch( - new ExecutorResultEvent($result), + new ExecutorResultEvent($result, $executorArguments), Events::POST_EXECUTOR )->getResult(); } diff --git a/src/Resources/config/profiler.yaml b/src/Resources/config/profiler.yaml new file mode 100644 index 000000000..3d48b1641 --- /dev/null +++ b/src/Resources/config/profiler.yaml @@ -0,0 +1,17 @@ +services: + Overblog\GraphQLBundle\Controller\ProfilerController: + public: true + arguments: + - "@?profiler" + - "@?twig" + - "@router" + - '@Overblog\GraphQLBundle\Request\Executor' + - "%overblog_graphql.profiler.query_match%" + + Overblog\GraphQLBundle\DataCollector\GraphQLCollector: + public: false + tags: + - name: data_collector + template: "@OverblogGraphQL/profiler/panel.html.twig" + id: graphql + - { name: kernel.event_listener, event: graphql.post_executor, method: onPostExecutor } diff --git a/src/Resources/views/profiler/graphql.html.twig b/src/Resources/views/profiler/graphql.html.twig new file mode 100644 index 000000000..3496091ad --- /dev/null +++ b/src/Resources/views/profiler/graphql.html.twig @@ -0,0 +1,215 @@ + + +
+
+

GraphQL requests

+
+
+

{{ tokens ? tokens|length : 'No' }} HTTP queries on GraphQL endpoint(s)

+ Refresh with latest query +
+ + {% if tokens %} + {% for result in tokens %} + {% set graphql = result.graphql %} + {% set css_class = result.status_code|default(0) > 399 ? 'status-error' : result.status_code|default(0) > 299 ? 'status-warning' : 'status-success' %} +
+
+ + {{ result.status_code|default('n/a') }} + + {{ result.time|date }} + {% if schemas|length > 0 %} + schema: {{ graphql.schema }} + {% endif %} + {{ result.token }} +
+ + + + + + + {% for idx, batch in graphql.batches %} + {% set isCurrentToken = result.token == token %} + + + + + + {% endfor %} +
#TimeInfo
{{ idx + 1}} + {{ (batch.queryTime*1000)|round(0) }} ms + +
+ {% if batch.error is defined %} +
+ + {% if batch.error.location is defined %} + At line {{ batch.error.location.line}}, column {{ batch.error.location.column}}
+ {% endif %} + {{ batch.error.message }} +
+
+ {% endif %} + {% if batch.graphql.operation is defined %} + {% set operation = batch.graphql.operation %} + {% set operationName = batch.graphql.operationName %} + {% set fields = batch.graphql.fields %} +
+ + {{ operation }} {% if operationName %}{{ operationName }}(...){% endif %} { + + {% for field in fields %} +
+ {% if field.alias %}{{field.alias }}: {% endif %}{{ field.name}}(...) +
+ {% endfor %} + + } + +
+ {% endif %} + {% if isCurrentToken %} + +
+
+
+ Variables: {{ profiler_dump(batch.variables, maxDepth=2) }} +
+
{{ batch.queryString }}
+
+
+ {{ profiler_dump(batch.result, maxDepth=3) }} +
+
+ {% else %} + + {% endif %} +
+
+
+ {% endfor %} + {% else %} +
+

No GraphQL queries stored.

+
+ {% endif %} +
+
+ {% for name, schema in schemas %} +
+

Schema: {{ name }}

+
+
{{schema}}
+
+
+ {% endfor %} +
+ + + + + + diff --git a/src/Resources/views/profiler/panel.html.twig b/src/Resources/views/profiler/panel.html.twig new file mode 100644 index 000000000..05ab8d9e7 --- /dev/null +++ b/src/Resources/views/profiler/panel.html.twig @@ -0,0 +1,26 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block svg %} + +{% endblock %} + +{% block toolbar %} + {% set icon %} + {{ block('svg')}} + GraphQL + {% endset %} + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: true }) }} +{% endblock %} + +{% block menu %} + {# This left-hand menu appears when using the full-screen profiler. #} + + {{ block('svg')}} + GraphQL + {{ collector.count }} + +{% endblock %} + +{% block panel %} + {{ render(controller('Overblog\\GraphQLBundle\\Controller\\ProfilerController', {token: token})) }} +{% endblock %} \ No newline at end of file diff --git a/tests/Controller/ProfilerControllerTest.php b/tests/Controller/ProfilerControllerTest.php new file mode 100644 index 000000000..5d6fe6ba6 --- /dev/null +++ b/tests/Controller/ProfilerControllerTest.php @@ -0,0 +1,93 @@ +getMockBuilder(Router::class)->disableOriginalConstructor()->setMethods(['generate'])->getMock(); + $router->expects($this->once())->method('generate')->willReturn('/endpoint'); + + return $router; + } + + protected function getMockExecutor($expected = true) + { + $executor = $this->getMockBuilder(Executor::class)->disableOriginalConstructor()->setMethods(['getSchemasNames', 'getSchema'])->getMock(); + if ($expected) { + $schema = new Schema([]); + $executor->expects($this->once())->method('getSchemasNames')->willReturn(['schema']); + $executor->expects($this->once())->method('getSchema')->willReturn($schema); + } + + return $executor; + } + + protected function getMockProfiler() + { + $profiler = $this->getMockBuilder(Profiler::class)->disableOriginalConstructor()->setMethods(['disable', 'loadProfile', 'find'])->getMock(); + + return $profiler; + } + + public function testInvokeWithoutProfiler(): void + { + $controller = new ProfilerController(null, null, $this->getMockRouter(), $this->getMockExecutor(false), null); + + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + $controller->__invoke(new Request(), 'token'); + } + + public function testInvokeWithoutTwig(): void + { + $controller = new ProfilerController($this->getMockProfiler(), null, $this->getMockRouter(), $this->getMockExecutor(false), null); + + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('The GraphQL Profiler require twig'); + $controller->__invoke(new Request(), 'token'); + } + + public function testWithToken(): void + { + $profilerMock = $this->getMockProfiler(); + $executorMock = $this->getMockExecutor(); + $routerMock = $this->getMockRouter(); + $twigMock = $this->getMockBuilder(Environment::class)->disableOriginalConstructor()->setMethods(['render'])->getMock(); + $controller = new ProfilerController($profilerMock, $twigMock, $routerMock, $executorMock, null); + $graphqlData = ['graphql_data']; + + $profilerMock->expects($this->once())->method('disable'); + $profilerMock->expects($this->once())->method('find')->willReturn([['token' => 'token']]); + $profileMock = $this->getMockBuilder(Profile::class)->disableOriginalConstructor()->setMethods(['getCollector'])->getMock(); + $profileMock->expects($this->once())->method('getCollector')->willReturn($graphqlData); + + $profilerMock->expects($this->exactly(2))->method('loadProfile')->willReturn($profileMock); + + $request = new Request(); + $twigMock->expects($this->once())->method('render')->with('@OverblogGraphQL/profiler/graphql.html.twig', [ + 'request' => $request, + 'profile' => $profileMock, + 'tokens' => [['token' => 'token', 'graphql' => $graphqlData]], + 'token' => 'token', + 'panel' => null, + 'schemas' => ['schema' => "\n"], + ]); + + $controller->__invoke($request, 'token'); + } +} diff --git a/tests/DataCollector/GraphQLCollectorTest.php b/tests/DataCollector/GraphQLCollectorTest.php new file mode 100644 index 000000000..3d777ed2f --- /dev/null +++ b/tests/DataCollector/GraphQLCollectorTest.php @@ -0,0 +1,69 @@ +attributes->set('_route_params', ['schemaName' => 'myschema']); + + $collector->onPostExecutor(new ExecutorResultEvent( + new ExecutionResult(['res' => 'ok', 'error' => 'my error']), + ExecutorArgumentsEvent::create(new ExtensibleSchema([]), 'invalid', new \ArrayObject()) + )); + + $collector->onPostExecutor(new ExecutorResultEvent( + new ExecutionResult(['res' => 'ok', 'error' => 'my error']), + ExecutorArgumentsEvent::create(new ExtensibleSchema([]), 'query{ myalias: test{field1, field2} }', new \ArrayObject(), null, ['variable1' => 'v1']) + )); + + $collector->collect($request, new Response()); + + $this->assertEquals($collector->getSchema(), 'myschema'); + $this->assertEquals($collector->getName(), 'graphql'); + $this->assertEquals($collector->getCount(), 1); + $this->assertTrue($collector->getError()); + $batches = $collector->getBatches(); + + $batchError = $batches[0]; + $batchSuccess = $batches[1]; + + $this->assertEquals($batchError['count'], 0); + $this->assertTrue(isset($batchError['error']['message'])); + + $this->assertEquals($batchSuccess['count'], 1); + $this->assertFalse(isset($batchSuccess['error'])); + $this->assertTrue($batchSuccess['variables'] instanceof Data); + $variables = $batchSuccess['variables']->getValue(); + $this->assertIsArray($variables); + $this->assertTrue(isset($variables['variable1'])); + + $this->assertNotNull($variables); + $this->assertEquals($batchSuccess['graphql'], [ + 'operation' => 'query', + 'operationName' => null, + 'fields' => [ + [ + 'name' => 'test', + 'alias' => 'myalias', + ], + ], + ]); + } +} diff --git a/tests/Request/ExecutorTest.php b/tests/Request/ExecutorTest.php index 32e69ff9d..07dd7100e 100644 --- a/tests/Request/ExecutorTest.php +++ b/tests/Request/ExecutorTest.php @@ -5,6 +5,7 @@ namespace Overblog\GraphQLBundle\Tests\Request; use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter; +use GraphQL\Type\Schema; use Overblog\GraphQLBundle\Executor\Executor; use Overblog\GraphQLBundle\Request\Executor as RequestExecutor; use PHPUnit\Framework\TestCase; @@ -12,12 +13,28 @@ class ExecutorTest extends TestCase { + protected function getMockedExecutor() + { + $dispatcher = $this->getMockBuilder(EventDispatcher::class)->setMethods(['dispatch'])->getMock(); + + return new RequestExecutor(new Executor(), new ReactPromiseAdapter(), $dispatcher); + } + public function testGetSchemaNoSchemaFound(): void { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('At least one schema should be declare.'); - $dispatcher = $this->getMockBuilder(EventDispatcher::class)->setMethods(['dispatch'])->getMock(); - (new RequestExecutor(new Executor(), new ReactPromiseAdapter(), $dispatcher))->getSchema('fake'); + $this->getMockedExecutor()->getSchema('fake'); + } + + public function testGetSchemasName(): void + { + $executor = $this->getMockedExecutor(); + $executor->addSchemaBuilder('schema1', function (): void { + }); + $executor->addSchema('schema2', new Schema([])); + + $this->assertSame($executor->getSchemasNames(), ['schema1', 'schema2']); } }