1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Overblog\GraphQLBundle\Config\Parser; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Annotations\AnnotationReader; |
8
|
|
|
use Doctrine\Common\Annotations\AnnotationRegistry; |
9
|
|
|
use Overblog\GraphQLBundle\Annotation as GQL; |
10
|
|
|
use Overblog\GraphQLBundle\Relay\Connection\ConnectionInterface; |
11
|
|
|
use Overblog\GraphQLBundle\Relay\Connection\EdgeInterface; |
12
|
|
|
use Symfony\Component\Config\Resource\FileResource; |
13
|
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder; |
14
|
|
|
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; |
15
|
|
|
|
16
|
|
|
class AnnotationParser implements PreParserInterface |
17
|
|
|
{ |
18
|
|
|
public const CLASSESMAP_CONTAINER_PARAMETER = 'overblog_graphql_types.classes_map'; |
19
|
|
|
|
20
|
|
|
private static $annotationReader = null; |
21
|
|
|
private static $classesMap = []; |
22
|
|
|
private static $providers = []; |
23
|
|
|
private static $doctrineMapping = []; |
24
|
|
|
|
25
|
|
|
public const GQL_SCALAR = 'scalar'; |
26
|
|
|
public const GQL_ENUM = 'enum'; |
27
|
|
|
public const GQL_TYPE = 'type'; |
28
|
|
|
public const GQL_INPUT = 'input'; |
29
|
|
|
public const GQL_UNION = 'union'; |
30
|
|
|
public const GQL_INTERFACE = 'interface'; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @see https://facebook.github.io/graphql/draft/#sec-Input-and-Output-Types |
34
|
|
|
*/ |
35
|
|
|
protected static $validInputTypes = [self::GQL_SCALAR, self::GQL_ENUM, self::GQL_INPUT]; |
36
|
|
|
protected static $validOutputTypes = [self::GQL_SCALAR, self::GQL_TYPE, self::GQL_INTERFACE, self::GQL_UNION, self::GQL_ENUM]; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* {@inheritdoc} |
40
|
|
|
* |
41
|
|
|
* @throws \ReflectionException |
42
|
|
|
* @throws InvalidArgumentException |
43
|
|
|
*/ |
44
|
20 |
|
public static function preParse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): void |
45
|
|
|
{ |
46
|
20 |
|
self::proccessFile($file, $container, $configs, true); |
47
|
20 |
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* {@inheritdoc} |
51
|
|
|
* |
52
|
|
|
* @throws \ReflectionException |
53
|
|
|
* @throws InvalidArgumentException |
54
|
|
|
*/ |
55
|
20 |
|
public static function parse(\SplFileInfo $file, ContainerBuilder $container, array $configs = []): array |
56
|
|
|
{ |
57
|
20 |
|
return self::proccessFile($file, $container, $configs); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Clear the Annotation parser. |
62
|
|
|
*/ |
63
|
19 |
|
public static function clear(): void |
64
|
|
|
{ |
65
|
19 |
|
self::$classesMap = []; |
66
|
19 |
|
self::$providers = []; |
67
|
19 |
|
self::$annotationReader = null; |
68
|
19 |
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Process a file. |
72
|
|
|
* |
73
|
|
|
* @param \SplFileInfo $file |
74
|
|
|
* @param ContainerBuilder $container |
75
|
|
|
* @param bool $resolveClassMap |
76
|
|
|
* |
77
|
|
|
* @throws \ReflectionException |
78
|
|
|
* @throws InvalidArgumentException |
79
|
|
|
*/ |
80
|
20 |
|
public static function proccessFile(\SplFileInfo $file, ContainerBuilder $container, array $configs, bool $resolveClassMap = false): array |
81
|
|
|
{ |
82
|
20 |
|
self::$doctrineMapping = $configs['doctrine']['types_mapping']; |
83
|
|
|
|
84
|
20 |
|
$rootQueryType = $configs['definitions']['schema']['default']['query'] ?? false; |
85
|
20 |
|
$rootMutationType = $configs['definitions']['schema']['default']['mutation'] ?? false; |
86
|
|
|
|
87
|
20 |
|
$container->addResource(new FileResource($file->getRealPath())); |
88
|
|
|
|
89
|
20 |
|
if (!$resolveClassMap) { |
90
|
20 |
|
$container->setParameter(self::CLASSESMAP_CONTAINER_PARAMETER, self::$classesMap); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
try { |
94
|
20 |
|
$fileContent = \file_get_contents($file->getRealPath()); |
95
|
|
|
|
96
|
20 |
|
$shortClassName = \substr($file->getFilename(), 0, -4); |
97
|
20 |
|
if (\preg_match('#namespace (.+);#', $fileContent, $namespace)) { |
98
|
20 |
|
$className = $namespace[1].'\\'.$shortClassName; |
99
|
20 |
|
$namespace = $namespace[1]; |
100
|
|
|
} else { |
101
|
|
|
$className = $shortClassName; |
102
|
|
|
} |
103
|
|
|
|
104
|
20 |
|
$reflexionEntity = new \ReflectionClass($className); |
105
|
|
|
|
106
|
20 |
|
$classAnnotations = self::getAnnotationReader()->getClassAnnotations($reflexionEntity); |
107
|
|
|
|
108
|
20 |
|
$properties = []; |
109
|
20 |
|
foreach ($reflexionEntity->getProperties() as $property) { |
110
|
19 |
|
$properties[$property->getName()] = ['property' => $property, 'annotations' => self::getAnnotationReader()->getPropertyAnnotations($property)]; |
111
|
|
|
} |
112
|
|
|
|
113
|
20 |
|
$methods = []; |
114
|
20 |
|
foreach ($reflexionEntity->getMethods() as $method) { |
115
|
19 |
|
$methods[$method->getName()] = ['method' => $method, 'annotations' => self::getAnnotationReader()->getMethodAnnotations($method)]; |
116
|
|
|
} |
117
|
|
|
|
118
|
20 |
|
$gqlTypes = []; |
119
|
|
|
|
120
|
20 |
|
foreach ($classAnnotations as $classAnnotation) { |
121
|
20 |
|
$gqlConfiguration = $gqlType = $gqlName = false; |
122
|
|
|
|
123
|
|
|
switch (true) { |
124
|
20 |
|
case $classAnnotation instanceof GQL\Type: |
125
|
20 |
|
$gqlType = self::GQL_TYPE; |
126
|
20 |
|
$gqlName = $classAnnotation->name ?: $shortClassName; |
127
|
|
|
|
128
|
20 |
|
if (!$resolveClassMap) { |
129
|
20 |
|
$isRootQuery = ($rootQueryType && $gqlName === $rootQueryType); |
130
|
20 |
|
$isRootMutation = ($rootMutationType && $gqlName === $rootMutationType); |
131
|
20 |
|
$currentValue = ($isRootQuery || $isRootMutation) ? \sprintf("service('%s')", self::formatNamespaceForExpression($className)) : 'value'; |
132
|
|
|
|
133
|
20 |
|
$gqlConfiguration = self::getGraphqlType($classAnnotation, $classAnnotations, $properties, $methods, $namespace, $currentValue); |
134
|
20 |
|
$providerFields = self::getGraphqlFieldsFromProviders($namespace, $className, $isRootMutation ? 'Mutation' : 'Query', $gqlName, ($isRootQuery || $isRootMutation)); |
135
|
20 |
|
$gqlConfiguration['config']['fields'] = $providerFields + $gqlConfiguration['config']['fields']; |
136
|
|
|
|
137
|
20 |
|
if ($classAnnotation instanceof GQL\Relay\Edge) { |
138
|
19 |
|
if (!$reflexionEntity->implementsInterface(EdgeInterface::class)) { |
139
|
|
|
throw new InvalidArgumentException(\sprintf('The annotation @Edge on class "%s" can only be used on class implementing the EdgeInterface.', $className)); |
140
|
|
|
} |
141
|
19 |
|
if (!isset($gqlConfiguration['config']['builders'])) { |
142
|
19 |
|
$gqlConfiguration['config']['builders'] = []; |
143
|
|
|
} |
144
|
19 |
|
\array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]]); |
145
|
|
|
} |
146
|
|
|
|
147
|
20 |
|
if ($classAnnotation instanceof GQL\Relay\Connection) { |
148
|
19 |
|
if (!$reflexionEntity->implementsInterface(ConnectionInterface::class)) { |
149
|
|
|
throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" can only be used on class implementing the ConnectionInterface.', $className)); |
150
|
|
|
} |
151
|
|
|
|
152
|
19 |
|
if (!($classAnnotation->edge xor $classAnnotation->node)) { |
153
|
|
|
throw new InvalidArgumentException(\sprintf('The annotation @Connection on class "%s" is invalid. You must define the "edge" OR the "node" attribute.', $className)); |
154
|
|
|
} |
155
|
|
|
|
156
|
19 |
|
$edgeType = $classAnnotation->edge; |
157
|
19 |
|
if (!$edgeType) { |
158
|
19 |
|
$edgeType = \sprintf('%sEdge', $gqlName); |
159
|
19 |
|
$gqlTypes[$edgeType] = [ |
160
|
19 |
|
'type' => 'object', |
161
|
|
|
'config' => [ |
162
|
|
|
'builders' => [ |
163
|
19 |
|
['builder' => 'relay-edge', 'builderConfig' => ['nodeType' => $classAnnotation->node]], |
164
|
|
|
], |
165
|
|
|
], |
166
|
|
|
]; |
167
|
|
|
} |
168
|
19 |
|
if (!isset($gqlConfiguration['config']['builders'])) { |
169
|
19 |
|
$gqlConfiguration['config']['builders'] = []; |
170
|
|
|
} |
171
|
19 |
|
\array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]); |
172
|
|
|
} |
173
|
|
|
} |
174
|
20 |
|
break; |
175
|
19 |
|
case $classAnnotation instanceof GQL\Input: |
176
|
19 |
|
$gqlType = self::GQL_INPUT; |
177
|
19 |
|
$gqlName = $classAnnotation->name ?: self::suffixName($shortClassName, 'Input'); |
178
|
19 |
|
if (!$resolveClassMap) { |
179
|
19 |
|
$gqlConfiguration = self::getGraphqlInput($classAnnotation, $classAnnotations, $properties, $namespace); |
180
|
|
|
} |
181
|
19 |
|
break; |
182
|
19 |
|
case $classAnnotation instanceof GQL\Scalar: |
183
|
19 |
|
$gqlType = self::GQL_SCALAR; |
184
|
19 |
|
if (!$resolveClassMap) { |
185
|
19 |
|
$gqlConfiguration = self::getGraphqlScalar($className, $classAnnotation, $classAnnotations); |
186
|
|
|
} |
187
|
19 |
|
break; |
188
|
19 |
|
case $classAnnotation instanceof GQL\Enum: |
189
|
19 |
|
$gqlType = self::GQL_ENUM; |
190
|
19 |
|
if (!$resolveClassMap) { |
191
|
19 |
|
$gqlConfiguration = self::getGraphqlEnum($classAnnotation, $classAnnotations, $reflexionEntity->getConstants()); |
192
|
|
|
} |
193
|
19 |
|
break; |
194
|
19 |
|
case $classAnnotation instanceof GQL\Union: |
195
|
19 |
|
$gqlType = self::GQL_UNION; |
196
|
19 |
|
if (!$resolveClassMap) { |
197
|
19 |
|
$gqlConfiguration = self::getGraphqlUnion($className, $classAnnotation, $classAnnotations, $methods); |
198
|
|
|
} |
199
|
19 |
|
break; |
200
|
19 |
|
case $classAnnotation instanceof GQL\TypeInterface: |
201
|
19 |
|
$gqlType = self::GQL_INTERFACE; |
202
|
19 |
|
if (!$resolveClassMap) { |
203
|
19 |
|
$gqlConfiguration = self::getGraphqlInterface($classAnnotation, $classAnnotations, $properties, $methods, $namespace); |
204
|
|
|
} |
205
|
19 |
|
break; |
206
|
19 |
|
case $classAnnotation instanceof GQL\Provider: |
207
|
19 |
|
if ($resolveClassMap) { |
208
|
19 |
|
self::$providers[$className] = ['annotation' => $classAnnotation, 'methods' => $methods]; |
209
|
|
|
} |
210
|
19 |
|
break; |
211
|
|
|
default: |
212
|
19 |
|
continue 2; |
213
|
|
|
} |
214
|
|
|
|
215
|
20 |
|
if ($gqlType) { |
216
|
20 |
|
if (!$gqlName) { |
217
|
19 |
|
$gqlName = $classAnnotation->name ?: $shortClassName; |
218
|
|
|
} |
219
|
|
|
|
220
|
20 |
|
if ($resolveClassMap) { |
221
|
20 |
|
if (isset(self::$classesMap[$gqlName])) { |
222
|
1 |
|
throw new InvalidArgumentException(\sprintf('The GraphQL type "%s" has already been registered in class "%s"', $gqlName, self::$classesMap[$gqlName]['class'])); |
223
|
|
|
} |
224
|
20 |
|
self::$classesMap[$gqlName] = ['type' => $gqlType, 'class' => $className]; |
225
|
|
|
} else { |
226
|
20 |
|
$gqlTypes = [$gqlName => $gqlConfiguration] + $gqlTypes; |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
} |
230
|
|
|
|
231
|
20 |
|
return $resolveClassMap ? self::$classesMap : $gqlTypes; |
232
|
8 |
|
} catch (\InvalidArgumentException $e) { |
233
|
8 |
|
throw new InvalidArgumentException(\sprintf('Failed to parse GraphQL annotations from file "%s".', $file), $e->getCode(), $e); |
234
|
|
|
} |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Retrieve annotation reader. |
239
|
|
|
* |
240
|
|
|
* @return AnnotationReader |
241
|
|
|
*/ |
242
|
20 |
|
private static function getAnnotationReader() |
243
|
|
|
{ |
244
|
20 |
|
if (null === self::$annotationReader) { |
245
|
19 |
|
if (!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationReader') || |
246
|
19 |
|
!\class_exists('\\Doctrine\\Common\\Annotations\\AnnotationRegistry')) { |
247
|
|
|
throw new \Exception('In order to use graphql annotation, you need to require doctrine annotations'); |
248
|
|
|
} |
249
|
|
|
|
250
|
19 |
|
AnnotationRegistry::registerLoader('class_exists'); |
|
|
|
|
251
|
19 |
|
self::$annotationReader = new AnnotationReader(); |
252
|
|
|
} |
253
|
|
|
|
254
|
20 |
|
return self::$annotationReader; |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Create a GraphQL Type configuration from annotations on class, properties and methods. |
259
|
|
|
* |
260
|
|
|
* @param GQL\Type $typeAnnotation |
261
|
|
|
* @param array $classAnnotations |
262
|
|
|
* @param array $properties |
263
|
|
|
* @param array $methods |
264
|
|
|
* @param string $namespace |
265
|
|
|
* @param string $currentValue |
266
|
|
|
* |
267
|
|
|
* @return array |
268
|
|
|
*/ |
269
|
20 |
|
private static function getGraphqlType(GQL\Type $typeAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace, string $currentValue) |
270
|
|
|
{ |
271
|
20 |
|
$typeConfiguration = []; |
272
|
|
|
|
273
|
20 |
|
$fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, false, false, $currentValue); |
274
|
20 |
|
$fields = self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true, $currentValue) + $fields; |
275
|
|
|
|
276
|
20 |
|
$typeConfiguration['fields'] = $fields; |
277
|
20 |
|
$typeConfiguration = self::getDescriptionConfiguration($classAnnotations) + $typeConfiguration; |
278
|
|
|
|
279
|
20 |
|
if ($typeAnnotation->interfaces) { |
|
|
|
|
280
|
19 |
|
$typeConfiguration['interfaces'] = $typeAnnotation->interfaces; |
281
|
|
|
} |
282
|
|
|
|
283
|
20 |
|
if ($typeAnnotation->resolveField) { |
284
|
19 |
|
$typeConfiguration['resolveField'] = self::formatExpression($typeAnnotation->resolveField); |
285
|
|
|
} |
286
|
|
|
|
287
|
20 |
|
if ($typeAnnotation->builders && \count($typeAnnotation->builders) > 0) { |
|
|
|
|
288
|
|
|
$typeConfiguration['builders'] = \array_map(function ($fieldsBuilderAnnotation) { |
289
|
19 |
|
return ['builder' => $fieldsBuilderAnnotation->builder, 'builderConfig' => $fieldsBuilderAnnotation->builderConfig]; |
290
|
19 |
|
}, $typeAnnotation->builders); |
291
|
|
|
} |
292
|
|
|
|
293
|
20 |
|
$publicAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\IsPublic'); |
294
|
20 |
|
if ($publicAnnotation) { |
295
|
19 |
|
$typeConfiguration['fieldsDefaultPublic'] = self::formatExpression($publicAnnotation->value); |
296
|
|
|
} |
297
|
|
|
|
298
|
20 |
|
$accessAnnotation = self::getFirstAnnotationMatching($classAnnotations, 'Overblog\GraphQLBundle\Annotation\Access'); |
299
|
20 |
|
if ($accessAnnotation) { |
300
|
19 |
|
$typeConfiguration['fieldsDefaultAccess'] = self::formatExpression($accessAnnotation->value); |
301
|
|
|
} |
302
|
|
|
|
303
|
20 |
|
return ['type' => $typeAnnotation->isRelay ? 'relay-mutation-payload' : 'object', 'config' => $typeConfiguration]; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Create a GraphQL Interface type configuration from annotations on properties. |
308
|
|
|
* |
309
|
|
|
* @param string $shortClassName |
310
|
|
|
* @param GQL\Interface $interfaceAnnotation |
|
|
|
|
311
|
|
|
* @param array $properties |
312
|
|
|
* @param array $methods |
313
|
|
|
* @param string $namespace |
314
|
|
|
* |
315
|
|
|
* @return array |
316
|
|
|
*/ |
317
|
19 |
|
private static function getGraphqlInterface(GQL\TypeInterface $interfaceAnnotation, array $classAnnotations, array $properties, array $methods, string $namespace) |
318
|
|
|
{ |
319
|
19 |
|
$interfaceConfiguration = []; |
320
|
|
|
|
321
|
19 |
|
$fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties); |
322
|
19 |
|
$fields = self::getGraphqlFieldsFromAnnotations($namespace, $methods, false, true) + $fields; |
323
|
|
|
|
324
|
19 |
|
$interfaceConfiguration['fields'] = $fields; |
325
|
19 |
|
$interfaceConfiguration = self::getDescriptionConfiguration($classAnnotations) + $interfaceConfiguration; |
326
|
|
|
|
327
|
19 |
|
$interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType); |
328
|
|
|
|
329
|
19 |
|
return ['type' => 'interface', 'config' => $interfaceConfiguration]; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
/** |
333
|
|
|
* Create a GraphQL Input type configuration from annotations on properties. |
334
|
|
|
* |
335
|
|
|
* @param string $shortClassName |
336
|
|
|
* @param GQL\Input $inputAnnotation |
337
|
|
|
* @param array $properties |
338
|
|
|
* @param string $namespace |
339
|
|
|
* |
340
|
|
|
* @return array |
341
|
|
|
*/ |
342
|
19 |
|
private static function getGraphqlInput(GQL\Input $inputAnnotation, array $classAnnotations, array $properties, string $namespace) |
343
|
|
|
{ |
344
|
19 |
|
$inputConfiguration = []; |
345
|
19 |
|
$fields = self::getGraphqlFieldsFromAnnotations($namespace, $properties, true); |
346
|
|
|
|
347
|
19 |
|
$inputConfiguration['fields'] = $fields; |
348
|
19 |
|
$inputConfiguration = self::getDescriptionConfiguration($classAnnotations) + $inputConfiguration; |
349
|
|
|
|
350
|
19 |
|
return ['type' => $inputAnnotation->isRelay ? 'relay-mutation-input' : 'input-object', 'config' => $inputConfiguration]; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Get a Graphql scalar configuration from given scalar annotation. |
355
|
|
|
* |
356
|
|
|
* @param string $shortClassName |
357
|
|
|
* @param string $className |
358
|
|
|
* @param GQL\Scalar $scalarAnnotation |
359
|
|
|
* @param array $classAnnotations |
360
|
|
|
* |
361
|
|
|
* @return array |
362
|
|
|
*/ |
363
|
19 |
|
private static function getGraphqlScalar(string $className, GQL\Scalar $scalarAnnotation, array $classAnnotations) |
364
|
|
|
{ |
365
|
19 |
|
$scalarConfiguration = []; |
366
|
|
|
|
367
|
19 |
|
if ($scalarAnnotation->scalarType) { |
368
|
19 |
|
$scalarConfiguration['scalarType'] = self::formatExpression($scalarAnnotation->scalarType); |
369
|
|
|
} else { |
370
|
|
|
$scalarConfiguration = [ |
371
|
19 |
|
'serialize' => [$className, 'serialize'], |
372
|
19 |
|
'parseValue' => [$className, 'parseValue'], |
373
|
19 |
|
'parseLiteral' => [$className, 'parseLiteral'], |
374
|
|
|
]; |
375
|
|
|
} |
376
|
|
|
|
377
|
19 |
|
$scalarConfiguration = self::getDescriptionConfiguration($classAnnotations) + $scalarConfiguration; |
378
|
|
|
|
379
|
19 |
|
return ['type' => 'custom-scalar', 'config' => $scalarConfiguration]; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Get a Graphql Enum configuration from given enum annotation. |
384
|
|
|
* |
385
|
|
|
* @param string $shortClassName |
386
|
|
|
* @param GQL\Enum $enumAnnotation |
387
|
|
|
* @param array $classAnnotations |
388
|
|
|
* @param array $constants |
389
|
|
|
* |
390
|
|
|
* @return array |
391
|
|
|
*/ |
392
|
19 |
|
private static function getGraphqlEnum(GQL\Enum $enumAnnotation, array $classAnnotations, array $constants) |
393
|
|
|
{ |
394
|
19 |
|
$enumValues = $enumAnnotation->values ? $enumAnnotation->values : []; |
395
|
|
|
|
396
|
19 |
|
$values = []; |
397
|
|
|
|
398
|
19 |
|
foreach ($constants as $name => $value) { |
399
|
|
|
$valueAnnotation = \current(\array_filter($enumValues, function ($enumValueAnnotation) use ($name) { |
400
|
19 |
|
return $enumValueAnnotation->name == $name; |
401
|
19 |
|
})); |
402
|
19 |
|
$valueConfig = []; |
403
|
19 |
|
$valueConfig['value'] = $value; |
404
|
|
|
|
405
|
19 |
|
if ($valueAnnotation && $valueAnnotation->description) { |
406
|
19 |
|
$valueConfig['description'] = $valueAnnotation->description; |
407
|
|
|
} |
408
|
|
|
|
409
|
19 |
|
if ($valueAnnotation && $valueAnnotation->deprecationReason) { |
410
|
19 |
|
$valueConfig['deprecationReason'] = $valueAnnotation->deprecationReason; |
411
|
|
|
} |
412
|
|
|
|
413
|
19 |
|
$values[$name] = $valueConfig; |
414
|
|
|
} |
415
|
|
|
|
416
|
19 |
|
$enumConfiguration = ['values' => $values]; |
417
|
19 |
|
$enumConfiguration = self::getDescriptionConfiguration($classAnnotations) + $enumConfiguration; |
418
|
|
|
|
419
|
19 |
|
return ['type' => 'enum', 'config' => $enumConfiguration]; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Get a Graphql Union configuration from given union annotation. |
424
|
|
|
* |
425
|
|
|
* @param string $className |
426
|
|
|
* @param GQL\Union $unionAnnotation |
427
|
|
|
* @param array $classAnnotations |
428
|
|
|
* @param array $methods |
429
|
|
|
* |
430
|
|
|
* @return array |
431
|
|
|
*/ |
432
|
19 |
|
private static function getGraphqlUnion(string $className, GQL\Union $unionAnnotation, array $classAnnotations, array $methods): array |
433
|
|
|
{ |
434
|
19 |
|
$unionConfiguration = ['types' => $unionAnnotation->types]; |
435
|
19 |
|
$unionConfiguration = self::getDescriptionConfiguration($classAnnotations) + $unionConfiguration; |
436
|
|
|
|
437
|
19 |
|
if ($unionAnnotation->resolveType) { |
438
|
19 |
|
$unionConfiguration['resolveType'] = self::formatExpression($unionAnnotation->resolveType); |
439
|
|
|
} else { |
440
|
19 |
|
if (isset($methods['resolveType'])) { |
441
|
19 |
|
$method = $methods['resolveType']['method']; |
442
|
19 |
|
if ($method->isStatic() && $method->isPublic()) { |
443
|
19 |
|
$unionConfiguration['resolveType'] = self::formatExpression(\sprintf("@=call('%s::%s', [service('overblog_graphql.type_resolver'), value], true)", self::formatNamespaceForExpression($className), 'resolveType')); |
444
|
|
|
} else { |
445
|
19 |
|
throw new InvalidArgumentException(\sprintf('The "resolveType()" method on class must be static and public. Or you must define a "resolveType" attribute on the @Union annotation.')); |
446
|
|
|
} |
447
|
|
|
} else { |
448
|
1 |
|
throw new InvalidArgumentException(\sprintf('The annotation @Union has no "resolveType" attribute and the related class has no "resolveType()" public static method. You need to define of them.')); |
449
|
|
|
} |
450
|
|
|
} |
451
|
|
|
|
452
|
19 |
|
return ['type' => 'union', 'config' => $unionConfiguration]; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Create Graphql fields configuration based on annotations. |
457
|
|
|
* |
458
|
|
|
* @param string $namespace |
459
|
|
|
* @param array $propertiesOrMethods |
460
|
|
|
* @param bool $isInput |
461
|
|
|
* @param bool $isMethod |
462
|
|
|
* @param string $currentValue |
463
|
|
|
* |
464
|
|
|
* @return array |
465
|
|
|
*/ |
466
|
20 |
|
private static function getGraphqlFieldsFromAnnotations(string $namespace, array $propertiesOrMethods, bool $isInput = false, bool $isMethod = false, string $currentValue = 'value', string $fieldAnnotationName = 'Field'): array |
467
|
|
|
{ |
468
|
20 |
|
$fields = []; |
469
|
20 |
|
foreach ($propertiesOrMethods as $target => $config) { |
470
|
19 |
|
$annotations = $config['annotations']; |
471
|
19 |
|
$method = $isMethod ? $config['method'] : false; |
472
|
19 |
|
$property = $isMethod ? false : $config['property']; |
|
|
|
|
473
|
|
|
|
474
|
19 |
|
$fieldAnnotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\GraphQLBundle\Annotation\%s', $fieldAnnotationName)); |
475
|
19 |
|
$accessAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Access'); |
476
|
19 |
|
$publicAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\IsPublic'); |
477
|
|
|
|
478
|
19 |
|
if (!$fieldAnnotation) { |
479
|
19 |
|
if ($accessAnnotation || $publicAnnotation) { |
480
|
1 |
|
throw new InvalidArgumentException(\sprintf('The annotations "@Access" and/or "@Visible" defined on "%s" are only usable in addition of annotation "@Field"', $target)); |
481
|
|
|
} |
482
|
19 |
|
continue; |
483
|
|
|
} |
484
|
|
|
|
485
|
19 |
|
if ($isMethod && !$method->isPublic()) { |
486
|
1 |
|
throw new InvalidArgumentException(\sprintf('The Annotation "@Field" can only be applied to public method. The method "%s" is not public.', $target)); |
487
|
|
|
} |
488
|
|
|
|
489
|
|
|
// Ignore field with resolver when the type is an Input |
490
|
19 |
|
if ($fieldAnnotation->resolve && $isInput) { |
491
|
|
|
continue; |
492
|
|
|
} |
493
|
|
|
|
494
|
19 |
|
$fieldName = $target; |
495
|
19 |
|
$fieldType = $fieldAnnotation->type; |
496
|
19 |
|
$fieldConfiguration = []; |
497
|
19 |
|
if ($fieldType) { |
498
|
19 |
|
$resolvedType = self::resolveClassFromType($fieldType); |
499
|
19 |
|
if ($resolvedType) { |
500
|
19 |
|
if ($isInput && !\in_array($resolvedType['type'], self::$validInputTypes)) { |
501
|
|
|
throw new InvalidArgumentException(\sprintf('The type "%s" on "%s" is a "%s" not valid on an Input @Field. Only Input, Scalar and Enum are allowed.', $fieldType, $target, $resolvedType['type'])); |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
19 |
|
$fieldConfiguration['type'] = $fieldType; |
506
|
|
|
} |
507
|
|
|
|
508
|
19 |
|
$fieldConfiguration = self::getDescriptionConfiguration($annotations, true) + $fieldConfiguration; |
509
|
|
|
|
510
|
19 |
|
if (!$isInput) { |
511
|
19 |
|
$args = []; |
|
|
|
|
512
|
19 |
|
$args = self::getArgs($fieldAnnotation->args, $isMethod && !$fieldAnnotation->argsBuilder ? $method : null); |
|
|
|
|
513
|
|
|
|
514
|
19 |
|
if (!empty($args)) { |
515
|
19 |
|
$fieldConfiguration['args'] = $args; |
516
|
|
|
} |
517
|
|
|
|
518
|
19 |
|
$fieldName = $fieldAnnotation->name ?: $fieldName; |
519
|
|
|
|
520
|
19 |
|
if ($fieldAnnotation->resolve) { |
521
|
19 |
|
$fieldConfiguration['resolve'] = self::formatExpression($fieldAnnotation->resolve); |
522
|
|
|
} else { |
523
|
19 |
|
if ($isMethod) { |
524
|
19 |
|
$fieldConfiguration['resolve'] = self::formatExpression(\sprintf('call(%s.%s, %s)', $currentValue, $target, self::formatArgsForExpression($args))); |
525
|
|
|
} else { |
526
|
19 |
|
if ($fieldName !== $target || 'value' !== $currentValue) { |
527
|
|
|
$fieldConfiguration['resolve'] = self::formatExpression(\sprintf('%s.%s', $currentValue, $target)); |
528
|
|
|
} |
529
|
|
|
} |
530
|
|
|
} |
531
|
|
|
|
532
|
19 |
|
if ($fieldAnnotation->argsBuilder) { |
533
|
19 |
|
if (\is_string($fieldAnnotation->argsBuilder)) { |
534
|
|
|
$fieldConfiguration['argsBuilder'] = $fieldAnnotation->argsBuilder; |
535
|
19 |
|
} elseif (\is_array($fieldAnnotation->argsBuilder)) { |
536
|
19 |
|
list($builder, $builderConfig) = $fieldAnnotation->argsBuilder; |
537
|
19 |
|
$fieldConfiguration['argsBuilder'] = ['builder' => $builder, 'config' => $builderConfig]; |
538
|
|
|
} else { |
539
|
|
|
throw new InvalidArgumentException(\sprintf('The attribute "argsBuilder" on GraphQL annotation "@%s" defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', $fieldAnnotationName, $target)); |
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
|
543
|
19 |
|
if ($fieldAnnotation->fieldBuilder) { |
544
|
19 |
|
if (\is_string($fieldAnnotation->fieldBuilder)) { |
545
|
|
|
$fieldConfiguration['builder'] = $fieldAnnotation->fieldBuilder; |
546
|
19 |
|
} elseif (\is_array($fieldAnnotation->fieldBuilder)) { |
547
|
19 |
|
list($builder, $builderConfig) = $fieldAnnotation->fieldBuilder; |
548
|
19 |
|
$fieldConfiguration['builder'] = $builder; |
549
|
19 |
|
$fieldConfiguration['builderConfig'] = $builderConfig ?: []; |
550
|
|
|
} else { |
551
|
19 |
|
throw new InvalidArgumentException(\sprintf('The attribute "argsBuilder" on GraphQL annotation "@%s" defined on "%s" must be a string or an array where first index is the builder name and the second is the config.', $fieldAnnotationName, $target)); |
552
|
|
|
} |
553
|
|
|
} else { |
554
|
19 |
|
if (!$fieldType) { |
555
|
19 |
|
if ($isMethod) { |
556
|
19 |
|
if ($method->hasReturnType()) { |
557
|
|
|
try { |
558
|
19 |
|
$fieldConfiguration['type'] = self::resolveGraphqlTypeFromReflectionType($method->getReturnType(), self::$validOutputTypes); |
559
|
|
|
} catch (\Exception $e) { |
560
|
19 |
|
throw new InvalidArgumentException(\sprintf('The attribute "type" on GraphQL annotation "@%s" is missing on method "%s" and cannot be auto-guessed from type hint "%s"', $fieldAnnotationName, $target, (string) $method->getReturnType())); |
561
|
|
|
} |
562
|
|
|
} else { |
563
|
19 |
|
throw new InvalidArgumentException(\sprintf('The attribute "type" on GraphQL annotation "@%s" is missing on method "%s" and cannot be auto-guessed as there is not return type hint.', $fieldAnnotationName, $target)); |
564
|
|
|
} |
565
|
|
|
} else { |
566
|
|
|
try { |
567
|
19 |
|
$fieldConfiguration['type'] = self::guessType($namespace, $annotations); |
568
|
2 |
|
} catch (\Exception $e) { |
569
|
2 |
|
throw new InvalidArgumentException(\sprintf('The attribute "type" on "@%s" defined on "%s" is required and cannot be auto-guessed : %s.', $fieldAnnotationName, $target, $e->getMessage())); |
570
|
|
|
} |
571
|
|
|
} |
572
|
|
|
} |
573
|
|
|
} |
574
|
|
|
|
575
|
19 |
|
if ($accessAnnotation) { |
576
|
19 |
|
$fieldConfiguration['access'] = self::formatExpression($accessAnnotation->value); |
577
|
|
|
} |
578
|
|
|
|
579
|
19 |
|
if ($publicAnnotation) { |
580
|
19 |
|
$fieldConfiguration['public'] = self::formatExpression($publicAnnotation->value); |
581
|
|
|
} |
582
|
|
|
|
583
|
19 |
|
if ($fieldAnnotation->complexity) { |
584
|
19 |
|
$fieldConfiguration['complexity'] = self::formatExpression($fieldAnnotation->complexity); |
585
|
|
|
} |
586
|
|
|
} |
587
|
|
|
|
588
|
19 |
|
$fields[$fieldName] = $fieldConfiguration; |
589
|
|
|
} |
590
|
|
|
|
591
|
20 |
|
return $fields; |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
/** |
595
|
|
|
* Return fields config from Provider methods. |
596
|
|
|
* |
597
|
|
|
* @param string $className |
598
|
|
|
* @param array $methods |
599
|
|
|
* @param bool $isMutation |
600
|
|
|
* |
601
|
|
|
* @return array |
602
|
|
|
*/ |
603
|
20 |
|
private static function getGraphqlFieldsFromProviders(string $namespace, string $className, string $annotationName, string $targetType, bool $isRoot = false) |
604
|
|
|
{ |
605
|
20 |
|
$fields = []; |
606
|
20 |
|
foreach (self::$providers as $className => $configuration) { |
607
|
20 |
|
$providerMethods = $configuration['methods']; |
608
|
20 |
|
$providerAnnotation = $configuration['annotation']; |
609
|
|
|
|
610
|
20 |
|
$filteredMethods = []; |
611
|
20 |
|
foreach ($providerMethods as $methodName => $config) { |
612
|
20 |
|
$annotations = $config['annotations']; |
613
|
|
|
|
614
|
20 |
|
$annotation = self::getFirstAnnotationMatching($annotations, \sprintf('Overblog\\GraphQLBundle\\Annotation\\%s', $annotationName)); |
615
|
20 |
|
if (!$annotation) { |
616
|
20 |
|
continue; |
617
|
|
|
} |
618
|
|
|
|
619
|
20 |
|
$annotationTarget = 'Query' === $annotationName ? $annotation->targetType : null; |
620
|
20 |
|
if (!$annotationTarget && $isRoot) { |
621
|
19 |
|
$annotationTarget = $targetType; |
622
|
|
|
} |
623
|
|
|
|
624
|
20 |
|
if ($annotationTarget !== $targetType) { |
625
|
20 |
|
continue; |
626
|
|
|
} |
627
|
|
|
|
628
|
19 |
|
$filteredMethods[$methodName] = $config; |
629
|
|
|
} |
630
|
|
|
|
631
|
20 |
|
$currentValue = \sprintf("service('%s')", self::formatNamespaceForExpression($className)); |
632
|
20 |
|
$providerFields = self::getGraphqlFieldsFromAnnotations($namespace, $filteredMethods, false, true, $currentValue, $annotationName); |
633
|
20 |
|
foreach ($providerFields as $fieldName => $fieldConfig) { |
634
|
19 |
|
if ($providerAnnotation->prefix) { |
635
|
19 |
|
$fieldName = \sprintf('%s%s', $providerAnnotation->prefix, $fieldName); |
636
|
|
|
} |
637
|
19 |
|
$fields[$fieldName] = $fieldConfig; |
638
|
|
|
} |
639
|
|
|
} |
640
|
|
|
|
641
|
20 |
|
return $fields; |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
/** |
645
|
|
|
* Get the config for description & deprecation reason. |
646
|
|
|
* |
647
|
|
|
* @param array $annotations |
648
|
|
|
* @param bool $withDeprecation |
649
|
|
|
* |
650
|
|
|
* @return array |
651
|
|
|
*/ |
652
|
20 |
|
private static function getDescriptionConfiguration(array $annotations, bool $withDeprecation = false) |
653
|
|
|
{ |
654
|
20 |
|
$config = []; |
655
|
20 |
|
$descriptionAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Description'); |
656
|
20 |
|
if ($descriptionAnnotation) { |
657
|
19 |
|
$config['description'] = $descriptionAnnotation->value; |
658
|
|
|
} |
659
|
|
|
|
660
|
20 |
|
if ($withDeprecation) { |
661
|
19 |
|
$deprecatedAnnotation = self::getFirstAnnotationMatching($annotations, 'Overblog\GraphQLBundle\Annotation\Deprecated'); |
662
|
19 |
|
if ($deprecatedAnnotation) { |
663
|
19 |
|
$config['deprecationReason'] = $deprecatedAnnotation->value; |
664
|
|
|
} |
665
|
|
|
} |
666
|
|
|
|
667
|
20 |
|
return $config; |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
/** |
671
|
|
|
* Get args config from an array of @Arg annotation or by auto-guessing if a method is provided. |
672
|
|
|
* |
673
|
|
|
* @param array $args |
674
|
|
|
* @param \ReflectionMethod $method |
675
|
|
|
* |
676
|
|
|
* @return array |
677
|
|
|
*/ |
678
|
19 |
|
private static function getArgs(array $args = null, \ReflectionMethod $method = null) |
679
|
|
|
{ |
680
|
19 |
|
$config = []; |
681
|
19 |
|
if ($args && !empty($args)) { |
682
|
19 |
|
foreach ($args as $arg) { |
683
|
19 |
|
$config[$arg->name] = ['type' => $arg->type] + ($arg->description ? ['description' => $arg->description] : []); |
684
|
|
|
} |
685
|
19 |
|
} elseif ($method) { |
686
|
19 |
|
$config = self::guessArgs($method); |
687
|
|
|
} |
688
|
|
|
|
689
|
19 |
|
return $config; |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
/** |
693
|
|
|
* Format an array of args to a list of arguments in an expression. |
694
|
|
|
* |
695
|
|
|
* @param array $args |
696
|
|
|
* |
697
|
|
|
* @return string |
698
|
|
|
*/ |
699
|
19 |
|
private static function formatArgsForExpression(array $args) |
700
|
|
|
{ |
701
|
19 |
|
$mapping = []; |
702
|
19 |
|
foreach ($args as $name => $config) { |
703
|
19 |
|
$mapping[] = \sprintf('%s: "%s"', $name, $config['type']); |
704
|
|
|
} |
705
|
|
|
|
706
|
19 |
|
return \sprintf('arguments({%s}, args)', \implode(', ', $mapping)); |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
/** |
710
|
|
|
* Format a namespace to be used in an expression (double escape). |
711
|
|
|
* |
712
|
|
|
* @param string $namespace |
713
|
|
|
* |
714
|
|
|
* @return string |
715
|
|
|
*/ |
716
|
20 |
|
private static function formatNamespaceForExpression(string $namespace) |
717
|
|
|
{ |
718
|
20 |
|
return \str_replace('\\', '\\\\', $namespace); |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
/** |
722
|
|
|
* Get the first annotation matching given class. |
723
|
|
|
* |
724
|
|
|
* @param array $annotations |
725
|
|
|
* @param string|array $annotationClass |
726
|
|
|
* |
727
|
|
|
* @return mixed |
728
|
|
|
*/ |
729
|
20 |
|
private static function getFirstAnnotationMatching(array $annotations, $annotationClass) |
730
|
|
|
{ |
731
|
20 |
|
if (\is_string($annotationClass)) { |
732
|
20 |
|
$annotationClass = [$annotationClass]; |
733
|
|
|
} |
734
|
|
|
|
735
|
20 |
|
foreach ($annotations as $annotation) { |
736
|
20 |
|
foreach ($annotationClass as $class) { |
737
|
20 |
|
if ($annotation instanceof $class) { |
738
|
20 |
|
return $annotation; |
739
|
|
|
} |
740
|
|
|
} |
741
|
|
|
} |
742
|
|
|
|
743
|
20 |
|
return false; |
744
|
|
|
} |
745
|
|
|
|
746
|
|
|
/** |
747
|
|
|
* Format an expression (ie. add "@=" if not set). |
748
|
|
|
* |
749
|
|
|
* @param string $expression |
750
|
|
|
* |
751
|
|
|
* @return string |
752
|
|
|
*/ |
753
|
19 |
|
private static function formatExpression(string $expression) |
754
|
|
|
{ |
755
|
19 |
|
return '@=' === \substr($expression, 0, 2) ? $expression : \sprintf('@=%s', $expression); |
756
|
|
|
} |
757
|
|
|
|
758
|
|
|
/** |
759
|
|
|
* Suffix a name if it is not already. |
760
|
|
|
* |
761
|
|
|
* @param string $name |
762
|
|
|
* @param string $suffix |
763
|
|
|
* |
764
|
|
|
* @return string |
765
|
|
|
*/ |
766
|
19 |
|
private static function suffixName(string $name, string $suffix) |
767
|
|
|
{ |
768
|
19 |
|
return \substr($name, -\strlen($suffix)) === $suffix ? $name : \sprintf('%s%s', $name, $suffix); |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Try to guess a field type base on is annotations. |
773
|
|
|
* |
774
|
|
|
* @param string $namespace |
775
|
|
|
* @param array $annotations |
776
|
|
|
* |
777
|
|
|
* @return string|false |
778
|
|
|
*/ |
779
|
19 |
|
private static function guessType(string $namespace, array $annotations) |
780
|
|
|
{ |
781
|
19 |
|
$columnAnnotation = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\Column'); |
782
|
19 |
|
if ($columnAnnotation) { |
783
|
19 |
|
$type = self::resolveTypeFromDoctrineType($columnAnnotation->type); |
784
|
19 |
|
$nullable = $columnAnnotation->nullable; |
785
|
19 |
|
if ($type) { |
786
|
19 |
|
return $nullable ? $type : \sprintf('%s!', $type); |
787
|
|
|
} else { |
788
|
1 |
|
throw new \Exception(\sprintf('Unable to auto-guess GraphQL type from Doctrine type "%s"', $columnAnnotation->type)); |
789
|
|
|
} |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
$associationAnnotations = [ |
793
|
19 |
|
'Doctrine\ORM\Mapping\OneToMany' => true, |
794
|
|
|
'Doctrine\ORM\Mapping\OneToOne' => false, |
795
|
|
|
'Doctrine\ORM\Mapping\ManyToMany' => true, |
796
|
|
|
'Doctrine\ORM\Mapping\ManyToOne' => false, |
797
|
|
|
]; |
798
|
|
|
|
799
|
19 |
|
$associationAnnotation = self::getFirstAnnotationMatching($annotations, \array_keys($associationAnnotations)); |
800
|
19 |
|
if ($associationAnnotation) { |
801
|
19 |
|
$target = self::fullyQualifiedClassName($associationAnnotation->targetEntity, $namespace); |
802
|
19 |
|
$type = self::resolveTypeFromClass($target, ['type']); |
803
|
|
|
|
804
|
19 |
|
if ($type) { |
805
|
19 |
|
$isMultiple = $associationAnnotations[\get_class($associationAnnotation)]; |
806
|
19 |
|
if ($isMultiple) { |
807
|
19 |
|
return \sprintf('[%s]!', $type); |
808
|
|
|
} else { |
809
|
19 |
|
$isNullable = false; |
810
|
19 |
|
$joinColumn = self::getFirstAnnotationMatching($annotations, 'Doctrine\ORM\Mapping\JoinColumn'); |
811
|
19 |
|
if ($joinColumn) { |
812
|
19 |
|
$isNullable = $joinColumn->nullable; |
813
|
|
|
} |
814
|
|
|
|
815
|
19 |
|
return \sprintf('%s%s', $type, $isNullable ? '' : '!'); |
816
|
|
|
} |
817
|
|
|
} else { |
818
|
1 |
|
throw new \Exception(\sprintf('Unable to auto-guess GraphQL type from Doctrine target class "%s" (check if the target class is a GraphQL type itself (with a @GQL\Type annotation).', $target)); |
819
|
|
|
} |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
throw new InvalidArgumentException(\sprintf('No Doctrine ORM annotation found.')); |
823
|
|
|
} |
824
|
|
|
|
825
|
|
|
/** |
826
|
|
|
* Resolve a FQN from classname and namespace. |
827
|
|
|
* |
828
|
|
|
* @param string $className |
829
|
|
|
* @param string $namespace |
830
|
|
|
* |
831
|
|
|
* @return string |
832
|
|
|
*/ |
833
|
19 |
|
public static function fullyQualifiedClassName(string $className, string $namespace) |
834
|
|
|
{ |
835
|
19 |
|
if (false === \strpos($className, '\\') && $namespace) { |
836
|
19 |
|
return $namespace.'\\'.$className; |
837
|
|
|
} |
838
|
|
|
|
839
|
1 |
|
return $className; |
840
|
|
|
} |
841
|
|
|
|
842
|
|
|
/** |
843
|
|
|
* Resolve a GraphqlType from a doctrine type. |
844
|
|
|
* |
845
|
|
|
* @param string $doctrineType |
846
|
|
|
* |
847
|
|
|
* @return string|false |
848
|
|
|
*/ |
849
|
19 |
|
private static function resolveTypeFromDoctrineType(string $doctrineType) |
850
|
|
|
{ |
851
|
19 |
|
if (isset(self::$doctrineMapping[$doctrineType])) { |
852
|
19 |
|
return self::$doctrineMapping[$doctrineType]; |
853
|
|
|
} |
854
|
|
|
|
855
|
19 |
|
switch ($doctrineType) { |
856
|
19 |
|
case 'integer': |
857
|
19 |
|
case 'smallint': |
858
|
19 |
|
case 'bigint': |
859
|
19 |
|
return 'Int'; |
860
|
|
|
break; |
|
|
|
|
861
|
19 |
|
case 'string': |
862
|
1 |
|
case 'text': |
863
|
19 |
|
return 'String'; |
864
|
|
|
break; |
865
|
1 |
|
case 'bool': |
866
|
1 |
|
case 'boolean': |
867
|
|
|
return 'Boolean'; |
868
|
|
|
break; |
869
|
1 |
|
case 'float': |
870
|
1 |
|
case 'decimal': |
871
|
|
|
return 'Float'; |
872
|
|
|
break; |
873
|
|
|
default: |
874
|
1 |
|
return false; |
875
|
|
|
} |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* Transform a method arguments from reflection to a list of GraphQL argument. |
880
|
|
|
* |
881
|
|
|
* @param \ReflectionMethod $method |
882
|
|
|
* |
883
|
|
|
* @return array |
884
|
|
|
*/ |
885
|
19 |
|
private static function guessArgs(\ReflectionMethod $method) |
886
|
|
|
{ |
887
|
19 |
|
$arguments = []; |
888
|
19 |
|
foreach ($method->getParameters() as $index => $parameter) { |
889
|
19 |
|
if (!$parameter->hasType()) { |
890
|
1 |
|
throw new InvalidArgumentException(\sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed as there is not type hint.', $index + 1, $parameter->getName(), $method->getName())); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
try { |
894
|
19 |
|
$gqlType = self::resolveGraphqlTypeFromReflectionType($parameter->getType(), self::$validInputTypes, $parameter->isDefaultValueAvailable()); |
895
|
|
|
} catch (\Exception $e) { |
896
|
|
|
throw new InvalidArgumentException(\sprintf('Argument n°%s "$%s" on method "%s" cannot be auto-guessed : %s".', $index + 1, $parameter->getName(), $method->getName(), $e->getMessage())); |
897
|
|
|
} |
898
|
|
|
|
899
|
19 |
|
$argumentConfig = []; |
900
|
19 |
|
if ($parameter->isDefaultValueAvailable()) { |
901
|
19 |
|
$argumentConfig['defaultValue'] = $parameter->getDefaultValue(); |
902
|
|
|
} |
903
|
|
|
|
904
|
19 |
|
$argumentConfig['type'] = $gqlType; |
905
|
|
|
|
906
|
19 |
|
$arguments[$parameter->getName()] = $argumentConfig; |
907
|
|
|
} |
908
|
|
|
|
909
|
19 |
|
return $arguments; |
910
|
|
|
} |
911
|
|
|
|
912
|
|
|
/** |
913
|
|
|
* Try to guess a GraphQL type from a Reflected Type. |
914
|
|
|
* |
915
|
|
|
* @param \ReflectionType $type |
916
|
|
|
* |
917
|
|
|
* @return string |
918
|
|
|
*/ |
919
|
19 |
|
private static function resolveGraphqlTypeFromReflectionType(\ReflectionType $type, array $filterGraphqlTypes = null, bool $isOptionnal = false) |
920
|
|
|
{ |
921
|
19 |
|
$stype = (string) $type; |
922
|
19 |
|
if ($type->isBuiltin()) { |
923
|
19 |
|
$gqlType = self::resolveTypeFromPhpType($stype); |
924
|
19 |
|
if (!$gqlType) { |
925
|
19 |
|
throw new \Exception(\sprintf('No corresponding GraphQL type found for builtin type "%s"', $stype)); |
926
|
|
|
} |
927
|
|
|
} else { |
928
|
19 |
|
$gqlType = self::resolveTypeFromClass($stype, $filterGraphqlTypes); |
929
|
19 |
|
if (!$gqlType) { |
930
|
|
|
throw new \Exception(\sprintf('No corresponding GraphQL %s found for class "%s"', $filterGraphqlTypes ? \implode(',', $filterGraphqlTypes) : 'object', $stype)); |
931
|
|
|
} |
932
|
|
|
} |
933
|
|
|
|
934
|
19 |
|
return \sprintf('%s%s', $gqlType, ($type->allowsNull() || $isOptionnal) ? '' : '!'); |
935
|
|
|
} |
936
|
|
|
|
937
|
|
|
/** |
938
|
|
|
* Resolve a GraphQL Type from a class name. |
939
|
|
|
* |
940
|
|
|
* @param string $className |
941
|
|
|
* @param array $wantedTypes |
942
|
|
|
* |
943
|
|
|
* @return string|false |
944
|
|
|
*/ |
945
|
19 |
|
private static function resolveTypeFromClass(string $className, array $wantedTypes = null) |
946
|
|
|
{ |
947
|
19 |
|
foreach (self::$classesMap as $gqlType => $config) { |
948
|
19 |
|
if ($config['class'] === $className) { |
949
|
19 |
|
if (!$wantedTypes || \in_array($config['type'], $wantedTypes)) { |
950
|
19 |
|
return $gqlType; |
951
|
|
|
} |
952
|
|
|
} |
953
|
|
|
} |
954
|
|
|
|
955
|
1 |
|
return false; |
956
|
|
|
} |
957
|
|
|
|
958
|
|
|
/** |
959
|
|
|
* Resolve a PHP class from a GraphQL type. |
960
|
|
|
* |
961
|
|
|
* @param string $type |
962
|
|
|
* |
963
|
|
|
* @return string|false |
964
|
|
|
*/ |
965
|
19 |
|
private static function resolveClassFromType(string $type) |
966
|
|
|
{ |
967
|
19 |
|
return self::$classesMap[$type] ?? false; |
968
|
|
|
} |
969
|
|
|
|
970
|
|
|
/** |
971
|
|
|
* Convert a PHP Builtin type to a GraphQL type. |
972
|
|
|
* |
973
|
|
|
* @param string $phpType |
974
|
|
|
* |
975
|
|
|
* @return string |
976
|
|
|
*/ |
977
|
19 |
|
private static function resolveTypeFromPhpType(string $phpType) |
978
|
|
|
{ |
979
|
19 |
|
switch ($phpType) { |
980
|
19 |
|
case 'boolean': |
981
|
19 |
|
case 'bool': |
982
|
19 |
|
return 'Boolean'; |
983
|
19 |
|
case 'integer': |
984
|
19 |
|
case 'int': |
985
|
19 |
|
return 'Int'; |
986
|
19 |
|
case 'float': |
987
|
19 |
|
case 'double': |
988
|
19 |
|
return 'Float'; |
989
|
19 |
|
case 'string': |
990
|
19 |
|
return 'String'; |
991
|
|
|
default: |
992
|
|
|
return false; |
|
|
|
|
993
|
|
|
} |
994
|
|
|
} |
995
|
|
|
} |
996
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.