Skip to content

Commit

Permalink
Support subscription operations (#649)
Browse files Browse the repository at this point in the history
* Added support for Subscription annotated controller methods as GraphQL fields

* Added tests

* Added docs
  • Loading branch information
oojacoboo authored Feb 5, 2024
1 parent 0e90ebf commit b63eaec
Show file tree
Hide file tree
Showing 24 changed files with 459 additions and 163 deletions.
22 changes: 19 additions & 3 deletions src/AggregateControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ class AggregateControllerQueryProvider implements QueryProviderInterface
* @param iterable<string> $controllers A list of controllers name in the container.
* @param ContainerInterface $controllersContainer The container we will fetch controllers from.
*/
public function __construct(private readonly iterable $controllers, private readonly FieldsBuilder $fieldsBuilder, private readonly ContainerInterface $controllersContainer)
{
public function __construct(
private readonly iterable $controllers,
private readonly FieldsBuilder $fieldsBuilder,
private readonly ContainerInterface $controllersContainer,
) {
}

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

foreach ($this->controllers as $controllerName) {
$controller = $this->controllersContainer->get($controllerName);
$controller = $this->controllersContainer->get($controllerName);
$mutationList[$controllerName] = $this->fieldsBuilder->getMutations($controller);
}

return $this->flattenList($mutationList);
}

/** @return array<string, FieldDefinition> */
public function getSubscriptions(): array
{
$subscriptionList = [];

foreach ($this->controllers as $controllerName) {
$controller = $this->controllersContainer->get($controllerName);
$subscriptionList[$controllerName] = $this->fieldsBuilder->getSubscriptions($controller);
}

return $this->flattenList($subscriptionList);
}

/**
* @param array<string, array<string, FieldDefinition>> $list
*
Expand Down
13 changes: 9 additions & 4 deletions src/AggregateControllerQueryProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ class AggregateControllerQueryProviderFactory implements QueryProviderFactoryInt
* @param iterable<string> $controllers A list of controllers name in the container.
* @param ContainerInterface $controllersContainer The container we will fetch controllers from.
*/
public function __construct(private readonly iterable $controllers, private readonly ContainerInterface $controllersContainer)
{
}
public function __construct(
private readonly iterable $controllers,
private readonly ContainerInterface $controllersContainer,
) {}

public function create(FactoryContext $context): QueryProviderInterface
{
return new AggregateControllerQueryProvider($this->controllers, $context->getFieldsBuilder(), $this->controllersContainer);
return new AggregateControllerQueryProvider(
$this->controllers,
$context->getFieldsBuilder(),
$this->controllersContainer,
);
}
}
17 changes: 16 additions & 1 deletion src/AggregateQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class AggregateQueryProvider implements QueryProviderInterface
/** @param QueryProviderInterface[] $queryProviders */
public function __construct(iterable $queryProviders)
{
$this->queryProviders = is_array($queryProviders) ? $queryProviders : iterator_to_array($queryProviders);
$this->queryProviders = is_array($queryProviders)
? $queryProviders
: iterator_to_array($queryProviders);
}

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

return array_merge(...$mutationsArray);
}

/** @return QueryField[] */
public function getSubscriptions(): array
{
$subscriptionsArray = array_map(static function (QueryProviderInterface $queryProvider) {
return $queryProvider->getSubscriptions();
}, $this->queryProviders);
if ($subscriptionsArray === []) {
return [];
}

return array_merge(...$subscriptionsArray);
}
}
19 changes: 19 additions & 0 deletions src/Annotations/Subscription.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Annotations;

use Attribute;

/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
* @Attribute("outputType", type = "string"),
* })
*/
#[Attribute(Attribute::TARGET_METHOD)]
class Subscription extends AbstractRequest
{
}
55 changes: 33 additions & 22 deletions src/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,26 @@

namespace TheCodingMachine\GraphQLite;

use const PHP_EOL;
use Doctrine\Common\Annotations\AnnotationException;
use function array_diff_key;
use function array_fill_keys;
use function array_intersect_key;
use function array_keys;
use function array_merge;
use function array_shift;
use function array_slice;
use function assert;
use function count;
use function get_parent_class;
use function in_array;
use function is_callable;
use function is_string;
use function key;
use function reset;
use function rtrim;
use function str_starts_with;
use function trim;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\NonNull;
Expand All @@ -27,12 +46,14 @@
use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations;
use TheCodingMachine\GraphQLite\Annotations\Query;
use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface;
use TheCodingMachine\GraphQLite\Annotations\Subscription;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException;
use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException;
use TheCodingMachine\GraphQLite\Mappers\Parameters\ParameterMiddlewareInterface;
use TheCodingMachine\GraphQLite\Mappers\Parameters\TypeHandler;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;

use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperInterface;
use TheCodingMachine\GraphQLite\Middlewares\FieldHandlerInterface;
use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface;
Expand All @@ -51,30 +72,10 @@
use TheCodingMachine\GraphQLite\Reflection\CachedDocBlockFactory;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\MutableObjectType;

use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Utils\PropertyAccessor;

use function array_diff_key;
use function array_fill_keys;
use function array_intersect_key;
use function array_keys;
use function array_merge;
use function array_shift;
use function array_slice;
use function assert;
use function count;
use function get_parent_class;
use function in_array;
use function is_callable;
use function is_string;
use function key;
use function reset;
use function rtrim;
use function str_starts_with;
use function trim;

use const PHP_EOL;

/**
* A class in charge if returning list of fields for queries / mutations / entities / input types
*/
Expand Down Expand Up @@ -123,6 +124,16 @@ public function getMutations(object $controller): array
return $this->getFieldsByAnnotations($controller, Mutation::class, false);
}

/**
* @return array<string, FieldDefinition>
*
* @throws ReflectionException
*/
public function getSubscriptions(object $controller): array
{
return $this->getFieldsByAnnotations($controller, Subscription::class, false);
}

/** @return array<string, FieldDefinition> QueryField indexed by name. */
public function getFields(object $controller, string|null $typeName = null): array
{
Expand Down Expand Up @@ -266,7 +277,7 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array
/**
* @param object|class-string<object> $controller The controller instance, or the name of the source class name
* @param class-string<AbstractRequest> $annotationName
* @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
* @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.
* @param string|null $typeName Type name for which fields should be extracted for.
*
* @return array<string, FieldDefinition>
Expand Down
40 changes: 28 additions & 12 deletions src/GlobControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace TheCodingMachine\GraphQLite;

use function class_exists;
use function interface_exists;
use function is_array;
use function str_replace;
use GraphQL\Type\Definition\FieldDefinition;
use InvalidArgumentException;
use Mouf\Composer\ClassNameMapper;
Expand All @@ -16,15 +20,11 @@
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use TheCodingMachine\GraphQLite\Annotations\Mutation;
use TheCodingMachine\GraphQLite\Annotations\Query;

use function class_exists;
use function interface_exists;
use function is_array;
use function str_replace;
use TheCodingMachine\GraphQLite\Annotations\Subscription;

/**
* Scans all the classes in a given namespace of the main project (not the vendor directory).
* Analyzes all classes and detects "Query" and "Mutation" annotations.
* Analyzes all classes and detects "Query", "Mutation", and "Subscription" annotations.
*
* Assumes that the container contains a class whose identifier is the same as the class name.
*/
Expand Down Expand Up @@ -53,14 +53,20 @@ public function __construct(
)
{
$this->classNameMapper = $classNameMapper ?? ClassNameMapper::createFromComposerFile(null, null, true);
$this->cacheContract = new Psr16Adapter($this->cache, str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace), $cacheTtl ?? 0);
$this->cacheContract = new Psr16Adapter(
$this->cache,
str_replace(['\\', '{', '}', '(', ')', '/', '@', ':'], '_', $namespace),
$cacheTtl ?? 0,
);
}

private function getAggregateControllerQueryProvider(): AggregateControllerQueryProvider
{
if ($this->aggregateControllerQueryProvider === null) {
$this->aggregateControllerQueryProvider = new AggregateControllerQueryProvider($this->getInstancesList(), $this->fieldsBuilder, $this->container);
}
$this->aggregateControllerQueryProvider ??= new AggregateControllerQueryProvider(
$this->getInstancesList(),
$this->fieldsBuilder,
$this->container,
);

return $this->aggregateControllerQueryProvider;
}
Expand Down Expand Up @@ -100,7 +106,7 @@ private function buildInstancesList(): array
if (! $refClass->isInstantiable()) {
continue;
}
if (! $this->hasQueriesOrMutations($refClass)) {
if (! $this->hasOperations($refClass)) {
continue;
}
if (! $this->container->has($className)) {
Expand All @@ -114,7 +120,7 @@ private function buildInstancesList(): array
}

/** @param ReflectionClass<object> $reflectionClass */
private function hasQueriesOrMutations(ReflectionClass $reflectionClass): bool
private function hasOperations(ReflectionClass $reflectionClass): bool
{
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $refMethod) {
$queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, Query::class);
Expand All @@ -125,6 +131,10 @@ private function hasQueriesOrMutations(ReflectionClass $reflectionClass): bool
if ($mutationAnnotation !== null) {
return true;
}
$subscriptionAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, Subscription::class);
if ($subscriptionAnnotation !== null) {
return true;
}
}
return false;
}
Expand All @@ -140,4 +150,10 @@ public function getMutations(): array
{
return $this->getAggregateControllerQueryProvider()->getMutations();
}

/** @return array<string,FieldDefinition> */
public function getSubscriptions(): array
{
return $this->getAggregateControllerQueryProvider()->getSubscriptions();
}
}
3 changes: 3 additions & 0 deletions src/QueryProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ public function getQueries(): array;

/** @return QueryField[] */
public function getMutations(): array;

/** @return QueryField[] */
public function getSubscriptions(): array;
}
Loading

0 comments on commit b63eaec

Please sign in to comment.