From 0e912538f64377914e58862035f62ba25109c2ed Mon Sep 17 00:00:00 2001 From: Jeremiah VALERIE Date: Thu, 15 Sep 2016 16:53:31 +0200 Subject: [PATCH] Add compatible react-relay-network-layer batching --- Controller/GraphController.php | 31 ++++++++++++------- README.md | 9 +++++- Request/BatchParser.php | 29 ++++++++++------- Request/Executor.php | 6 ++-- Resources/config/routing/graphql.yml | 6 ++++ .../Controller/GraphControllerTest.php | 24 +++++++------- 6 files changed, 66 insertions(+), 39 deletions(-) diff --git a/Controller/GraphController.php b/Controller/GraphController.php index 14990d6dd..96ffe6aff 100644 --- a/Controller/GraphController.php +++ b/Controller/GraphController.php @@ -19,25 +19,32 @@ class GraphController extends Controller { public function endpointAction(Request $request) { - if ($request->query->has('batch')) { - $data = $this->processBatchQuery($request); - } else { - $data = $this->processNormalQuery($request); - } + $payload = $this->processNormalQuery($request); - return new JsonResponse($data, 200); + return new JsonResponse($payload, 200); } - private function processBatchQuery(Request $request) + public function batchEndpointAction(Request $request) { - $params = $this->get('overblog_graphql.request_batch_parser')->parse($request); - $data = []; + $payloads = $this->processBatchQuery($request); + + return new JsonResponse($payloads, 200); + } - foreach ($params as $i => $entry) { - $data[$i] = $this->get('overblog_graphql.request_executor')->execute($entry)->toArray(); + private function processBatchQuery(Request $request) + { + $queries = $this->get('overblog_graphql.request_batch_parser')->parse($request); + $payloads = []; + + foreach ($queries as $query) { + $payloadResult = $this->get('overblog_graphql.request_executor')->execute([ + 'query' => $query['query'], + 'variables' => $query['variables'], + ]); + $payloads[] = ['id' => $query['id'], 'payload' => $payloadResult->toArray()]; } - return $data; + return $payloads; } private function processNormalQuery(Request $request) diff --git a/README.md b/README.md index c9ce8b244..523223ad0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ OverblogGraphQLBundle ====================== -This Bundle provide integration [GraphQL](https://facebook.github.io/graphql/) using [webonyx/graphql-php](https://github.com/webonyx/graphql-php) +This Symfony 2 / 3 bundle provide integration [GraphQL](https://facebook.github.io/graphql/) using [webonyx/graphql-php](https://github.com/webonyx/graphql-php) and [GraphQL Relay](https://facebook.github.io/relay/docs/graphql-relay-specification.html). +It also supports batching using libs like [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer). [![Build Status](https://travis-ci.org/overblog/GraphQLBundle.svg?branch=master)](https://travis-ci.org/overblog/GraphQLBundle) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/overblog/GraphQLBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/overblog/GraphQLBundle/?branch=master) @@ -877,6 +878,12 @@ Expression | Description | Scope **Tips**: the expression language service can be custom using bundle configuration. +Batching +--------- + +Batching can help decrease io between server and client. +The default route of batching is `/batch`. + Contribute ---------- diff --git a/Request/BatchParser.php b/Request/BatchParser.php index c2fab5f1c..d07812497 100644 --- a/Request/BatchParser.php +++ b/Request/BatchParser.php @@ -16,6 +16,14 @@ class BatchParser implements ParserInterface { + const PARAM_ID = 'id'; + + private static $queriesDefaultValue = [ + self::PARAM_ID => null, + self::PARAM_QUERY => null, + self::PARAM_VARIABLES => null, + ]; + /** * @param Request $request * @@ -24,24 +32,21 @@ class BatchParser implements ParserInterface public function parse(Request $request) { // Extracts the GraphQL request parameters - $data = $this->getParsedBody($request); + $queries = $this->getParsedBody($request); - if (empty($data)) { + if (empty($queries)) { throw new BadRequestHttpException('Must provide at least one valid query.'); } - foreach ($data as $i => &$entry) { - if (empty($entry[static::PARAM_QUERY]) || !is_string($entry[static::PARAM_QUERY])) { - throw new BadRequestHttpException(sprintf('No valid query found in node "%s"', $i)); - } + foreach ($queries as $i => &$query) { + $query = $query + self::$queriesDefaultValue; - $entry = $entry + [ - static::PARAM_VARIABLES => null, - static::PARAM_OPERATION_NAME => null, - ]; + if (!is_string($query[static::PARAM_QUERY])) { + throw new BadRequestHttpException(sprintf('%s is not a valid query', json_encode($query[static::PARAM_QUERY]))); + } } - return $data; + return $queries; } /** @@ -57,7 +62,7 @@ private function getParsedBody(Request $request) // JSON object if ($type !== static::CONTENT_TYPE_JSON) { - throw new BadRequestHttpException(sprintf('Only request with content type "%" is accepted.', static::CONTENT_TYPE_JSON)); + throw new BadRequestHttpException(sprintf('Only request with content type "%s" is accepted.', static::CONTENT_TYPE_JSON)); } $parsedBody = json_decode($request->getContent(), true); diff --git a/Request/Executor.php b/Request/Executor.php index e2a4a1635..caee39642 100644 --- a/Request/Executor.php +++ b/Request/Executor.php @@ -80,11 +80,11 @@ public function execute(array $data, array $context = []) $executionResult = GraphQL::executeAndReturnResult( $this->schema, - isset($data['query']) ? $data['query'] : null, + isset($data[ParserInterface::PARAM_QUERY]) ? $data[ParserInterface::PARAM_QUERY] : null, $context, $context, - $data['variables'], - $data['operationName'] + $data[ParserInterface::PARAM_VARIABLES], + isset($data[ParserInterface::PARAM_OPERATION_NAME]) ? $data[ParserInterface::PARAM_OPERATION_NAME] : null ); if (null !== $this->errorHandler) { diff --git a/Resources/config/routing/graphql.yml b/Resources/config/routing/graphql.yml index 0333a21c6..1e9dbd8f6 100644 --- a/Resources/config/routing/graphql.yml +++ b/Resources/config/routing/graphql.yml @@ -3,3 +3,9 @@ overblog_graphql_endpoint: defaults: _controller: OverblogGraphQLBundle:Graph:endpoint _format: "json" + +overblog_graphql_batch_endpoint: + path: /batch + defaults: + _controller: OverblogGraphQLBundle:Graph:batchEndpoint + _format: "json" diff --git a/Tests/Functional/Controller/GraphControllerTest.php b/Tests/Functional/Controller/GraphControllerTest.php index 55634d5d4..a16afb901 100644 --- a/Tests/Functional/Controller/GraphControllerTest.php +++ b/Tests/Functional/Controller/GraphControllerTest.php @@ -154,20 +154,22 @@ public function testBatchEndpointAction() $client = static::createClient(['test_case' => 'connection']); $data = [ - 'friends' => [ + [ + 'id' => 'friends', 'query' => $this->friendsQuery, ], - 'friendsTotalCount' => [ + [ + 'id' => 'friendsTotalCount', 'query' => $this->friendsTotalCountQuery, ], ]; - $client->request('POST', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); + $client->request('POST', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($data)); $result = $client->getResponse()->getContent(); $expected = [ - 'friends' => ['data' => $this->expectedData], - 'friendsTotalCount' => ['data' => ['user' => ['friends' => ['totalCount' => 4]]]], + ['id' => 'friends', 'payload' => ['data' => $this->expectedData]], + ['id' => 'friendsTotalCount', 'payload' => ['data' => ['user' => ['friends' => ['totalCount' => 4]]]]], ]; $this->assertEquals($expected, json_decode($result, true), $result); } @@ -179,18 +181,18 @@ public function testBatchEndpointAction() public function testBatchEndpointWithEmptyQuery() { $client = static::createClient(); - $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{}'); + $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{}'); $client->getResponse()->getContent(); } /** * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - * @expectedExceptionMessage Only request with content type " is accepted. + * @expectedExceptionMessage Only request with content type "application/json" is accepted. */ public function testBatchEndpointWrongContentType() { $client = static::createClient(); - $client->request('GET', '/?batch'); + $client->request('GET', '/batch'); $client->getResponse()->getContent(); } @@ -201,18 +203,18 @@ public function testBatchEndpointWrongContentType() public function testBatchEndpointWithInvalidJson() { $client = static::createClient(); - $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{'); + $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{'); $client->getResponse()->getContent(); } /** * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - * @expectedExceptionMessage No valid query found in node "test" + * @expectedExceptionMessage 1 is not a valid query */ public function testBatchEndpointWithInvalidQuery() { $client = static::createClient(); - $client->request('GET', '/?batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{"test" : {"query": 1}}'); + $client->request('GET', '/batch', [], [], ['CONTENT_TYPE' => 'application/json'], '{"test" : {"query": 1}}'); $client->getResponse()->getContent(); } }