Skip to content

Commit 9d35d06

Browse files
committed
Autowire services in parameters
This PR adds the ability to autowire services in parameters. The hard part with Symfony is to detect all services that must be marked public (i.e. all services that could be used by GraphQLite to inject a service).
1 parent ec0b599 commit 9d35d06

File tree

9 files changed

+220
-8
lines changed

9 files changed

+220
-8
lines changed

DependencyInjection/Configuration.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ public function getConfigTreeBuilder()
3535
->booleanNode('INCLUDE_TRACE')->defaultFalse()->info('Include stacktrace in output when an error arises')->end()
3636
->booleanNode('RETHROW_INTERNAL_EXCEPTIONS')->defaultFalse()->info('Exceptions are not caught by the engine and propagated to Symfony')->end()
3737
->booleanNode('RETHROW_UNSAFE_EXCEPTIONS')->defaultTrue()->info('Exceptions that do not implement ClientAware interface are not caught by the engine and propagated to Symfony.')->end()
38+
->end()
3839
->end()
40+
->arrayNode('autowire')
41+
->children()
42+
->booleanNode('by_class_name')->defaultTrue()->info('Autowire services based on the parameter\'s fully qualified class name')->end()
43+
->booleanNode('by_parameter_name')->defaultFalse()->info('Autowire services based on the parameter\'s name')->end()
44+
->end()
3945
->end()
4046
;
4147

DependencyInjection/GraphqliteCompilerPass.php

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,32 @@
99
use Doctrine\Common\Annotations\AnnotationRegistry;
1010
use Doctrine\Common\Annotations\CachedReader;
1111
use Doctrine\Common\Cache\ApcuCache;
12+
use function error_log;
13+
use Mouf\Composer\ClassNameMapper;
14+
use Psr\SimpleCache\CacheInterface;
15+
use Symfony\Component\Cache\Simple\ApcuCache as SymfonyApcuCache;
16+
use Symfony\Component\Cache\Simple\PhpFilesCache as SymfonyPhpFilesCache;
1217
use function function_exists;
1318
use GraphQL\Type\Definition\InputObjectType;
1419
use GraphQL\Type\Definition\ObjectType;
1520
use Psr\Container\ContainerInterface;
1621
use ReflectionClass;
22+
use ReflectionMethod;
1723
use function str_replace;
1824
use function strpos;
1925
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
2026
use Symfony\Component\DependencyInjection\ContainerBuilder;
2127
use Symfony\Component\DependencyInjection\Definition;
2228
use Symfony\Component\DependencyInjection\Reference;
29+
use TheCodingMachine\CacheUtils\ClassBoundCache;
30+
use TheCodingMachine\CacheUtils\ClassBoundCacheContract;
31+
use TheCodingMachine\CacheUtils\ClassBoundCacheContractInterface;
32+
use TheCodingMachine\CacheUtils\ClassBoundMemoryAdapter;
33+
use TheCodingMachine\CacheUtils\FileBoundCache;
34+
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
2335
use TheCodingMachine\GraphQLite\AnnotationReader;
36+
use TheCodingMachine\GraphQLite\Annotations\AbstractRequest;
37+
use TheCodingMachine\GraphQLite\Annotations\Field;
2438
use TheCodingMachine\GraphQLite\Annotations\Mutation;
2539
use TheCodingMachine\GraphQLite\Annotations\Query;
2640
use TheCodingMachine\Graphqlite\Bundle\QueryProviders\ControllerQueryProvider;
@@ -68,6 +82,11 @@ public function process(ContainerBuilder $container)
6882
$globTtl = 2;
6983
}
7084

85+
/**
86+
* @var array<string, array<int, string>>
87+
*/
88+
$classToServicesMap = [];
89+
7190
foreach ($container->getDefinitions() as $id => $definition) {
7291
if ($definition->isAbstract() || $definition->getClass() === null) {
7392
continue;
@@ -90,18 +109,29 @@ public function process(ContainerBuilder $container)
90109
}
91110
if ($typeAnnotation !== null || $this->getAnnotationReader()->getExtendTypeAnnotation($reflectionClass) !== null) {
92111
$definition->setPublic(true);
93-
} else {
94-
foreach ($reflectionClass->getMethods() as $method) {
95-
$factory = $reader->getFactoryAnnotation($method);
96-
if ($factory !== null) {
97-
$definition->setPublic(true);
98-
}
112+
}
113+
foreach ($reflectionClass->getMethods() as $method) {
114+
$factory = $reader->getFactoryAnnotation($method);
115+
if ($factory !== null) {
116+
$definition->setPublic(true);
99117
}
100118
}
101119
}
102120
}
103121
}
104122

123+
foreach ($controllersNamespaces as $controllersNamespace) {
124+
foreach ($this->getClassList($controllersNamespace) as $className => $refClass) {
125+
$this->makePublicInjectedServices($refClass, $reader, $container);
126+
}
127+
}
128+
129+
foreach ($typesNamespaces as $typeNamespace) {
130+
foreach ($this->getClassList($typeNamespace) as $className => $refClass) {
131+
$this->makePublicInjectedServices($refClass, $reader, $container);
132+
}
133+
}
134+
105135
foreach ($container->findTaggedServiceIds('graphql.annotated.controller') as $id => $tag) {
106136
$definition = $container->findDefinition($id);
107137
$class = $definition->getClass();
@@ -185,6 +215,45 @@ public function process(ContainerBuilder $container)
185215
}
186216
}
187217

218+
private function makePublicInjectedServices(ReflectionClass $refClass, AnnotationReader $reader, ContainerBuilder $container): void
219+
{
220+
$services = $this->getCodeCache()->get($refClass, function() use ($refClass, $reader, $container) {
221+
$services = [];
222+
foreach ($refClass->getMethods() as $method) {
223+
$field = $reader->getRequestAnnotation($method, AbstractRequest::class);
224+
if ($field !== null) {
225+
$services += $this->getListOfInjectedServices($method, $container);
226+
}
227+
}
228+
return $services;
229+
});
230+
231+
foreach ($services as $service) {
232+
$container->getDefinition($service)->setPublic(true);
233+
}
234+
235+
}
236+
237+
/**
238+
* @param ReflectionMethod $method
239+
* @param ContainerBuilder $container
240+
* @return array<string, string> key = value = service name
241+
*/
242+
private function getListOfInjectedServices(ReflectionMethod $method, ContainerBuilder $container): array
243+
{
244+
$services = [];
245+
foreach ($method->getParameters() as $parameter) {
246+
$type = $parameter->getType();
247+
if ($type !== null) {
248+
$fqcn = $type->getName();
249+
if ($container->has($fqcn)) {
250+
$services[$fqcn] = $fqcn;
251+
}
252+
}
253+
}
254+
return $services;
255+
}
256+
188257
/**
189258
* @param object $controller
190259
*/
@@ -211,4 +280,59 @@ private function getAnnotationReader(): AnnotationReader
211280
}
212281
return $this->annotationReader;
213282
}
283+
284+
/**
285+
* @var CacheInterface
286+
*/
287+
private $cache;
288+
289+
private function getPsr16Cache(): CacheInterface
290+
{
291+
if ($this->cache === null) {
292+
if (function_exists('apcu_fetch')) {
293+
$this->cache = new SymfonyApcuCache('graphqlite_bundle');
294+
} else {
295+
$this->cache = new SymfonyPhpFilesCache('graphqlite_bundle');
296+
}
297+
}
298+
return $this->cache;
299+
}
300+
301+
/**
302+
* @var ClassBoundCacheContractInterface
303+
*/
304+
private $codeCache;
305+
306+
private function getCodeCache(): ClassBoundCacheContractInterface
307+
{
308+
if ($this->codeCache === null) {
309+
$this->codeCache = new ClassBoundCacheContract(new ClassBoundMemoryAdapter(new ClassBoundCache(new FileBoundCache($this->getPsr16Cache()))));
310+
}
311+
return $this->codeCache;
312+
}
313+
314+
/**
315+
* Returns the array of globbed classes.
316+
* Only instantiable classes are returned.
317+
*
318+
* @return array<string,ReflectionClass> Key: fully qualified class name
319+
*/
320+
private function getClassList(string $namespace, int $globTtl = 2, bool $recursive = true): array
321+
{
322+
$explorer = new GlobClassExplorer($namespace, $this->getPsr16Cache(), $globTtl, ClassNameMapper::createFromComposerFile(null, null, true), $recursive);
323+
$allClasses = $explorer->getClasses();
324+
foreach ($allClasses as $className) {
325+
if (! class_exists($className)) {
326+
continue;
327+
}
328+
$refClass = new ReflectionClass($className);
329+
if (! $refClass->isInstantiable()) {
330+
continue;
331+
}
332+
$classes[$className] = $refClass;
333+
}
334+
335+
return $classes;
336+
}
337+
214338
}

DependencyInjection/GraphqliteExtension.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,23 @@ public function load(array $configs, ContainerBuilder $container)
5454
$namespaceType = [];
5555
}
5656

57+
if (isset($configs[0]['autowire']['by_class_name'])) {
58+
$autowireByClassName = $configs[0]['autowire']['by_class_name'];
59+
} else {
60+
$autowireByClassName = true;
61+
}
62+
if (isset($configs[0]['autowire']['by_parameter_name'])) {
63+
$autowireByParameterName = $configs[0]['autowire']['by_parameter_name'];
64+
} else {
65+
$autowireByParameterName = false;
66+
}
67+
5768
$container->setParameter('graphqlite.namespace.controllers', $namespaceController);
5869
$container->setParameter('graphqlite.namespace.types', $namespaceType);
5970

71+
$container->setParameter('graphqlite.autowire.by_class_name', $autowireByClassName);
72+
$container->setParameter('graphqlite.autowire.by_parameter_name', $autowireByParameterName);
73+
6074
$loader->load('graphqlite.xml');
6175

6276
$definition = $container->getDefinition(ServerConfig::class);

Resources/config/container/graphqlite.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@
2929

3030
<service id="TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface" alias="TheCodingMachine\GraphQLite\Mappers\Root\CompositeRootTypeMapper" />
3131

32+
<service id="TheCodingMachine\GraphQLite\Mappers\Parameters\ResolveInfoParameterMapper">
33+
<tag name="graphql.parameter_mapper" />
34+
</service>
35+
36+
<service id="TheCodingMachine\GraphQLite\Mappers\Parameters\ContainerParameterMapper">
37+
<argument type="service" id="service_container" />
38+
<argument>%graphqlite.autowire.by_class_name%</argument>
39+
<argument>%graphqlite.autowire.by_parameter_name%</argument>
40+
<tag name="graphql.parameter_mapper" />
41+
</service>
42+
3243
<service id="TheCodingMachine\GraphQLite\Mappers\Parameters\CompositeParameterMapper">
3344
<argument type="tagged" tag="graphql.parameter_mapper" />
3445
</service>

Tests/Fixtures/Entities/Contact.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
use DateTimeInterface;
88
use Psr\Http\Message\UploadedFileInterface;
9+
use stdClass;
910
use TheCodingMachine\GraphQLite\Annotations\Field;
1011
use TheCodingMachine\GraphQLite\Annotations\Type;
12+
use TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Controller\TestGraphqlController;
1113

1214
/**
1315
* @Type()
@@ -31,4 +33,16 @@ public function getName(): string
3133
{
3234
return $this->name;
3335
}
36+
37+
/**
38+
* @Field()
39+
* @return string
40+
*/
41+
public function injectService(TestGraphqlController $testService = null): string
42+
{
43+
if (!$testService instanceof TestGraphqlController) {
44+
return 'KO';
45+
}
46+
return 'OK';
47+
}
3448
}

Tests/FunctionalTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace TheCodingMachine\Graphqlite\Bundle\Tests;
44

5+
use function json_decode;
56
use PHPUnit\Framework\TestCase;
67
use Symfony\Component\HttpFoundation\Request;
78
use TheCodingMachine\GraphQLite\Schema;
@@ -59,6 +60,36 @@ public function testServiceWiring()
5960
], $result);
6061
}
6162

63+
public function testServiceAutowiring()
64+
{
65+
$kernel = new GraphqliteTestingKernel('test', true);
66+
$kernel->boot();
67+
$container = $kernel->getContainer();
68+
69+
$schema = $container->get(Schema::class);
70+
$this->assertInstanceOf(Schema::class, $schema);
71+
$schema->assertValid();
72+
73+
$request = Request::create('/graphql', 'GET', ['query' => '
74+
{
75+
contact {
76+
injectService
77+
}
78+
}']);
79+
80+
$response = $kernel->handle($request);
81+
82+
$result = json_decode($response->getContent(), true);
83+
84+
$this->assertSame([
85+
'data' => [
86+
'contact' => [
87+
'injectService' => 'OK',
88+
]
89+
]
90+
], $result);
91+
}
92+
6293
public function testErrors()
6394
{
6495
$kernel = new GraphqliteTestingKernel('test', true);

Tests/GraphqliteTestingKernel.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
4040
'namespace' => [
4141
'controllers' => ['TheCodingMachine\\Graphqlite\\Bundle\\Tests\\Fixtures\\Controller\\'],
4242
'types' => ['TheCodingMachine\\Graphqlite\\Bundle\\Tests\\Fixtures\\Types\\', 'TheCodingMachine\\Graphqlite\\Bundle\\Tests\\Fixtures\\Entities\\']
43+
],
44+
'autowire' => [
45+
'by_parameter_name' => true
4346
]
4447
));
4548
});

composer.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
],
1818
"require" : {
1919
"php" : ">=7.2",
20-
"thecodingmachine/graphqlite" : "~4.0.0",
20+
"thecodingmachine/graphqlite" : "dev-autowiring as 4.0.0",
2121
"symfony/framework-bundle": "^4.1.9",
2222
"doctrine/annotations": "^1.6",
2323
"doctrine/cache": "^1.8",
2424
"symfony/psr-http-message-bridge": "^1",
2525
"nyholm/psr7": "^1.1",
2626
"zendframework/zend-diactoros": "^1.8.6",
27-
"overblog/graphiql-bundle": "^0.1.2"
27+
"overblog/graphiql-bundle": "^0.1.2",
28+
"thecodingmachine/cache-utils": "^1"
2829
},
2930
"require-dev": {
3031
"symfony/security-bundle": "^4.1.9",
@@ -33,6 +34,12 @@
3334
"phpstan/phpstan": "^0.10.6",
3435
"beberlei/porpaginas": "^1.2"
3536
},
37+
"repositories": [
38+
{
39+
"type": "vcs",
40+
"url": "https://github.com/moufmouf/graphqlite"
41+
}
42+
],
3643
"scripts": {
3744
"phpstan": "phpstan analyse GraphqliteBundle.php DependencyInjection/ Controller/ Resources/ Security/ -c phpstan.neon --level=7 --no-progress"
3845
},

phpunit.xml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
<whitelist>
2121
<directory>./</directory>
2222
<exclude>
23+
<directory>cache</directory>
24+
<directory>build</directory>
2325
<directory>./Resources</directory>
2426
<directory>./Tests</directory>
2527
<directory>./vendor</directory>

0 commit comments

Comments
 (0)