Skip to content

Commit 7c73f6a

Browse files
authored
Merge pull request #19 from moufmouf/inject_service_in_params
Autowire services in parameters
2 parents ec0b599 + 5c95e20 commit 7c73f6a

File tree

10 files changed

+230
-8
lines changed

10 files changed

+230
-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: 144 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;
@@ -61,13 +75,21 @@ public function process(ContainerBuilder $container)
6175
$controllersNamespaces = $container->getParameter('graphqlite.namespace.controllers');
6276
$typesNamespaces = $container->getParameter('graphqlite.namespace.types');
6377

78+
$autowireByParameterName = $container->getParameter('graphqlite.autowire.by_parameter_name');
79+
$autowireByClassName = $container->getParameter('graphqlite.autowire.by_class_name');
80+
6481
// 2 seconds of TTL in environment mode. Otherwise, let's cache forever!
6582
$env = $container->getParameter('kernel.environment');
6683
$globTtl = null;
6784
if ($env === 'dev') {
6885
$globTtl = 2;
6986
}
7087

88+
/**
89+
* @var array<string, array<int, string>>
90+
*/
91+
$classToServicesMap = [];
92+
7193
foreach ($container->getDefinitions() as $id => $definition) {
7294
if ($definition->isAbstract() || $definition->getClass() === null) {
7395
continue;
@@ -90,18 +112,31 @@ public function process(ContainerBuilder $container)
90112
}
91113
if ($typeAnnotation !== null || $this->getAnnotationReader()->getExtendTypeAnnotation($reflectionClass) !== null) {
92114
$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-
}
115+
}
116+
foreach ($reflectionClass->getMethods() as $method) {
117+
$factory = $reader->getFactoryAnnotation($method);
118+
if ($factory !== null) {
119+
$definition->setPublic(true);
99120
}
100121
}
101122
}
102123
}
103124
}
104125

126+
if ($autowireByParameterName || $autowireByClassName) {
127+
foreach ($controllersNamespaces as $controllersNamespace) {
128+
foreach ($this->getClassList($controllersNamespace) as $className => $refClass) {
129+
$this->makePublicInjectedServices($refClass, $reader, $container, $autowireByClassName, $autowireByParameterName);
130+
}
131+
}
132+
133+
foreach ($typesNamespaces as $typeNamespace) {
134+
foreach ($this->getClassList($typeNamespace) as $className => $refClass) {
135+
$this->makePublicInjectedServices($refClass, $reader, $container, $autowireByClassName, $autowireByParameterName);
136+
}
137+
}
138+
}
139+
105140
foreach ($container->findTaggedServiceIds('graphql.annotated.controller') as $id => $tag) {
106141
$definition = $container->findDefinition($id);
107142
$class = $definition->getClass();
@@ -185,6 +220,53 @@ public function process(ContainerBuilder $container)
185220
}
186221
}
187222

223+
private function makePublicInjectedServices(ReflectionClass $refClass, AnnotationReader $reader, ContainerBuilder $container, bool $autowireByClassName, bool $autowireByParameterName): void
224+
{
225+
$services = $this->getCodeCache()->get($refClass, function() use ($refClass, $reader, $container, $autowireByClassName, $autowireByParameterName) {
226+
$services = [];
227+
foreach ($refClass->getMethods() as $method) {
228+
$field = $reader->getRequestAnnotation($method, AbstractRequest::class);
229+
if ($field !== null) {
230+
$services += $this->getListOfInjectedServices($method, $container, $autowireByClassName, $autowireByParameterName);
231+
}
232+
}
233+
return $services;
234+
});
235+
236+
foreach ($services as $service) {
237+
$container->getDefinition($service)->setPublic(true);
238+
}
239+
240+
}
241+
242+
/**
243+
* @param ReflectionMethod $method
244+
* @param ContainerBuilder $container
245+
* @return array<string, string> key = value = service name
246+
*/
247+
private function getListOfInjectedServices(ReflectionMethod $method, ContainerBuilder $container, bool $autowireByClassName, bool $autowireByParameterName): array
248+
{
249+
$services = [];
250+
foreach ($method->getParameters() as $parameter) {
251+
if ($autowireByParameterName) {
252+
$parameterName = $parameter->getName();
253+
if ($container->has($parameterName)) {
254+
$services[$parameterName] = $parameterName;
255+
}
256+
}
257+
if ($autowireByClassName) {
258+
$type = $parameter->getType();
259+
if ($type !== null) {
260+
$fqcn = $type->getName();
261+
if ($container->has($fqcn)) {
262+
$services[$fqcn] = $fqcn;
263+
}
264+
}
265+
}
266+
}
267+
return $services;
268+
}
269+
188270
/**
189271
* @param object $controller
190272
*/
@@ -211,4 +293,60 @@ private function getAnnotationReader(): AnnotationReader
211293
}
212294
return $this->annotationReader;
213295
}
296+
297+
/**
298+
* @var CacheInterface
299+
*/
300+
private $cache;
301+
302+
private function getPsr16Cache(): CacheInterface
303+
{
304+
if ($this->cache === null) {
305+
if (function_exists('apcu_fetch')) {
306+
$this->cache = new SymfonyApcuCache('graphqlite_bundle');
307+
} else {
308+
$this->cache = new SymfonyPhpFilesCache('graphqlite_bundle');
309+
}
310+
}
311+
return $this->cache;
312+
}
313+
314+
/**
315+
* @var ClassBoundCacheContractInterface
316+
*/
317+
private $codeCache;
318+
319+
private function getCodeCache(): ClassBoundCacheContractInterface
320+
{
321+
if ($this->codeCache === null) {
322+
$this->codeCache = new ClassBoundCacheContract(new ClassBoundMemoryAdapter(new ClassBoundCache(new FileBoundCache($this->getPsr16Cache()))));
323+
}
324+
return $this->codeCache;
325+
}
326+
327+
/**
328+
* Returns the array of globbed classes.
329+
* Only instantiable classes are returned.
330+
*
331+
* @return array<string,ReflectionClass> Key: fully qualified class name
332+
*/
333+
private function getClassList(string $namespace, int $globTtl = 2, bool $recursive = true): array
334+
{
335+
$explorer = new GlobClassExplorer($namespace, $this->getPsr16Cache(), $globTtl, ClassNameMapper::createFromComposerFile(null, null, true), $recursive);
336+
$allClasses = $explorer->getClasses();
337+
$classes = [];
338+
foreach ($allClasses as $className) {
339+
if (! class_exists($className)) {
340+
continue;
341+
}
342+
$refClass = new ReflectionClass($className);
343+
if (! $refClass->isInstantiable()) {
344+
continue;
345+
}
346+
$classes[$className] = $refClass;
347+
}
348+
349+
return $classes;
350+
}
351+
214352
}

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, stdClass $someService = null): string
42+
{
43+
if (!$testService instanceof TestGraphqlController || $someService === null) {
44+
return 'KO';
45+
}
46+
return 'OK';
47+
}
3448
}

Tests/Fixtures/config/services.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ services:
1717
resource: '../*'
1818
exclude: '../{Entities}'
1919

20+
someService:
21+
class: stdClass
2022
# controllers are imported separately to make sure services can be injected
2123
# as action arguments even if you don't extend any base controller class
2224
#App\Controller\:

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: 3 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" : "^4",
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",

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)