Skip to content

Commit

Permalink
Merge pull request #114 from moufmouf/interface_support
Browse files Browse the repository at this point in the history
Adding support for annotating PHP interfaces
  • Loading branch information
moufmouf committed Jul 23, 2019
2 parents 8274600 + fa267ce commit 06fb017
Show file tree
Hide file tree
Showing 55 changed files with 1,372 additions and 256 deletions.
2 changes: 1 addition & 1 deletion docs/extend_type.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Use the `@ExtendType` annotation to add additional fields to a type that is alre
<div class="alert alert-info">
Extending a type has nothing to do with type inheritance.
If you are looking for a way to expose a class and its children classes, have a look at
the <a href="inheritance">Inheritance</a> section</a>
the <a href="inheritance-interfaces">Inheritance</a> section</a>
</div>

Let's assume you have a `Product` class. In order to get the name of a product, there is no `getName()` method in
Expand Down
176 changes: 176 additions & 0 deletions docs/inheritance-interfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
id: inheritance-interfaces
title: Inheritance and interfaces
sidebar_label: Inheritance and interfaces
---

## Modeling inheritance

Some of your entities may extend other entities. GraphQLite will do its best to represent this hierarchy of objects in GraphQL using interfaces.

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

```php
/**
* @Type
*/
class Contact
{
// ...
}

/**
* @Type
*/
class User extends Contact
{
// ...
}
```

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

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

When writing your GraphQL query, you are able to use fragments to retrieve fields from the `User` type:

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

Written in [GraphQL type language](https://graphql.org/learn/schema/#type-language), the representation of types
would look like this:

```graphql
interface ContactInterface {
// List of fields declared in Contact class
}

type Contact implements ContactInterface {
// List of fields declared in Contact class
}

type User implements ContactInterface {
// List of fields declared in Contact and User classes
}
```

Behind the scene, GraphQLite will detect that the `Contact` class is extended by the `User` class.
Because the class is extended, a GraphQL `ContactInterface` interface is created dynamically.

The GraphQL `User` type will also automatically implement this `ContactInterface`. The interface contains all the fields
available in the `Contact` type.

## Mapping interfaces

If you want to create a pure GraphQL interface, you can also add a `@Type` annotation on a PHP interface.

```php
/**
* @Type
*/
interface UserInterface
{
/**
* @Field
*/
public function getUserName(): string;
}
```

This will automatically create a GraphQL interface whose description is:

```graphql
interface UserInterface {
userName: String!
}
```

### Implementing interfaces

You don't have to do anything special to implement an interface in your GraphQL types.
Simply "implement" the interface in PHP and you are done!

```php
/**
* @Type
*/
class User implements UserInterface
{
public function getUserName(): string;
}
```

This will translate in GraphQL schema as:

```graphql
interface UserInterface {
userName: String!
}

type User implements UserInterface {
userName: String!
}
```

Please note that you do not need to put the `@Field` annotation again in the implementing class.

### Interfaces without an explicit implementing type

You don't have to explicitly put a `@Type` annotation on the class implementing the interface (though this
is usually a good idea).

```php
/**
* Look, this class has no @Type annotation
*/
class User implements UserInterface
{
public function getUserName(): string;
}
```

```php
class UserController
{
/**
* @Query()
*/
public function getUser(): UserInterface // This will work!
{
// ...
}
}
```

<div class="alert alert-info">If GraphQLite cannot find a proper GraphQL Object type implementing an interface, it
will create an object type "on the fly".</div>

In the example above, because the `User` class has no `@Type` annotations, GraphQLite will
create a `UserImpl` type that implements `UserInterface`.

```graphql
interface UserInterface {
userName: String!
}

type UserImpl implements UserInterface {
userName: String!
}
```
82 changes: 0 additions & 82 deletions docs/inheritance.md

This file was deleted.

3 changes: 3 additions & 0 deletions docs/type_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ Hopefully, you can force the name of the GraphQL output type using the "name" at
class Product { /* ... */ }
```

<div class="alert alert-info">You can also put a <a href="inheritance-interfaces#mapping-interfaces"><code>@Type</code> annotation on a PHP interface
to map your code to a GraphQL interface</a>.</div>

## Array mapping

You can type-hint against arrays (or iterators) as long as you add a detailed `@return` statement in the PHPDoc.
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

<exclude-pattern>tests/dependencies/*</exclude-pattern>
<exclude-pattern>src/Utils/NamespacedCache.php</exclude-pattern>
<exclude-pattern>src/Types/TypeAnnotatedInterfaceType.php</exclude-pattern>

<!-- Include full Doctrine Coding Standard -->
<rule ref="Doctrine">
Expand Down
3 changes: 2 additions & 1 deletion src/Annotations/ExtendType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use BadMethodCallException;
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
use function class_exists;
use function interface_exists;
use function ltrim;

/**
Expand Down Expand Up @@ -36,7 +37,7 @@ public function __construct(array $attributes = [])
}
$this->class = $attributes['class'] ?? null;
$this->name = $attributes['name'] ?? null;
if ($this->class !== null && ! class_exists($this->class)) {
if ($this->class !== null && ! class_exists($this->class) && ! interface_exists($this->class)) {
throw ClassNotFoundException::couldNotFindClass($this->class);
}
}
Expand Down
16 changes: 15 additions & 1 deletion src/Annotations/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use RuntimeException;
use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException;
use TheCodingMachine\GraphQLite\GraphQLException;
use function class_exists;
use function interface_exists;
use function ltrim;

/**
Expand Down Expand Up @@ -85,9 +87,21 @@ public function getClass(): string
public function setClass(string $class): void
{
$this->class = ltrim($class, '\\');
if (! class_exists($this->class)) {
$isInterface = interface_exists($this->class);
if (! class_exists($this->class) && ! $isInterface) {
throw ClassNotFoundException::couldNotFindClass($this->class);
}

if (! $isInterface) {
return;
}

if ($this->default === false) {
throw new GraphQLException('Problem in annotation @Type for interface "' . $class . '": you cannot use the default="false" attribute on interfaces');
}
if ($this->disableInheritance === true) {
throw new GraphQLException('Problem in annotation @Type for interface "' . $class . '": you cannot use the disableInheritance="true" attribute on interfaces');
}
}

public function isSelfType(): bool
Expand Down
3 changes: 2 additions & 1 deletion src/GlobControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Symfony\Contracts\Cache\CacheInterface as CacheContractInterface;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use function class_exists;
use function interface_exists;
use function str_replace;

/**
Expand Down Expand Up @@ -92,7 +93,7 @@ private function buildInstancesList(): array
$classes = $explorer->getClasses();
$instances = [];
foreach ($classes as $className) {
if (! class_exists($className)) {
if (! class_exists($className) && ! interface_exists($className)) {
continue;
}
$refClass = new ReflectionClass($className);
Expand Down
15 changes: 13 additions & 2 deletions src/Mappers/CannotMapTypeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Exception;
use GraphQL\Error\Error;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
Expand Down Expand Up @@ -66,12 +67,22 @@ public static function mustBeInputType(string $subTypeName): self
return new self('type "' . $subTypeName . '" must be an input type.');
}

public static function createForExtendType(string $className, ObjectType $type): self
/**
* @param NamedType&(ObjectType|InterfaceType) $type
*
* @return CannotMapTypeException
*/
public static function createForExtendType(string $className, NamedType $type): self
{
return new self('cannot extend GraphQL type "' . $type->name . '" mapped by class "' . $className . '". Check your TypeMapper configuration.');
}

public static function createForExtendName(string $name, ObjectType $type): self
/**
* @param NamedType&(ObjectType|InterfaceType) $type
*
* @return CannotMapTypeException
*/
public static function createForExtendName(string $name, NamedType $type): self
{
return new self('cannot extend GraphQL type "' . $type->name . '" with type "' . $name . '". Check your TypeMapper configuration.');
}
Expand Down
Loading

0 comments on commit 06fb017

Please sign in to comment.