Skip to content

Commit ef543f3

Browse files
authored
Merge 5db0c3f into 4b752f1
2 parents 4b752f1 + 5db0c3f commit ef543f3

File tree

15 files changed

+542
-10
lines changed

15 files changed

+542
-10
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ OverblogGraphQLBundle
33

44
This Symfony bundle provides integration of [GraphQL](https://facebook.github.io/graphql/) using [webonyx/graphql-php](https://github.com/webonyx/graphql-php)
55
and [GraphQL Relay](https://facebook.github.io/relay/docs/graphql-relay-specification.html).
6-
It also supports batching using libs like [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer) or [Apollo GraphQL](http://dev.apollodata.com/core/network.html#query-batching).
6+
It also supports:
7+
* batching with [ReactRelayNetworkLayer](https://github.com/nodkz/react-relay-network-layer)
8+
* batching with [Apollo GraphQL](http://dev.apollodata.com/core/network.html#query-batching).
9+
* upload and batching upload with [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client)
710

811
[![Build Status](https://travis-ci.org/overblog/GraphQLBundle.svg?branch=master)](https://travis-ci.org/overblog/GraphQLBundle)
912
[![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)

Request/BatchParser.php

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
class BatchParser implements ParserInterface
99
{
10+
use UploadParserTrait;
11+
1012
const PARAM_ID = 'id';
1113

1214
private static $queriesDefaultValue = [
@@ -49,17 +51,29 @@ public function parse(Request $request)
4951
*/
5052
private function getParsedBody(Request $request)
5153
{
52-
$type = explode(';', $request->headers->get('content-type'))[0];
54+
$contentType = explode(';', $request->headers->get('content-type'))[0];
5355

5456
// JSON object
55-
if ($type !== static::CONTENT_TYPE_JSON) {
56-
throw new BadRequestHttpException(sprintf('Only request with content type "%s" is accepted.', static::CONTENT_TYPE_JSON));
57-
}
57+
switch ($contentType) {
58+
case static::CONTENT_TYPE_JSON:
59+
$parsedBody = json_decode($request->getContent(), true);
60+
61+
if (JSON_ERROR_NONE !== json_last_error()) {
62+
throw new BadRequestHttpException('POST body sent invalid JSON');
63+
}
64+
break;
5865

59-
$parsedBody = json_decode($request->getContent(), true);
66+
case static::CONTENT_TYPE_FORM_DATA:
67+
$parsedBody = $this->treatUploadFiles($request->request->all(), $request->files->all());
68+
break;
6069

61-
if (JSON_ERROR_NONE !== json_last_error()) {
62-
throw new BadRequestHttpException('POST body sent invalid JSON');
70+
default:
71+
throw new BadRequestHttpException(sprintf(
72+
'Batching parser only accepts "%s" or "%s" content-type but got %s.',
73+
static::CONTENT_TYPE_JSON,
74+
static::CONTENT_TYPE_FORM_DATA,
75+
json_encode($contentType)
76+
));
6377
}
6478

6579
return $parsedBody;

Request/Parser.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
class Parser implements ParserInterface
99
{
10+
use UploadParserTrait;
11+
1012
/**
1113
* @param Request $request
1214
*
@@ -55,7 +57,7 @@ private function getParsedBody(Request $request)
5557
// URL-encoded query-string
5658
case static::CONTENT_TYPE_FORM:
5759
case static::CONTENT_TYPE_FORM_DATA:
58-
$parsedBody = $request->request->all();
60+
$parsedBody = $this->treatUploadFiles($request->request->all(), $request->files->all());
5961
break;
6062

6163
default:

Request/UploadParserTrait.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Overblog\GraphQLBundle\Request;
4+
5+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
6+
use Symfony\Component\PropertyAccess\PropertyAccess;
7+
8+
trait UploadParserTrait
9+
{
10+
/**
11+
* @param array $operations
12+
* @param array $map
13+
* @param array $files
14+
*
15+
* @return array
16+
*/
17+
protected function mappingUploadFiles(array $operations, array $map, array $files)
18+
{
19+
$accessor = PropertyAccess::createPropertyAccessorBuilder()
20+
->enableExceptionOnInvalidIndex()
21+
->getPropertyAccessor();
22+
23+
foreach ($map as $fileName => $locations) {
24+
foreach ($locations as $location) {
25+
$fileKey = sprintf('[%s]', $fileName);
26+
if (!$accessor->isReadable($files, $fileKey)) {
27+
throw new BadRequestHttpException(sprintf('File %s is missing in the request.', json_encode($fileName)));
28+
}
29+
$file = $accessor->getValue($files, $fileKey);
30+
$locationKey = $this->locationToPropertyAccessPath($location);
31+
if (!$accessor->isReadable($operations, $locationKey)) {
32+
throw new BadRequestHttpException(sprintf('Map entry %s could not be localized in operations.', json_encode($location)));
33+
}
34+
$accessor->setValue($operations, $locationKey, $file);
35+
}
36+
}
37+
38+
return $operations;
39+
}
40+
41+
protected function locationToPropertyAccessPath($location)
42+
{
43+
return array_reduce(
44+
explode('.', $location),
45+
function ($carry, $item) {
46+
return sprintf('%s[%s]', $carry, $item);
47+
}
48+
);
49+
}
50+
51+
protected function isUploadPayload(array $payload)
52+
{
53+
return isset($payload['operations']) && isset($payload['map']) && is_array($payload['operations']) && is_array($payload['map']);
54+
}
55+
56+
protected function treatUploadFiles(array $parsedBody, array $files)
57+
{
58+
if ($this->isUploadPayload($parsedBody)) {
59+
return $this->mappingUploadFiles($parsedBody['operations'], $parsedBody['map'], $files);
60+
} else {
61+
return $parsedBody;
62+
}
63+
}
64+
}

Resources/doc/definitions/upload-files.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,53 @@
11
### Upload files
22

3+
Using apollo-upload-client
4+
--------------------------
5+
6+
The bundle comes of the box with a server compatible with
7+
[apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client).
8+
9+
1. Register upload scalar type to enabled it
10+
11+
```yaml
12+
# config/services.yaml
13+
services
14+
Overblog\GraphQLBundle\Upload\Type\GraphQLUploadType:
15+
class: Overblog\GraphQLBundle\Upload\Type\GraphQLUploadType
16+
arguments:
17+
- MyUpload
18+
tags:
19+
- { name: overblog_graphql.type, alias: MyUpload }
20+
```
21+
You can name as you want just replace `MyUpload` in above example.
22+
23+
2. Use it in your Schema
24+
25+
Here an example:
26+
27+
```yaml
28+
Mutation:
29+
type: object
30+
config:
31+
fields:
32+
singleUpload:
33+
type: String!
34+
resolve: '@=args["file"].getBasename()'
35+
args:
36+
file: MyUpload!
37+
multipleUpload:
38+
type: '[String!]'
39+
resolve: '@=[args["files"][0].getBasename(), args["files"][1].getBasename()]'
40+
args:
41+
files: '[MyUpload!]!'
42+
```
43+
44+
**Notes:**
45+
- Files args are of type `Symfony\Component\HttpFoundation\File\UploadedFile`
46+
- Upload scalar type can be use only on inputs fields (args or InputObject)
47+
48+
The classic way
49+
---------------
50+
351
here an example of how uploading can be done using this bundle
452

553
* First define schema
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
imports:
2+
- { resource: ../config.yml }
3+
4+
overblog_graphql:
5+
errors_handler:
6+
rethrow_internal_exceptions: true
7+
definitions:
8+
class_namespace: "Overblog\\GraphQLBundle\\Upload\\__DEFINITIONS__"
9+
schema:
10+
query: Query
11+
mutation: Mutation
12+
mappings:
13+
types:
14+
-
15+
type: yaml
16+
dir: "%kernel.root_dir%/config/upload/mapping"
17+
18+
services:
19+
Overblog\GraphQLBundle\Upload\Type\GraphQLUploadType:
20+
class: Overblog\GraphQLBundle\Upload\Type\GraphQLUploadType
21+
arguments:
22+
- Upload
23+
tags:
24+
- { name: overblog_graphql.type, alias: Upload }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Query:
2+
type: object
3+
config:
4+
fields:
5+
foo: {type: String!}
6+
7+
Mutation:
8+
type: object
9+
config:
10+
fields:
11+
singleUpload:
12+
type: String!
13+
resolve: '@=args["file"].getBasename()'
14+
args:
15+
file: Upload!
16+
multipleUpload:
17+
type: '[String!]'
18+
resolve: '@=[args["files"][0].getBasename(), args["files"][1].getBasename()]'
19+
args:
20+
files: '[Upload!]!'
21+
serializationIsUnsupported:
22+
type: Upload!
23+
resolve: '@=args["file"]'
24+
args:
25+
file: Upload!
26+
oldUpload:
27+
type: String!
28+
resolve: '@=args["file"]'
29+
args:
30+
file: String!

Tests/Functional/Controller/GraphControllerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public function testBatchEndpointWithEmptyQuery()
230230

231231
/**
232232
* @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
233-
* @expectedExceptionMessage Only request with content type "application/json" is accepted.
233+
* @expectedExceptionMessage Batching parser only accepts "application/json" or "multipart/form-data" content-type but got "".
234234
*/
235235
public function testBatchEndpointWrongContentType()
236236
{

0 commit comments

Comments
 (0)