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