Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a DecoratableTypeResolver base class #1213

Open
wants to merge 13 commits into
base: 8.x-4.x
Choose a base branch
from
73 changes: 73 additions & 0 deletions src/GraphQL/DecoratableTypeResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Drupal\graphql\GraphQL;

/**
* A base class for decoratable type resolvers.
*
* @package Drupal\graphql\GraphQL
* @see \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface
*/
abstract class DecoratableTypeResolver implements DecoratableTypeResolverInterface {

/**
* The previous type resolver that was set in the chain.
*
* @var \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface|null
*/
private ?DecoratableTypeResolverInterface $decorated;

/**
* Create a new decoratable type resolver.
*
* @param \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface|null $resolver
* The previous type resolver if any.
*/
public function __construct(?DecoratableTypeResolverInterface $resolver) {
$this->decorated = $resolver;
}

/**
* Resolve the type for the provided object.
*
* @param mixed $object
* The object to resolve to a concrete type.
*
* @return string|null
* The GraphQL type name or NULL if this resolver could not determine it.
*/
abstract protected function resolve($object) : ?string;

/**
* Allows this type resolver to be called by the GraphQL library.
*
* Takes care of chaining the various type resolvers together and invokes the
* `resolve` method for each concrete implementation in the chain.
*
* @param mixed $object
* The object to resolve to a concrete type.
*
* @return string
* The resolved GraphQL type name.
*
* @throws \RuntimeException
* When a type was passed for which no type resolver exists in the chain.
*/
public function __invoke($object) : string {
$type = $this->resolve($object);
if ($type !== NULL) {
return $type;
}

if ($this->decorated !== NULL) {
$type = $this->decorated->__invoke($object);
if ($type !== NULL) {
return $type;
}
}

$klass = get_class($object);
throw new \RuntimeException("Can not map instance of '${klass}' to concrete GraphQL Type.");
}

}
62 changes: 62 additions & 0 deletions src/GraphQL/DecoratableTypeResolverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Drupal\graphql\GraphQL;

/**
* A decoratable type resolver to resolve GraphQL interfaces to concrete types.
*
* Type resolvers should extend this class so that they can be chained in
* schema extensions plugins.
*
* For example with the following class defined.
* ```php
* class ConcreteTypeResolver extends DecoratableTypeResolver {
*
* protected function resolve($object) : ?string {
* return $object instanceof MyType ? 'MyType' : NULL;
* }
* }
* ```
*
* A schema extension would call:
* ```php
* $registry->addTypeResolver(
* 'InterfaceType',
* new ConcreteTypeResolver($registry->getTypeResolver('InterfaceType'))
* );
* ```
*
* TypeResolvers should not extend other type resolvers but always extend this
* class directly. Classes will be called in the reverse order of being added
* (classes added last will be called first).
*
* @package Drupal\social_graphql\GraphQL
*/
interface DecoratableTypeResolverInterface {

/**
* Create a new decoratable type resolver.
*
* @param \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface|null $resolver
* The previous type resolver if any.
*/
public function __construct(?DecoratableTypeResolverInterface $resolver);

/**
* Allows this type resolver to be called by the GraphQL library.
*
* Takes care of chaining the various type resolvers together and invokes the
* `resolve` method for each concrete implementation in the chain.
*
* @param mixed $object
* The object to resolve to a concrete type.
*
* @return string
* The resolved GraphQL type name.
*
* @throws \RuntimeException
* When a type was passed for which no type resolver exists in the chain.
*/
public function __invoke($object) : string;

}
70 changes: 70 additions & 0 deletions tests/src/Kernel/TypeResolver/DecoratableTypeResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Drupal\Tests\graphql\Kernel\TypeResolver;

use Drupal\graphql\GraphQL\DecoratableTypeResolver;
use Drupal\node\NodeInterface;
use Drupal\Tests\graphql\Kernel\GraphQLTestBase;

/**
* Test the pages type resolver.
*/
class DecoratableTypeResolverTest extends GraphQLTestBase {

/**
* The type resolver.
*
* @var \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface
*/
protected $resolver;

/**
* The decorated type resolver.
*
* @var \Drupal\graphql\GraphQL\DecoratableTypeResolverInterface
*/
protected $decoratedResolver;

/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->resolver = $this->getMockForAbstractClass(DecoratableTypeResolver::class, [NULL]);
$this->resolver->method('resolve')
->willReturnCallback(function ($object) {
return ucfirst($object->bundle());
});

$this->decoratedResolver = $this->getMockForAbstractClass(DecoratableTypeResolver::class, [$this->resolver]);
$this->decoratedResolver->method('resolve')
->willReturnCallback(function ($object) {
if ($object->bundle(
) === 'article') {
return 'DecoratedArticle';
}
return NULL;
});

}

/**
* Test the decoration.
*/
public function testDecoration(): void {
$newsNode = $this->createMock(NodeInterface::class);
$newsNode->method('bundle')
->willReturn('news');

$articleNode = $this->createMock(NodeInterface::class);
$articleNode->method('bundle')
->willReturn('article');

$this->assertEquals('News', $this->resolver->__invoke($newsNode));
$this->assertEquals('Article', $this->resolver->__invoke($articleNode));

$this->assertEquals('News', $this->decoratedResolver->__invoke($newsNode));
$this->assertEquals('DecoratedArticle', $this->decoratedResolver->__invoke($articleNode));
}

}