Skip to content

Commit 4e299f2

Browse files
authored
Merge 78c88ca into 4b752f1
2 parents 4b752f1 + 78c88ca commit 4e299f2

27 files changed

+858
-39
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
$this->initOptionalFunctions();
18+
}
19+
20+
/**
21+
* {@inheritdoc}
22+
*/
23+
public function serialize($value)
24+
{
25+
return $this->call('serialize', $value);
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function parseValue($value)
32+
{
33+
return $this->call('parseValue', $value);
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ $valueNode)
40+
{
41+
return $this->call('parseLiteral', $valueNode);
42+
}
43+
44+
private function call($type, $value)
45+
{
46+
if (isset($this->config['scalarType'])) {
47+
$scalarType = $this->config['scalarType'];
48+
$scalarType = is_callable($scalarType) ? $scalarType() : $scalarType;
49+
50+
return call_user_func([$scalarType, $type], $value);
51+
} else {
52+
return parent::$type($value);
53+
}
54+
}
55+
56+
private function initOptionalFunctions()
57+
{
58+
foreach (['parseLiteral', 'parseValue'] as $field) {
59+
if (!isset($this->config[$field])) {
60+
$this->config[$field] = static function () {
61+
return null;
62+
};
63+
}
64+
}
65+
}
66+
67+
public function assertValid()
68+
{
69+
if (isset($this->config['scalarType'])) {
70+
$scalarType = $this->config['scalarType'];
71+
if (is_callable($scalarType)) {
72+
$scalarType = $scalarType();
73+
}
74+
75+
Utils::invariant(
76+
$scalarType instanceof ScalarType,
77+
sprintf(
78+
'%s must provide a valid "scalarType" instance of %s but got: %s',
79+
$this->name,
80+
ScalarType::class,
81+
Utils::printSafe($scalarType)
82+
)
83+
);
84+
} else {
85+
parent::assertValid();
86+
}
87+
}
88+
}

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'))[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+
}

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)