Skip to content

Commit 5bbdf22

Browse files
committed
Make the resolver maps registrable using service tags and handle per-schema priority
1 parent 505c07f commit 5bbdf22

File tree

9 files changed

+271
-63
lines changed

9 files changed

+271
-63
lines changed

UPGRADE-0.13.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ UPGRADE FROM 0.12 to 0.13
55

66
- [Rename default_field config](#rename-default_field-config)
77
- [Improve default field resolver](#improve-default-field-resolver)
8+
- [Use service tags to register resolver maps](#use-service-tags-to-register-resolver-maps)
89

910
### Rename default_field config
1011

@@ -50,3 +51,27 @@ MyType:
5051
```
5152
5253
[see default Field Resolver per type for more details](https://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver-per-type)
54+
55+
### Use service tags to register resolver maps
56+
57+
The resolver maps used to be configured using the `overblog_graphql.definitions.schema.resolver_maps`
58+
option. This has been deprecated in favour of using service tags to register them.
59+
60+
```diff
61+
# config/graphql.yaml
62+
overblog_graphql:
63+
definitions:
64+
schema:
65+
# ...
66+
- resolver_maps:
67+
- - 'App\GraphQL\MyResolverMap'
68+
```
69+
70+
```diff
71+
# services/graphql.yaml
72+
services:
73+
- App\GraphQL\MyResolverMap: ~
74+
+ App\GraphQL\MyResolverMap:
75+
+ tags:
76+
+ - { name: overblog_graphql.resolver_map, schema: default }
77+
```

docs/definitions/quick-start.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,6 @@ in files `config/graphql/types/*.graphql`
3030

3131
4. Define schema Resolvers ([more details](resolver-map.md))
3232

33-
```yaml
34-
# config/packages/graphql.yaml
35-
overblog_graphql:
36-
definitions:
37-
schema:
38-
# ...
39-
resolver_maps:
40-
- App\Resolver\MyResolverMap
41-
```
42-
4333
```php
4434
<?php
4535

docs/definitions/resolver-map.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,21 @@ class MyResolverMap extends ResolverMap
118118
}
119119
```
120120

121-
Declare resolverMap to current schema
121+
Each resolver map must be tagged with the `overblog_graphql.resolver_map` tag
122+
that defines at which priority it should run for the given schema. The priority
123+
is an optional attribute and it has a default value of 0. The higher the number,
124+
the earlier the resolver map is executed.
122125

123126
```yaml
124-
overblog_graphql:
125-
definitions:
126-
schema:
127-
# ...
128-
# resolver maps services IDs
129-
resolver_maps:
130-
- App\Resolver\MyResolverMap
131-
127+
# config/services.yaml
132128
services:
133-
App\Resolver\MyResolverMap: ~
129+
App\Resolver\MyResolverMap1:
130+
tags:
131+
- { name: overblog_graphql.resolver_map, schema: default }
132+
133+
App\Resolver\MyResolverMap2:
134+
tags:
135+
- { name: overblog_graphql.resolver_map, schema: default, priority: 10 }
134136
```
135137
136138
**Notes:**
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Overblog\GraphQLBundle\DependencyInjection\Compiler;
6+
7+
use Overblog\GraphQLBundle\EventListener\TypeDecoratorListener;
8+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
11+
use Symfony\Component\DependencyInjection\Reference;
12+
13+
final class ResolverMapTaggedServiceMappingPass implements CompilerPassInterface
14+
{
15+
private const SERVICE_TAG = 'overblog_graphql.resolver_map';
16+
17+
/**
18+
* {@inheritdoc}
19+
*/
20+
public function process(ContainerBuilder $container): void
21+
{
22+
$resolverMapsSortedBySchema = [];
23+
$resolverMapsBySchemas = $container->getParameter('overblog_graphql.resolver_maps');
24+
$typeDecoratorListenerDefinition = $container->getDefinition(TypeDecoratorListener::class);
25+
26+
foreach ($container->findTaggedServiceIds(self::SERVICE_TAG, true) as $serviceId => $tags) {
27+
foreach ($tags as $tag) {
28+
if (!isset($tag['schema'])) {
29+
throw new RuntimeException(\sprintf('The "schema" attribute on the "overblog_graphql.resolver_map" tag of the "%s" service is required.', $serviceId));
30+
}
31+
32+
if (!isset($resolverMapsBySchemas[$tag['schema']])) {
33+
throw new RuntimeException(\sprintf('Service "%s" is invalid: schema "%s" specified on the tag "%s" does not exist (known ones are: "%s").', $serviceId, $tag['schema'], self::SERVICE_TAG, \implode('", "', \array_keys($resolverMapsBySchemas))));
34+
}
35+
36+
$resolverMapsBySchemas[$tag['schema']][$serviceId] = $tag['priority'] ?? ($resolverMapsBySchemas[$tag['schema']][$serviceId] ?? 0);
37+
}
38+
}
39+
40+
foreach ($resolverMapsBySchemas as $schema => $resolverMaps) {
41+
foreach ($resolverMaps as $resolverMap => $priority) {
42+
$resolverMapsSortedBySchema[$schema][$priority][] = $resolverMap;
43+
}
44+
}
45+
46+
foreach ($resolverMapsSortedBySchema as $schema => $resolverMaps) {
47+
\krsort($resolverMaps);
48+
49+
$resolverMaps = \array_merge(...$resolverMaps);
50+
51+
foreach ($resolverMaps as $index => $resolverMap) {
52+
$resolverMaps[$index] = new Reference($resolverMap);
53+
}
54+
55+
$typeDecoratorListenerDefinition->addMethodCall('addSchemaResolverMaps', [
56+
$schema,
57+
$resolverMaps,
58+
]);
59+
}
60+
61+
$container->getParameterBag()->remove('overblog_graphql.resolver_maps');
62+
}
63+
}

src/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ private function definitionsSchemaSection()
210210
->arrayNode('resolver_maps')
211211
->defaultValue([])
212212
->prototype('scalar')->end()
213+
->setDeprecated('The "%path%.%node%" configuration is deprecated since version 0.13 and will be removed in 0.14. Add the "overblog_graphql.resolver_map" tag to the services instead.')
213214
->end()
214215
->arrayNode('types')
215216
->defaultValue([])

src/DependencyInjection/OverblogGraphQLExtension.php

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
use Overblog\GraphQLBundle\EventListener\DebugListener;
2222
use Overblog\GraphQLBundle\EventListener\ErrorHandlerListener;
2323
use Overblog\GraphQLBundle\EventListener\ErrorLoggerListener;
24-
use Overblog\GraphQLBundle\EventListener\TypeDecoratorListener;
2524
use Overblog\GraphQLBundle\Request\Executor;
2625
use Overblog\GraphQLBundle\Validator\ValidatorFactory;
2726
use Symfony\Component\Config\FileLocator;
@@ -89,8 +88,10 @@ private function registerForAutoconfiguration(ContainerBuilder $container): void
8988
{
9089
$container->registerForAutoconfiguration(MutationInterface::class)
9190
->addTag('overblog_graphql.mutation');
91+
9292
$container->registerForAutoconfiguration(ResolverInterface::class)
9393
->addTag('overblog_graphql.resolver');
94+
9495
$container->registerForAutoconfiguration(Type::class)
9596
->addTag('overblog_graphql.type');
9697
}
@@ -235,41 +236,36 @@ private function setSchemaBuilderArguments(array $config, ContainerBuilder $cont
235236

236237
private function setSchemaArguments(array $config, ContainerBuilder $container): void
237238
{
238-
if (isset($config['definitions']['schema'])) {
239-
$executorDefinition = $container->getDefinition(Executor::class);
240-
$typeDecoratorListenerDefinition = $container->getDefinition(TypeDecoratorListener::class);
241-
242-
foreach ($config['definitions']['schema'] as $schemaName => $schemaConfig) {
243-
// builder
244-
$schemaBuilderID = \sprintf('%s.schema_builder_%s', $this->getAlias(), $schemaName);
245-
$definition = $container->register($schemaBuilderID, \Closure::class);
246-
$definition->setFactory([new Reference('overblog_graphql.schema_builder'), 'getBuilder']);
247-
$definition->setArguments([
248-
$schemaName,
249-
$schemaConfig['query'],
250-
$schemaConfig['mutation'],
251-
$schemaConfig['subscription'],
252-
$schemaConfig['types'],
253-
]);
254-
// schema
255-
$schemaID = \sprintf('%s.schema_%s', $this->getAlias(), $schemaName);
256-
$definition = $container->register($schemaID, Schema::class);
257-
$definition->setFactory([new Reference($schemaBuilderID), 'call']);
258-
259-
if (!empty($schemaConfig['resolver_maps'])) {
260-
$typeDecoratorListenerDefinition->addMethodCall(
261-
'addSchemaResolverMaps',
262-
[
263-
$schemaName,
264-
\array_map(function ($id) {
265-
return new Reference($id);
266-
}, $schemaConfig['resolver_maps']),
267-
]
268-
);
269-
}
270-
$executorDefinition->addMethodCall('addSchemaBuilder', [$schemaName, new Reference($schemaBuilderID)]);
271-
}
239+
if (!isset($config['definitions']['schema'])) {
240+
return;
272241
}
242+
243+
$executorDefinition = $container->getDefinition(Executor::class);
244+
$resolverMapsBySchema = [];
245+
246+
foreach ($config['definitions']['schema'] as $schemaName => $schemaConfig) {
247+
// builder
248+
$schemaBuilderID = \sprintf('%s.schema_builder_%s', $this->getAlias(), $schemaName);
249+
$definition = $container->register($schemaBuilderID, \Closure::class);
250+
$definition->setFactory([new Reference('overblog_graphql.schema_builder'), 'getBuilder']);
251+
$definition->setArguments([
252+
$schemaName,
253+
$schemaConfig['query'],
254+
$schemaConfig['mutation'],
255+
$schemaConfig['subscription'],
256+
$schemaConfig['types'],
257+
]);
258+
// schema
259+
$schemaID = \sprintf('%s.schema_%s', $this->getAlias(), $schemaName);
260+
$definition = $container->register($schemaID, Schema::class);
261+
$definition->setFactory([new Reference($schemaBuilderID), 'call']);
262+
263+
$executorDefinition->addMethodCall('addSchemaBuilder', [$schemaName, new Reference($schemaBuilderID)]);
264+
265+
$resolverMapsBySchema[$schemaName] = \array_fill_keys($schemaConfig['resolver_maps'], 0);
266+
}
267+
268+
$container->setParameter(\sprintf('%s.resolver_maps', $this->getAlias()), $resolverMapsBySchema);
273269
}
274270

275271
private function setServicesAliases(array $config, ContainerBuilder $container): void

src/OverblogGraphQLBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Overblog\GraphQLBundle\DependencyInjection\Compiler\ExpressionFunctionPass;
1111
use Overblog\GraphQLBundle\DependencyInjection\Compiler\GlobalVariablesPass;
1212
use Overblog\GraphQLBundle\DependencyInjection\Compiler\MutationTaggedServiceMappingTaggedPass;
13+
use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverMapTaggedServiceMappingPass;
1314
use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverMethodAliasesPass;
1415
use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverTaggedServiceMappingPass;
1516
use Overblog\GraphQLBundle\DependencyInjection\Compiler\TypeGeneratorPass;
@@ -43,6 +44,7 @@ public function build(ContainerBuilder $container): void
4344
$container->addCompilerPass(new ExpressionFunctionPass());
4445
$container->addCompilerPass(new ResolverMethodAliasesPass());
4546
$container->addCompilerPass(new AliasedPass());
47+
$container->addCompilerPass(new ResolverMapTaggedServiceMappingPass());
4648
$container->addCompilerPass(new TypeGeneratorPass(), PassConfig::TYPE_BEFORE_REMOVING);
4749
$container->addCompilerPass(new TypeTaggedServiceMappingPass(), PassConfig::TYPE_BEFORE_REMOVING);
4850
$container->addCompilerPass(new ResolverTaggedServiceMappingPass(), PassConfig::TYPE_BEFORE_REMOVING);
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Overblog\GraphQLBundle\Tests\DependencyInjection\Compiler;
6+
7+
use Overblog\GraphQLBundle\DependencyInjection\Compiler\ResolverMapTaggedServiceMappingPass;
8+
use Overblog\GraphQLBundle\EventListener\TypeDecoratorListener;
9+
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
12+
use Symfony\Component\DependencyInjection\Reference;
13+
14+
final class ResolverMapTaggedServiceMappingPassTest extends TestCase
15+
{
16+
public function testProcess(): void
17+
{
18+
$container = new ContainerBuilder();
19+
$container->setParameter('overblog_graphql.resolver_maps', [
20+
'foo' => [],
21+
'bar' => [],
22+
]);
23+
24+
$container->register(TypeDecoratorListener::class);
25+
$container->register('App\\GraphQl\\Resolver\\ResolverMap1')
26+
->addTag('overblog_graphql.resolver_map', ['schema' => 'foo']);
27+
28+
$container->register('App\\GraphQl\\Resolver\\ResolverMap2')
29+
->addTag('overblog_graphql.resolver_map', ['schema' => 'foo', 'priority' => 10])
30+
->addTag('overblog_graphql.resolver_map', ['schema' => 'bar', 'priority' => -10]);
31+
32+
$container->register('App\\GraphQl\\Resolver\\ResolverMap3')
33+
->addTag('overblog_graphql.resolver_map', ['schema' => 'foo', 'priority' => -10])
34+
->addTag('overblog_graphql.resolver_map', ['schema' => 'bar', 'priority' => 10]);
35+
36+
$container->register('App\\GraphQl\\Resolver\\ResolverMap4')
37+
->addTag('overblog_graphql.resolver_map', ['schema' => 'bar']);
38+
39+
(new ResolverMapTaggedServiceMappingPass())->process($container);
40+
41+
$typeDecoratorListenerDefinition = $container->getDefinition(TypeDecoratorListener::class);
42+
43+
$methodCalls = $typeDecoratorListenerDefinition->getMethodCalls();
44+
45+
$this->assertCount(2, $methodCalls);
46+
47+
$this->assertSame('addSchemaResolverMaps', $methodCalls[0][0]);
48+
$this->assertSame('foo', $methodCalls[0][1][0]);
49+
$this->assertEquals([
50+
new Reference('App\\GraphQl\\Resolver\\ResolverMap2'),
51+
new Reference('App\\GraphQl\\Resolver\\ResolverMap1'),
52+
new Reference('App\\GraphQl\\Resolver\\ResolverMap3'),
53+
], $methodCalls[0][1][1]);
54+
55+
$this->assertSame('addSchemaResolverMaps', $methodCalls[1][0]);
56+
$this->assertSame('bar', $methodCalls[1][1][0]);
57+
$this->assertEquals([
58+
new Reference('App\\GraphQl\\Resolver\\ResolverMap3'),
59+
new Reference('App\\GraphQl\\Resolver\\ResolverMap4'),
60+
new Reference('App\\GraphQl\\Resolver\\ResolverMap2'),
61+
], $methodCalls[1][1][1]);
62+
}
63+
64+
public function testProcessThrowsIfSchemaAttributeIsNotDefinedOnTag(): void
65+
{
66+
$container = new ContainerBuilder();
67+
$container->setParameter('overblog_graphql.resolver_maps', []);
68+
69+
$container->register(TypeDecoratorListener::class);
70+
$container->register('App\\GraphQl\\Resolver\\ResolverMap')
71+
->addTag('overblog_graphql.resolver_map');
72+
73+
$this->expectException(RuntimeException::class);
74+
$this->expectExceptionMessage('The "schema" attribute on the "overblog_graphql.resolver_map" tag of the "App\\GraphQl\\Resolver\\ResolverMap" service is required.');
75+
76+
(new ResolverMapTaggedServiceMappingPass())->process($container);
77+
}
78+
79+
public function testProcessWithResolverMapBothTaggedAndInConfigDoesNotAddItTwice(): void
80+
{
81+
$container = new ContainerBuilder();
82+
$container->setParameter('overblog_graphql.resolver_maps', [
83+
'foo' => [
84+
'App\\GraphQl\\Resolver\\ResolverMap1' => -10,
85+
'App\\GraphQl\\Resolver\\ResolverMap2' => 0,
86+
],
87+
]);
88+
89+
$container->register(TypeDecoratorListener::class);
90+
$container->register('App\\GraphQl\\Resolver\\ResolverMap1')
91+
->addTag('overblog_graphql.resolver_map', ['schema' => 'foo', 'priority' => 10]);
92+
93+
$container->register('App\\GraphQl\\Resolver\\ResolverMap2');
94+
95+
(new ResolverMapTaggedServiceMappingPass())->process($container);
96+
97+
$typeDecoratorListenerDefinition = $container->getDefinition(TypeDecoratorListener::class);
98+
99+
$methodCalls = $typeDecoratorListenerDefinition->getMethodCalls();
100+
101+
$this->assertCount(1, $methodCalls);
102+
$this->assertSame('foo', $methodCalls[0][1][0]);
103+
$this->assertEquals([
104+
new Reference('App\\GraphQl\\Resolver\\ResolverMap1'),
105+
new Reference('App\\GraphQl\\Resolver\\ResolverMap2'),
106+
], $methodCalls[0][1][1]);
107+
}
108+
109+
public function testProcessThrowsIfTagReferencesUnknownSchema(): void
110+
{
111+
$container = new ContainerBuilder();
112+
$container->register(TypeDecoratorListener::class);
113+
$container->setParameter('overblog_graphql.resolver_maps', [
114+
'foo' => [],
115+
'bar' => [],
116+
]);
117+
118+
$container->register('App\\GraphQl\\Resolver\\ResolverMap')
119+
->addTag('overblog_graphql.resolver_map', ['schema' => 'baz']);
120+
121+
$this->expectException(RuntimeException::class);
122+
$this->expectExceptionMessage('Service "App\\GraphQl\\Resolver\\ResolverMap" is invalid: schema "baz" specified on the tag "overblog_graphql.resolver_map" does not exist (known ones are: "foo", "bar").');
123+
124+
(new ResolverMapTaggedServiceMappingPass())->process($container);
125+
}
126+
}

0 commit comments

Comments
 (0)