Skip to content

Commit b63eaec

Browse files
authored
Support subscription operations (#649)
* Added support for Subscription annotated controller methods as GraphQL fields * Added tests * Added docs
1 parent 0e90ebf commit b63eaec

24 files changed

+459
-163
lines changed

src/AggregateControllerQueryProvider.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ class AggregateControllerQueryProvider implements QueryProviderInterface
2929
* @param iterable<string> $controllers A list of controllers name in the container.
3030
* @param ContainerInterface $controllersContainer The container we will fetch controllers from.
3131
*/
32-
public function __construct(private readonly iterable $controllers, private readonly FieldsBuilder $fieldsBuilder, private readonly ContainerInterface $controllersContainer)
33-
{
32+
public function __construct(
33+
private readonly iterable $controllers,
34+
private readonly FieldsBuilder $fieldsBuilder,
35+
private readonly ContainerInterface $controllersContainer,
36+
) {
3437
}
3538

3639
/** @return array<string,FieldDefinition> */
@@ -52,13 +55,26 @@ public function getMutations(): array
5255
$mutationList = [];
5356

5457
foreach ($this->controllers as $controllerName) {
55-
$controller = $this->controllersContainer->get($controllerName);
58+
$controller = $this->controllersContainer->get($controllerName);
5659
$mutationList[$controllerName] = $this->fieldsBuilder->getMutations($controller);
5760
}
5861

5962
return $this->flattenList($mutationList);
6063
}
6164

65+
/** @return array<string, FieldDefinition> */
66+
public function getSubscriptions(): array
67+
{
68+
$subscriptionList = [];
69+
70+
foreach ($this->controllers as $controllerName) {
71+
$controller = $this->controllersContainer->get($controllerName);
72+
$subscriptionList[$controllerName] = $this->fieldsBuilder->getSubscriptions($controller);
73+
}
74+
75+
return $this->flattenList($subscriptionList);
76+
}
77+
6278
/**
6379
* @param array<string, array<string, FieldDefinition>> $list
6480
*

src/AggregateControllerQueryProviderFactory.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ class AggregateControllerQueryProviderFactory implements QueryProviderFactoryInt
1515
* @param iterable<string> $controllers A list of controllers name in the container.
1616
* @param ContainerInterface $controllersContainer The container we will fetch controllers from.
1717
*/
18-
public function __construct(private readonly iterable $controllers, private readonly ContainerInterface $controllersContainer)
19-
{
20-
}
18+
public function __construct(
19+
private readonly iterable $controllers,
20+
private readonly ContainerInterface $controllersContainer,
21+
) {}
2122

2223
public function create(FactoryContext $context): QueryProviderInterface
2324
{
24-
return new AggregateControllerQueryProvider($this->controllers, $context->getFieldsBuilder(), $this->controllersContainer);
25+
return new AggregateControllerQueryProvider(
26+
$this->controllers,
27+
$context->getFieldsBuilder(),
28+
$this->controllersContainer,
29+
);
2530
}
2631
}

src/AggregateQueryProvider.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ class AggregateQueryProvider implements QueryProviderInterface
2020
/** @param QueryProviderInterface[] $queryProviders */
2121
public function __construct(iterable $queryProviders)
2222
{
23-
$this->queryProviders = is_array($queryProviders) ? $queryProviders : iterator_to_array($queryProviders);
23+
$this->queryProviders = is_array($queryProviders)
24+
? $queryProviders
25+
: iterator_to_array($queryProviders);
2426
}
2527

2628
/** @return QueryField[] */
@@ -48,4 +50,17 @@ public function getMutations(): array
4850

4951
return array_merge(...$mutationsArray);
5052
}
53+
54+
/** @return QueryField[] */
55+
public function getSubscriptions(): array
56+
{
57+
$subscriptionsArray = array_map(static function (QueryProviderInterface $queryProvider) {
58+
return $queryProvider->getSubscriptions();
59+
}, $this->queryProviders);
60+
if ($subscriptionsArray === []) {
61+
return [];
62+
}
63+
64+
return array_merge(...$subscriptionsArray);
65+
}
5166
}

src/Annotations/Subscription.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite\Annotations;
6+
7+
use Attribute;
8+
9+
/**
10+
* @Annotation
11+
* @Target({"METHOD"})
12+
* @Attributes({
13+
* @Attribute("outputType", type = "string"),
14+
* })
15+
*/
16+
#[Attribute(Attribute::TARGET_METHOD)]
17+
class Subscription extends AbstractRequest
18+
{
19+
}

src/FieldsBuilder.php

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,26 @@
44

55
namespace TheCodingMachine\GraphQLite;
66

7+
use const PHP_EOL;
78
use Doctrine\Common\Annotations\AnnotationException;
9+
use function array_diff_key;
10+
use function array_fill_keys;
11+
use function array_intersect_key;
12+
use function array_keys;
13+
use function array_merge;
14+
use function array_shift;
15+
use function array_slice;
16+
use function assert;
17+
use function count;
18+
use function get_parent_class;
19+
use function in_array;
20+
use function is_callable;
21+
use function is_string;
22+
use function key;
23+
use function reset;
24+
use function rtrim;
25+
use function str_starts_with;
26+
use function trim;
827
use GraphQL\Type\Definition\FieldDefinition;
928
use GraphQL\Type\Definition\InputType;
1029
use GraphQL\Type\Definition\NonNull;
@@ -27,12 +46,14 @@
2746
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
2847
use TheCodingMachine\GraphQLite\Annotations\Query;
2948
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
49+
use TheCodingMachine\GraphQLite\Annotations\Subscription;
3050
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
3151
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
3252
use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException;
3353
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
3454
use TheCodingMachine\GraphQLite\Mappers\Parameters\TypeHandler;
3555
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
56+
3657
use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
3758
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
3859
use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface;
@@ -51,30 +72,10 @@
5172
use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory;
5273
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
5374
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
75+
5476
use TheCodingMachine\GraphQLite\Types\TypeResolver;
5577
use TheCodingMachine\GraphQLite\Utils\PropertyAccessor;
5678

57-
use function array_diff_key;
58-
use function array_fill_keys;
59-
use function array_intersect_key;
60-
use function array_keys;
61-
use function array_merge;
62-
use function array_shift;
63-
use function array_slice;
64-
use function assert;
65-
use function count;
66-
use function get_parent_class;
67-
use function in_array;
68-
use function is_callable;
69-
use function is_string;
70-
use function key;
71-
use function reset;
72-
use function rtrim;
73-
use function str_starts_with;
74-
use function trim;
75-
76-
use const PHP_EOL;
77-
7879
/**
7980
* A class in charge if returning list of fields for queries / mutations / entities / input types
8081
*/
@@ -123,6 +124,16 @@ public function getMutations(object $controller): array
123124
return $this->getFieldsByAnnotations($controller, Mutation::class, false);
124125
}
125126

127+
/**
128+
* @return array<string, FieldDefinition>
129+
*
130+
* @throws ReflectionException
131+
*/
132+
public function getSubscriptions(object $controller): array
133+
{
134+
return $this->getFieldsByAnnotations($controller, Subscription::class, false);
135+
}
136+
126137
/** @return array<string, FieldDefinition> QueryField indexed by name. */
127138
public function getFields(object $controller, string|null $typeName = null): array
128139
{
@@ -266,7 +277,7 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array
266277
/**
267278
* @param object|class-string<object> $controller The controller instance, or the name of the source class name
268279
* @param class-string<AbstractRequest> $annotationName
269-
* @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation
280+
* @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query, @Mutation, and @Subscription.
270281
* @param string|null $typeName Type name for which fields should be extracted for.
271282
*
272283
* @return array<string, FieldDefinition>

src/GlobControllerQueryProvider.php

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
namespace TheCodingMachine\GraphQLite;
66

7+
use function class_exists;
8+
use function interface_exists;
9+
use function is_array;
10+
use function str_replace;
711
use GraphQL\Type\Definition\FieldDefinition;
812
use InvalidArgumentException;
913
use Mouf\Composer\ClassNameMapper;
@@ -16,15 +20,11 @@
1620
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
1721
use TheCodingMachine\GraphQLite\Annotations\Mutation;
1822
use TheCodingMachine\GraphQLite\Annotations\Query;
19-
20-
use function class_exists;
21-
use function interface_exists;
22-
use function is_array;
23-
use function str_replace;
23+
use TheCodingMachine\GraphQLite\Annotations\Subscription;
2424

2525
/**
2626
* Scans all the classes in a given namespace of the main project (not the vendor directory).
27-
* Analyzes all classes and detects "Query" and "Mutation" annotations.
27+
* Analyzes all classes and detects "Query", "Mutation", and "Subscription" annotations.
2828
*
2929
* Assumes that the container contains a class whose identifier is the same as the class name.
3030
*/
@@ -53,14 +53,20 @@ public function __construct(
5353
)
5454
{
5555
$this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true);
56-
$this->cacheContract = new Psr16Adapter($this->cache, str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace), $cacheTtl ?? 0);
56+
$this->cacheContract = new Psr16Adapter(
57+
$this->cache,
58+
str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace),
59+
$cacheTtl ?? 0,
60+
);
5761
}
5862

5963
private function getAggregateControllerQueryProvider(): AggregateControllerQueryProvider
6064
{
61-
if ($this->aggregateControllerQueryProvider === null) {
62-
$this->aggregateControllerQueryProvider = new AggregateControllerQueryProvider($this->getInstancesList(), $this->fieldsBuilder, $this->container);
63-
}
65+
$this->aggregateControllerQueryProvider ??= new AggregateControllerQueryProvider(
66+
$this->getInstancesList(),
67+
$this->fieldsBuilder,
68+
$this->container,
69+
);
6470

6571
return $this->aggregateControllerQueryProvider;
6672
}
@@ -100,7 +106,7 @@ private function buildInstancesList(): array
100106
if (! $refClass->isInstantiable()) {
101107
continue;
102108
}
103-
if (! $this->hasQueriesOrMutations($refClass)) {
109+
if (! $this->hasOperations($refClass)) {
104110
continue;
105111
}
106112
if (! $this->container->has($className)) {
@@ -114,7 +120,7 @@ private function buildInstancesList(): array
114120
}
115121

116122
/** @param ReflectionClass<object> $reflectionClass */
117-
private function hasQueriesOrMutations(ReflectionClass $reflectionClass): bool
123+
private function hasOperations(ReflectionClass $reflectionClass): bool
118124
{
119125
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) {
120126
$queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, Query::class);
@@ -125,6 +131,10 @@ private function hasQueriesOrMutations(ReflectionClass $reflectionClass): bool
125131
if ($mutationAnnotation !== null) {
126132
return true;
127133
}
134+
$subscriptionAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, Subscription::class);
135+
if ($subscriptionAnnotation !== null) {
136+
return true;
137+
}
128138
}
129139
return false;
130140
}
@@ -140,4 +150,10 @@ public function getMutations(): array
140150
{
141151
return $this->getAggregateControllerQueryProvider()->getMutations();
142152
}
153+
154+
/** @return array<string,FieldDefinition> */
155+
public function getSubscriptions(): array
156+
{
157+
return $this->getAggregateControllerQueryProvider()->getSubscriptions();
158+
}
143159
}

src/QueryProviderInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,7 @@ public function getQueries(): array;
1414

1515
/** @return QueryField[] */
1616
public function getMutations(): array;
17+
18+
/** @return QueryField[] */
19+
public function getSubscriptions(): array;
1720
}

0 commit comments

Comments
 (0)