Skip to content
Open
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));
}

}