Skip to content

Commit f1dd087

Browse files
committed
Fix #1169
When using attributes or annotations, we can try to auto resolve the types associated with interfaces.
1 parent 4a78526 commit f1dd087

File tree

22 files changed

+320
-38
lines changed

22 files changed

+320
-38
lines changed

docs/annotations/annotations-reference.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,13 +591,15 @@ class Hero {
591591

592592
This annotation is used on _class_ to define a GraphQL interface.
593593

594-
Required attributes:
594+
Optional attributes:
595595

596596
- **resolveType** : An expression to resolve the types
597+
- **name** : The GraphQL name of the interface (default to the class name without namespace)
597598

598-
Optional attributes:
599+
If the `resolveType` attribute is not set, the service `overblog_graphql.interface_type_resolver` will be used to try to resolve the type automatically based on types implementing the interface and their associated class.
600+
The system will register a map of interfaces with the list of types and their associated class name implementing the interface (the parameter is named `overblog_graphql_types.interfaces_map` in the container) and use it to resolve the type from the value (the first type where the class `instanceof` operator returns true will be used).
599601

600-
- **name** : The GraphQL name of the interface (default to the class name without namespace)
602+
```php
601603

602604
## @Scalar
603605

src/Annotation/TypeInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ final class TypeInterface extends Annotation
2020
/**
2121
* Resolver type for interface.
2222
*/
23-
public string $resolveType;
23+
public ?string $resolveType;
2424

2525
/**
2626
* Interface name.
@@ -31,7 +31,7 @@ final class TypeInterface extends Annotation
3131
* @param string $resolveType The express resolve type
3232
* @param string|null $name The GraphQL name of the interface
3333
*/
34-
public function __construct(string $resolveType, ?string $name = null)
34+
public function __construct(?string $resolveType = null, ?string $name = null)
3535
{
3636
$this->resolveType = $resolveType;
3737
$this->name = $name;

src/Config/Parser/MetadataParser/ClassesTypesMap.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ final class ClassesTypesMap
1111
*/
1212
private array $classesMap = [];
1313

14+
/**
15+
* @var array<string, array{class: string, type: string}>
16+
*/
17+
private array $interfacesMap = [];
18+
1419
public function hasType(string $gqlType): bool
1520
{
1621
return isset($this->classesMap[$gqlType]);
@@ -72,8 +77,25 @@ public function searchClassesMapBy(callable $predicate, string $type): array
7277
return $classNames;
7378
}
7479

75-
public function toArray(): array
80+
public function classesToArray(): array
7681
{
7782
return $this->classesMap;
7883
}
84+
85+
/**
86+
* Add a type and its associated class to the interfaces map
87+
*/
88+
public function addInterfaceType(string $interfaceType, string $graphqlType, string $className): void
89+
{
90+
if (!isset($this->interfacesMap[$interfaceType])) {
91+
$this->interfacesMap[$interfaceType] = [];
92+
}
93+
94+
$this->interfacesMap[$interfaceType][$className] = $graphqlType;
95+
}
96+
97+
public function interfacesToArray(): array
98+
{
99+
return $this->interfacesMap;
100+
}
79101
}

src/Config/Parser/MetadataParser/MetadataParser.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ public static function parse(SplFileInfo $file, ContainerBuilder $container, arr
8888
return self::processFile($file, $container, $configs, false);
8989
}
9090

91+
public static function finalize(ContainerBuilder $container): void
92+
{
93+
$parameter = 'overblog_graphql_types.interfaces_map';
94+
$value = $container->hasParameter($parameter) ? $container->getParameter($parameter) : [];
95+
foreach (self::$map->interfacesToArray() as $interface => $types) {
96+
if (!isset($value[$interface])) {
97+
$value[$interface] = [];
98+
}
99+
foreach ($types as $className => $typeName) {
100+
$value[$interface][$className] = $typeName;
101+
}
102+
}
103+
104+
$container->setParameter('overblog_graphql_types.interfaces_map', $value);
105+
}
106+
91107
/**
92108
* @internal
93109
*/
@@ -134,7 +150,7 @@ private static function processFile(SplFileInfo $file, ContainerBuilder $contain
134150
}
135151
}
136152

137-
return $preProcess ? self::$map->toArray() : $gqlTypes;
153+
return $preProcess ? self::$map->classesToArray() : $gqlTypes;
138154
} catch (ReflectionException $e) {
139155
return $gqlTypes;
140156
} catch (\InvalidArgumentException $e) {
@@ -189,6 +205,11 @@ private static function classMetadatasToGQLConfiguration(
189205

190206
array_unshift($gqlConfiguration['config']['builders'], ['builder' => 'relay-connection', 'builderConfig' => ['edgeType' => $edgeType]]);
191207
}
208+
209+
$interfaces = $gqlConfiguration['config']['interfaces'] ?? [];
210+
foreach ($interfaces as $interface) {
211+
self::$map->addInterfaceType($interface, $gqlName, $reflectionClass->getName());
212+
}
192213
}
193214
break;
194215

@@ -224,7 +245,10 @@ private static function classMetadatasToGQLConfiguration(
224245
case $classMetadata instanceof Metadata\TypeInterface:
225246
$gqlType = self::GQL_INTERFACE;
226247
if (!$preProcess) {
227-
$gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata);
248+
if (!$gqlName) {
249+
$gqlName = !empty($classMetadata->name) ? $classMetadata->name : $reflectionClass->getShortName();
250+
}
251+
$gqlConfiguration = self::typeInterfaceMetadataToGQLConfiguration($reflectionClass, $classMetadata, $gqlName);
228252
}
229253
break;
230254

@@ -380,7 +404,7 @@ private static function graphQLTypeConfigFromAnnotation(ReflectionClass $reflect
380404
*
381405
* @return array{type: 'interface', config: array}
382406
*/
383-
private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation): array
407+
private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass $reflectionClass, Metadata\TypeInterface $interfaceAnnotation, string $gqlName): array
384408
{
385409
$interfaceConfiguration = [];
386410

@@ -390,7 +414,12 @@ private static function typeInterfaceMetadataToGQLConfiguration(ReflectionClass
390414
$interfaceConfiguration['fields'] = array_merge($fieldsFromProperties, $fieldsFromMethods);
391415
$interfaceConfiguration = self::getDescriptionConfiguration(static::getMetadatas($reflectionClass)) + $interfaceConfiguration;
392416

393-
$interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
417+
if (isset($interfaceAnnotation->resolveType)) {
418+
$interfaceConfiguration['resolveType'] = self::formatExpression($interfaceAnnotation->resolveType);
419+
} else {
420+
// Try to use default interface resolver type
421+
$interfaceConfiguration['resolveType'] = self::formatExpression(sprintf("service('overblog_graphql.interface_type_resolver').resolveType('%s', value)", $gqlName));
422+
}
394423

395424
return ['type' => 'interface', 'config' => $interfaceConfiguration];
396425
}
@@ -687,7 +716,7 @@ private static function getGraphQLInputFieldsFromMetadatas(ReflectionClass $refl
687716

688717
if ($fieldMetadata instanceof InputField && null !== $fieldMetadata->defaultValue) {
689718
$fieldConfiguration['defaultValue'] = $fieldMetadata->defaultValue;
690-
} elseif ($reflector->hasDefaultValue()) {
719+
} elseif ($reflector->hasDefaultValue() && null !== $reflector->getDefaultValue()) {
691720
$fieldConfiguration['defaultValue'] = $reflector->getDefaultValue();
692721
}
693722

src/Controller/ProfilerController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function __invoke(Request $request, string $token): Response
6363

6464
$tokens = array_map(function ($tokenData) {
6565
$profile = $this->profiler->loadProfile($tokenData['token']);
66-
if (!$profile->hasCollector('graphql')) {
66+
if (!$profile || !$profile->hasCollector('graphql')) {
6767
return false;
6868
}
6969
$tokenData['graphql'] = $profile->getCollector('graphql');

src/DependencyInjection/Compiler/ConfigParserPass.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ private function getConfigs(ContainerBuilder $container): array
9191
$config = $container->getParameterBag()->resolveValue($container->getParameter('overblog_graphql.config'));
9292
$container->getParameterBag()->remove('overblog_graphql.config');
9393
$container->setParameter($this->getAlias().'.classes_map', []);
94+
$container->setParameter($this->getAlias().'.interfaces_map', []);
95+
9496
$typesMappings = $this->mappingConfig($config, $container);
9597
// reset treated files
9698
$this->treatedFiles = [];
@@ -117,6 +119,9 @@ private function getConfigs(ContainerBuilder $container): array
117119
// flatten config is a requirement to support inheritance
118120
$flattenTypeConfig = array_merge(...$typeConfigs);
119121

122+
AnnotationParser::finalize($container);
123+
AttributeParser::finalize($container);
124+
120125
return $flattenTypeConfig;
121126
}
122127

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Overblog\GraphQLBundle\Resolver;
6+
7+
class InterfaceTypeResolver
8+
{
9+
private TypeResolver $typeResolver;
10+
private array $interfacesMap;
11+
12+
public function __construct(TypeResolver $typeResolver, array $interfacesMap = [])
13+
{
14+
$this->typeResolver = $typeResolver;
15+
$this->interfacesMap = $interfacesMap;
16+
}
17+
18+
public function resolveType(string $interfaceType, mixed $value)
19+
{
20+
if (!isset($this->interfacesMap[$interfaceType])) {
21+
throw new UnresolvableException(sprintf('Default interface type resolver was unable to find interface with name "%s"', $interfaceType));
22+
}
23+
24+
$gqlType = null;
25+
$types = $this->interfacesMap[$interfaceType];
26+
foreach ($types as $className => $type) {
27+
if ($value instanceof $className) {
28+
$gqlType = $type;
29+
break;
30+
}
31+
}
32+
33+
if (null === $gqlType) {
34+
throw new UnresolvableException(sprintf('Default interface type resolver with interface "%s" did not find a matching instance in: %s', $interfaceType, implode(', ', array_keys($types))));
35+
}
36+
37+
return $this->typeResolver->resolve($gqlType);
38+
}
39+
}

src/Resources/config/aliases.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
overblog_graphql.request_parser: '@Overblog\GraphQLBundle\Request\Parser'
1313
overblog_graphql.request_batch_parser: '@Overblog\GraphQLBundle\Request\BatchParser'
1414
overblog_graphql.arguments_transformer: '@Overblog\GraphQLBundle\Transformer\ArgumentsTransformer'
15+
overblog_graphql.interface_type_resolver: '@Overblog\GraphQLBundle\Resolver\InterfaceTypeResolver'
1516

1617
overblog_graphql.schema_builder:
1718
alias: 'Overblog\GraphQLBundle\Definition\Builder\SchemaBuilder'

src/Resources/config/services.yaml

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ services:
1010
Overblog\GraphQLBundle\Resolver\FieldResolver: ~
1111

1212
Overblog\GraphQLBundle\Definition\GraphQLServices:
13-
tags: ['container.service_locator']
13+
tags: ["container.service_locator"]
1414

1515
Overblog\GraphQLBundle\Request\Executor:
1616
arguments:
1717
- "@overblog_graphql.executor"
1818
- "@overblog_graphql.promise_adapter"
1919
- "@event_dispatcher"
20-
- '@overblog_graphql.default_field_resolver'
20+
- "@overblog_graphql.default_field_resolver"
2121
calls:
2222
- ["setMaxQueryComplexity", ["%overblog_graphql.query_max_complexity%"]]
2323
- ["setMaxQueryDepth", ["%overblog_graphql.query_max_depth%"]]
@@ -34,14 +34,14 @@ services:
3434

3535
Overblog\GraphQLBundle\Resolver\TypeResolver:
3636
calls:
37-
- ['setDispatcher', ['@event_dispatcher']]
37+
- ["setDispatcher", ["@event_dispatcher"]]
3838
tags:
3939
- { name: overblog_graphql.service, alias: typeResolver }
4040

4141
Overblog\GraphQLBundle\Transformer\ArgumentsTransformer:
4242
arguments:
43-
- '@?validator'
44-
- '%overblog_graphql_types.classes_map%'
43+
- "@?validator"
44+
- "%overblog_graphql_types.classes_map%"
4545

4646
Overblog\GraphQLBundle\Resolver\QueryResolver:
4747
tags:
@@ -51,31 +51,36 @@ services:
5151
tags:
5252
- { name: overblog_graphql.service, alias: mutationResolver }
5353

54+
Overblog\GraphQLBundle\Resolver\InterfaceTypeResolver:
55+
arguments:
56+
- '@Overblog\GraphQLBundle\Resolver\TypeResolver'
57+
- "%overblog_graphql_types.interfaces_map%"
58+
5459
Overblog\GraphQLBundle\Resolver\AccessResolver:
5560
arguments:
56-
- '@overblog_graphql.promise_adapter'
61+
- "@overblog_graphql.promise_adapter"
5762

5863
Overblog\GraphQLBundle\ExpressionLanguage\ExpressionLanguage:
5964
arguments:
60-
- '@?overblog_graphql.cache_expression_language_parser'
65+
- "@?overblog_graphql.cache_expression_language_parser"
6166

6267
Overblog\GraphQLBundle\Generator\TypeGenerator:
6368
arguments:
64-
- '%overblog_graphql_types.config%'
69+
- "%overblog_graphql_types.config%"
6570
- '@Overblog\GraphQLBundle\Generator\TypeBuilder'
6671
- '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
6772
- !service
68-
class: Overblog\GraphQLBundle\Generator\TypeGeneratorOptions
69-
arguments:
70-
- '%overblog_graphql.class_namespace%'
71-
- '%overblog_graphql.cache_dir%'
72-
- '%overblog_graphql.use_classloader_listener%'
73-
- '%kernel.cache_dir%'
74-
- '%overblog_graphql.cache_dir_permissions%'
73+
class: Overblog\GraphQLBundle\Generator\TypeGeneratorOptions
74+
arguments:
75+
- "%overblog_graphql.class_namespace%"
76+
- "%overblog_graphql.cache_dir%"
77+
- "%overblog_graphql.use_classloader_listener%"
78+
- "%kernel.cache_dir%"
79+
- "%overblog_graphql.cache_dir_permissions%"
7580

7681
Overblog\GraphQLBundle\Definition\ArgumentFactory:
7782
arguments:
78-
- '%overblog_graphql.argument_class%'
83+
- "%overblog_graphql.argument_class%"
7984
tags:
8085
- { name: overblog_graphql.service, alias: argumentFactory }
8186

@@ -90,7 +95,7 @@ services:
9095

9196
Overblog\GraphQLBundle\Definition\ConfigProcessor:
9297
arguments:
93-
- !tagged_iterator 'overblog_graphql.definition_config_processor'
98+
- !tagged_iterator "overblog_graphql.definition_config_processor"
9499

95100
GraphQL\Executor\Promise\PromiseAdapter: "@overblog_graphql.promise_adapter"
96101

@@ -100,7 +105,7 @@ services:
100105

101106
Overblog\GraphQLBundle\Security\Security:
102107
arguments:
103-
- '@?security.helper'
108+
- "@?security.helper"
104109
tags:
105110
- { name: overblog_graphql.service, alias: security, public: false }
106111

@@ -111,12 +116,12 @@ services:
111116
Overblog\GraphQLBundle\Generator\TypeBuilder:
112117
arguments:
113118
- '@Overblog\GraphQLBundle\Generator\Converter\ExpressionConverter'
114-
- '%overblog_graphql.class_namespace%'
119+
- "%overblog_graphql.class_namespace%"
115120

116121
Overblog\GraphQLBundle\Validator\InputValidatorFactory:
117122
arguments:
118-
- '@?validator.validator_factory'
119-
- '@?validator'
120-
- '@?translator.default'
123+
- "@?validator.validator_factory"
124+
- "@?validator"
125+
- "@?translator.default"
121126
tags:
122127
- { name: overblog_graphql.service, alias: input_validator_factory, public: false }

tests/Config/Parser/MetadataParserTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ public function testInterfaces(): void
252252
'description' => 'The armored interface',
253253
'resolveType' => '@=query(\'character_type\', [value])',
254254
]);
255+
256+
$this->expect('Biped', 'interface', [
257+
'resolveType' => "@=service('overblog_graphql.interface_type_resolver').resolveType('Biped', value)",
258+
]);
255259
}
256260

257261
public function testEnum(): void
@@ -304,7 +308,7 @@ public function testUnionAutoguessed(): void
304308
public function testInterfaceAutoguessed(): void
305309
{
306310
$this->expect('Mandalorian', 'object', [
307-
'interfaces' => ['Character', 'WithArmor'],
311+
'interfaces' => ['Biped', 'Character', 'WithArmor'],
308312
'fields' => [
309313
'name' => ['type' => 'String!', 'description' => 'The name of the character'],
310314
'friends' => ['type' => '[Character]', 'description' => 'The friends of the character', 'resolve' => "@=query('App\\MyResolver::getFriends')"],

0 commit comments

Comments
 (0)