Skip to content

Commit 0aa091a

Browse files
authored
Merge 94202e9 into 4b752f1
2 parents 4b752f1 + 94202e9 commit 0aa091a

27 files changed

+885
-40
lines changed

Config/CustomScalarTypeDefinition.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ public function getDefinition()
1515
->children()
1616
->append($this->nameSection())
1717
->append($this->descriptionSection())
18-
->variableNode('serialize')->isRequired()->end()
19-
->variableNode('parseValue')->isRequired()->end()
20-
->variableNode('parseLiteral')->isRequired()->end()
18+
->variableNode('scalarType')->end()
19+
->variableNode('serialize')->end()
20+
->variableNode('parseValue')->end()
21+
->variableNode('parseLiteral')->end()
2122
->end();
2223

2324
return $node;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Overblog\GraphQLBundle\Definition\Type;
4+
5+
use GraphQL\Type\Definition\CustomScalarType as BaseCustomScalarType;
6+
use GraphQL\Type\Definition\ScalarType;
7+
use GraphQL\Utils\Utils;
8+
9+
class CustomScalarType extends BaseCustomScalarType
10+
{
11+
public function __construct(array $config = [])
12+
{
13+
$config['name'] = isset($config['name']) ? $config['name'] : uniqid('CustomScalar');
14+
parent::__construct($config);
15+
16+
$this->config['scalarType'] = isset($this->config['scalarType']) ? $this->config['scalarType'] : null;
17+
}
18+
19+
/**
20+
* {@inheritdoc}
21+
*/
22+
public function serialize($value)
23+
{
24+
return $this->call('serialize', $value);
25+
}
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function parseValue($value)
31+
{
32+
return $this->call('parseValue', $value);
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
39+
{
40+
return $this->call('parseLiteral', $valueNode);
41+
}
42+
43+
private function call($type, $value)
44+
{
45+
if (isset($this->config['scalarType'])) {
46+
$scalarType = $this->config['scalarType'];
47+
$scalarType = is_callable($scalarType) ? $scalarType() : $scalarType;
48+
49+
return call_user_func([$scalarType, $type], $value);
50+
} else {
51+
return parent::$type($value);
52+
}
53+
}
54+
55+
public function assertValid()
56+
{
57+
if (isset($this->config['scalarType'])) {
58+
$scalarType = $this->config['scalarType'];
59+
if (is_callable($scalarType)) {
60+
$scalarType = $scalarType();
61+
}
62+
63+
Utils::invariant(
64+
$scalarType instanceof ScalarType,
65+
sprintf(
66+
'%s must provide a valid "scalarType" instance of %s but got: %s',
67+
$this->name,
68+
ScalarType::class,
69+
Utils::printSafe($scalarType)
70+
)
71+
);
72+
} else {
73+
parent::assertValid();
74+
}
75+
}
76+
}

Definition/Type/SchemaDecorator.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Overblog\GraphQLBundle\Definition\Type;
44

5-
use GraphQL\Type\Definition\CustomScalarType;
65
use GraphQL\Type\Definition\EnumType;
76
use GraphQL\Type\Definition\InterfaceType;
87
use GraphQL\Type\Definition\ObjectType;
@@ -84,7 +83,12 @@ private function decorateInterfaceOrUnionType($type, ResolverMapInterface $resol
8483

8584
private function decorateCustomScalarType(CustomScalarType $type, ResolverMapInterface $resolverMap)
8685
{
87-
static $allowedFields = [ResolverMapInterface::SERIALIZE, ResolverMapInterface::PARSE_VALUE, ResolverMapInterface::PARSE_LITERAL];
86+
static $allowedFields = [
87+
ResolverMapInterface::SCALAR_TYPE,
88+
ResolverMapInterface::SERIALIZE,
89+
ResolverMapInterface::PARSE_VALUE,
90+
ResolverMapInterface::PARSE_LITERAL,
91+
];
8892

8993
foreach ($allowedFields as $fieldName) {
9094
$this->configTypeMapping($type, $resolverMap, $fieldName);

Generator/TypeGenerator.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Composer\Autoload\ClassLoader;
66
use Overblog\GraphQLBundle\Config\Processor;
77
use Overblog\GraphQLBundle\Definition\Argument;
8+
use Overblog\GraphQLBundle\Definition\Type\CustomScalarType;
89
use Overblog\GraphQLGenerator\Generator\TypeGenerator as BaseTypeGenerator;
910
use Symfony\Component\Filesystem\Filesystem;
1011

@@ -141,6 +142,25 @@ function ($childrenComplexity, $args = []) <closureUseStatements>{
141142
return $code;
142143
}
143144

145+
/**
146+
* @param array $value
147+
*
148+
* @return string
149+
*/
150+
protected function generateScalarType(array $value)
151+
{
152+
return $this->callableCallbackFromArrayValue($value, 'scalarType');
153+
}
154+
155+
protected function generateParentClassName(array $config)
156+
{
157+
if ('custom-scalar' === $config['type']) {
158+
return $this->shortenClassName(CustomScalarType::class);
159+
} else {
160+
return parent::generateParentClassName($config);
161+
}
162+
}
163+
144164
public function compile($mode)
145165
{
146166
$cacheDir = $this->getCacheDir();

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'), 2)[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->handleUploadedFiles($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: 7 additions & 2 deletions
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
*
@@ -31,7 +33,7 @@ public function parse(Request $request)
3133
private function getParsedBody(Request $request)
3234
{
3335
$body = $request->getContent();
34-
$type = explode(';', $request->headers->get('content-type'))[0];
36+
$type = explode(';', $request->headers->get('content-type'), 2)[0];
3537

3638
switch ($type) {
3739
// Plain string
@@ -54,10 +56,13 @@ private function getParsedBody(Request $request)
5456

5557
// URL-encoded query-string
5658
case static::CONTENT_TYPE_FORM:
57-
case static::CONTENT_TYPE_FORM_DATA:
5859
$parsedBody = $request->request->all();
5960
break;
6061

62+
case static::CONTENT_TYPE_FORM_DATA:
63+
$parsedBody = $this->handleUploadedFiles($request->request->all(), $request->files->all());
64+
break;
65+
6166
default:
6267
$parsedBody = [];
6368
break;

Request/UploadParserTrait.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
private function handleUploadedFiles(array $parameters, array $files)
11+
{
12+
$payload = $this->normalized($parameters);
13+
if ($this->isUploadPayload($payload)) {
14+
return $this->bindUploadedFiles($payload['operations'], $payload['map'], $files);
15+
} else {
16+
return $parameters;
17+
}
18+
}
19+
20+
private function bindUploadedFiles(array $operations, array $map, array $files)
21+
{
22+
$accessor = PropertyAccess::createPropertyAccessorBuilder()
23+
->enableExceptionOnInvalidIndex()
24+
->getPropertyAccessor();
25+
26+
foreach ($map as $fileName => $locations) {
27+
foreach ($locations as $location) {
28+
$fileKey = sprintf('[%s]', $fileName);
29+
if (!$accessor->isReadable($files, $fileKey)) {
30+
throw new BadRequestHttpException(sprintf('File %s is missing in the request.', json_encode($fileName)));
31+
}
32+
$file = $accessor->getValue($files, $fileKey);
33+
$locationKey = $this->locationToPropertyAccessPath($location);
34+
if (!$accessor->isReadable($operations, $locationKey)) {
35+
throw new BadRequestHttpException(sprintf('Map entry %s could not be localized in operations.', json_encode($location)));
36+
}
37+
$accessor->setValue($operations, $locationKey, $file);
38+
}
39+
}
40+
41+
return $operations;
42+
}
43+
44+
private function isUploadPayload(array $payload)
45+
{
46+
if (isset($payload['operations']) && isset($payload['map']) && is_array($payload['operations']) && is_array($payload['map'])) {
47+
$payloadKeys = array_keys($payload);
48+
// the specs says that operations must be place before map
49+
$operationsPosition = array_search('operations', $payloadKeys);
50+
$mapPosition = array_search('map', $payloadKeys);
51+
52+
return $operationsPosition < $mapPosition;
53+
} else {
54+
return false;
55+
}
56+
}
57+
58+
private function locationToPropertyAccessPath($location)
59+
{
60+
return array_reduce(
61+
explode('.', $location),
62+
function ($carry, $item) {
63+
return sprintf('%s[%s]', $carry, $item);
64+
}
65+
);
66+
}
67+
68+
private function normalized(array $parsedBody)
69+
{
70+
foreach (['operations', 'map'] as $key) {
71+
if (isset($parsedBody[$key]) && is_string($parsedBody[$key])) {
72+
$parsedBody[$key] = json_decode($parsedBody[$key], true);
73+
}
74+
}
75+
76+
return $parsedBody;
77+
}
78+
}

Resolver/ResolverMapInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ interface ResolverMapInterface
1010
const RESOLVE_FIELD = '__resolveField';
1111
const IS_TYPEOF = '__isTypeOf';
1212
// custom scalar
13+
const SCALAR_TYPE = '__scalarType';
1314
const SERIALIZE = '__serialize';
1415
const PARSE_VALUE = '__parseValue';
1516
const PARSE_LITERAL = '__parseLiteral';

Resources/doc/definitions/resolver-map.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,12 @@ and override `map` method and return an `array` or any `ArrayAccess` and `Traver
3939
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::RESOLVE_FIELD` equivalent to `resolveField`.
4040
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::IS_TYPE_OF` equivalent to `isTypeOf`.
4141
* [Custom scalar](type-system/scalars.md#custom-scalar) type
42-
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SERIALIZE` equivalent to `serialize`
43-
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_VALUE` equivalent to `parseValue`
44-
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_LITERAL` equivalent to `parseLiteral`
42+
- Direct usage:
43+
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SERIALIZE` equivalent to `serialize`
44+
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_VALUE` equivalent to `parseValue`
45+
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::PARSE_LITERAL` equivalent to `parseLiteral`
46+
- Reusing an existing scalar type
47+
- `Overblog\GraphQLBundle\Resolver\ResolverMapInterface::SCALAR_TYPE` equivalent to `scalarType`
4548

4649
Usage
4750
-----
@@ -109,6 +112,8 @@ class MyResolverMap extends ResolverMap
109112
return str_replace(' Formatted Baz', '', $valueNode->value);
110113
},
111114
],
115+
// or reuse an existing scalar (note: description and name will be override by decorator)
116+
//'Baz' => [self::SCALAR_TYPE => function () { return new FooScalarType(); }],
112117
];
113118
}
114119
}

0 commit comments

Comments
 (0)