Skip to content

Adding support for inherited types based on GraphQL interface #44

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

Merged
merged 7 commits into from
Nov 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,75 @@ class PostType implements FromSourceFieldsInterface
```


Inheritance
-----------

Your PHP model extend each others. GraphQL-controllers will do its best to represent this hierarchy of objects in GraphQL using interfaces.

Let's say you have 2 classes: `Contact` and `User` (which extends `Contact`)

```php
class Contact
{
// ...
}

class User extends Contact
{
// ...
}
```

Let's say you create 2 types for those 2 classes:

```php
/**
* @Type(class=Contact::class)
*/
class ContactType
{
// ...
}

/**
* @Type(class=User::class)
*/
class UserType
{
// ...
}
```

Now, let's assume you have a query that returns a contact:

```
class ContactController
{
/**
* @Query()
*/
public function getContact(): Contact
{
// ...
}
}
```

When writing a GraphQL query, you can query using fragments:

```graphql
getContact {
name
... User {
email
}
}
```

Behind the scene, GraphQL-controllers will detect that the `Contact` class is extended by the `User` class. Because the
class is extended, a GraphQL `ContactInterface` interface is created dynamically. You don't have to do anything.
The GraphQL `User` type will automatically implement this `ContactInterface`. The interface contains all the fields
available in the `Contact` type.

Troubleshooting
---------------
Expand Down
106 changes: 106 additions & 0 deletions src/AnnotationReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php


namespace TheCodingMachine\GraphQL\Controllers;


use Doctrine\Common\Annotations\Reader;
use ReflectionClass;
use ReflectionMethod;
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest;
use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException;
use TheCodingMachine\GraphQL\Controllers\Annotations\Logged;
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
use TheCodingMachine\GraphQL\Controllers\Annotations\SourceField;
use TheCodingMachine\GraphQL\Controllers\Annotations\Type;

class AnnotationReader
{
/**
* @var Reader
*/
private $reader;

public function __construct(Reader $reader)
{
$this->reader = $reader;
}

public function getTypeAnnotation(ReflectionClass $refClass): ?Type
{
try {
/** @var Type|null $typeField */
$typeField = $this->getClassAnnotation($refClass, Type::class);
} catch (ClassNotFoundException $e) {
throw ClassNotFoundException::wrapException($e, $refClass->getName());
}
return $typeField;
}

public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest
{
/** @var AbstractRequest|null $queryAnnotation */
$queryAnnotation = $this->reader->getMethodAnnotation($refMethod, $annotationName);
return $queryAnnotation;
}

public function getLoggedAnnotation(ReflectionMethod $refMethod): ?Logged
{
/** @var Logged|null $loggedAnnotation */
$loggedAnnotation = $this->reader->getMethodAnnotation($refMethod, Logged::class);
return $loggedAnnotation;
}

public function getRightAnnotation(ReflectionMethod $refMethod): ?Right
{
/** @var Right|null $rightAnnotation */
$rightAnnotation = $this->reader->getMethodAnnotation($refMethod, Right::class);
return $rightAnnotation;
}

/**
* @return SourceField[]
*/
public function getSourceFields(ReflectionClass $refClass): array
{
/** @var SourceField[] $sourceFields */
$sourceFields = $this->getClassAnnotations($refClass);
$sourceFields = \array_filter($sourceFields, function($annotation): bool {
return $annotation instanceof SourceField;
});
return $sourceFields;
}

/**
* Returns a class annotation. Finds in the parents if not found in the main class.
*
* @return object|null
*/
private function getClassAnnotation(ReflectionClass $refClass, string $annotationClass)
{
do {
$type = $this->reader->getClassAnnotation($refClass, $annotationClass);
if ($type !== null) {
return $type;
}
$refClass = $refClass->getParentClass();
} while ($refClass);
return null;
}

/**
* Returns the class annotations. Finds in the parents too.
*
* @return object[]
*/
public function getClassAnnotations(ReflectionClass $refClass): array
{
$annotations = [];
do {
$annotations = array_merge($this->reader->getClassAnnotations($refClass), $annotations);
$refClass = $refClass->getParentClass();
} while ($refClass);
return $annotations;
}

}
43 changes: 0 additions & 43 deletions src/AnnotationUtils.php

This file was deleted.

20 changes: 20 additions & 0 deletions src/Annotations/Exceptions/ClassNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php


namespace TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions;


use InvalidArgumentException;

class ClassNotFoundException extends InvalidArgumentException
{
public static function couldNotFindClass(string $className): self
{
return new self("Could not autoload class '$className'");
}

public static function wrapException(self $e, string $className): self
{
return new self($e->getMessage()." defined in @Type annotation of class '$className'");
}
}
5 changes: 5 additions & 0 deletions src/Annotations/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace TheCodingMachine\GraphQL\Controllers\Annotations;

use function class_exists;
use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException;
use TheCodingMachine\GraphQL\Controllers\MissingAnnotationException;

/**
Expand Down Expand Up @@ -31,6 +33,9 @@ public function __construct(array $attributes = [])
throw new MissingAnnotationException('In annotation @Type, missing compulsory parameter "class".');
}
$this->className = $attributes['class'];
if (!class_exists($this->className)) {
throw ClassNotFoundException::couldNotFindClass($this->className);
}
}

/**
Expand Down
23 changes: 9 additions & 14 deletions src/ControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use function gettype;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\OutputType;
use TheCodingMachine\GraphQL\Controllers\Annotations\Exceptions\ClassNotFoundException;
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
use function is_object;
use Iterator;
Expand All @@ -25,7 +26,6 @@
use phpDocumentor\Reflection\Types\Null_;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\String_;
use Doctrine\Common\Annotations\Reader;
use phpDocumentor\Reflection\Types\Integer;
use ReflectionClass;
use TheCodingMachine\GraphQL\Controllers\Annotations\AbstractRequest;
Expand Down Expand Up @@ -53,7 +53,7 @@ class ControllerQueryProvider implements QueryProviderInterface
*/
private $controller;
/**
* @var Reader
* @var AnnotationReader
*/
private $annotationReader;
/**
Expand Down Expand Up @@ -141,8 +141,7 @@ private function getFieldsByAnnotations(string $annotationName, bool $injectSour

foreach ($refClass->getMethods() as $refMethod) {
// First, let's check the "Query" or "Mutation" or "Field" annotation
/** @var AbstractRequest $queryAnnotation */
$queryAnnotation = $this->annotationReader->getMethodAnnotation($refMethod, $annotationName);
$queryAnnotation = $this->annotationReader->getRequestAnnotation($refMethod, $annotationName);

if ($queryAnnotation !== null) {
if (!$this->isAuthorized($refMethod)) {
Expand Down Expand Up @@ -227,10 +226,7 @@ private function getSourceFields(): array
$contextFactory = new ContextFactory();

/** @var SourceField[] $sourceFields */
$sourceFields = AnnotationUtils::getClassAnnotations($this->annotationReader, $refClass);
$sourceFields = \array_filter($sourceFields, function($annotation): bool {
return $annotation instanceof SourceField;
});
$sourceFields = $this->annotationReader->getSourceFields($refClass);

if ($this->controller instanceof FromSourceFieldsInterface) {
$sourceFields = array_merge($sourceFields, $this->controller->getSourceFields());
Expand All @@ -240,8 +236,7 @@ private function getSourceFields(): array
return [];
}

/** @var \TheCodingMachine\GraphQL\Controllers\Annotations\Type|null $typeField */
$typeField = AnnotationUtils::getClassAnnotation($this->annotationReader, $refClass, \TheCodingMachine\GraphQL\Controllers\Annotations\Type::class);
$typeField = $this->annotationReader->getTypeAnnotation($refClass);

if ($typeField === null) {
throw MissingAnnotationException::missingTypeExceptionToUseSourceField();
Expand Down Expand Up @@ -337,14 +332,14 @@ private function getMethodFromPropertyName(\ReflectionClass $reflectionClass, st
*/
private function isAuthorized(\ReflectionMethod $reflectionMethod) : bool
{
$loggedAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Logged::class);
$loggedAnnotation = $this->annotationReader->getLoggedAnnotation($reflectionMethod);

if ($loggedAnnotation !== null && !$this->authenticationService->isLogged()) {
return false;
}

/** @var Right $rightAnnotation */
$rightAnnotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, Right::class);

$rightAnnotation = $this->annotationReader->getRightAnnotation($reflectionMethod);

if ($rightAnnotation !== null && !$this->authorizationService->isAllowed($rightAnnotation->getName())) {
return false;
Expand Down Expand Up @@ -524,7 +519,7 @@ private function toGraphQlType(Type $type, bool $mapToInputType): GraphQLType
if ($mapToInputType) {
return $this->typeMapper->mapClassToInputType($className);
} else {
return $this->typeMapper->mapClassToType($className);
return $this->typeMapper->mapClassToInterfaceOrType($className);
}
} elseif ($type instanceof Array_) {
return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $mapToInputType)));
Expand Down
9 changes: 0 additions & 9 deletions src/GlobControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,11 @@

namespace TheCodingMachine\GraphQL\Controllers;

use Doctrine\Common\Annotations\Reader;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\OutputType;
use Mouf\Composer\ClassNameMapper;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use TheCodingMachine\GraphQL\Controllers\AggregateControllerQueryProvider;
use TheCodingMachine\GraphQL\Controllers\Annotations\Type;
use TheCodingMachine\GraphQL\Controllers\AnnotationUtils;
use TheCodingMachine\GraphQL\Controllers\QueryField;
use TheCodingMachine\GraphQL\Controllers\QueryProviderInterface;
use TheCodingMachine\GraphQL\Controllers\Registry\RegistryInterface;
use TheCodingMachine\GraphQL\Controllers\TypeGenerator;

/**
* Scans all the classes in a given namespace of the main project (not the vendor directory).
Expand Down
Loading