Skip to content

Commit

Permalink
Add Input Builder feature. Rename InputType to Input. Tests + docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincz committed Oct 20, 2018
1 parent ae4c5b1 commit be2dc20
Show file tree
Hide file tree
Showing 20 changed files with 594 additions and 99 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"symfony/process": "^3.4 || ^4.0",
"symfony/security-bundle": "^3.4 || ^4.0",
"symfony/templating": "^3.4 || ^4.0",
"symfony/validator": "^3.4 || ^4.0",
"symfony/web-profiler-bundle": "^3.4 || ^4.0",
"symfony/yaml": "^3.4 || ^4.0"
},
Expand Down
18 changes: 13 additions & 5 deletions docs/annotations/annotations-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ In the following reference examples, the `use Overblog\GraphQLBundle\Annotation

- For example, `@GQL\Access("isAuthenticated()")` will be converted to `['access' => '@=isAuthenticated()']`.

- You can use multiple type annotations on the same class. For example, if you need your class to be a Graphql Type AND a Graphql InputType, you just need to add the two annotations. Incompatible annotations or properties for a specified Type will simply be ignored.
- You can use multiple type annotations on the same class. For example, if you need your class to be a Graphql Type AND a Graphql Input, you just need to add the two annotations. Incompatible annotations or properties for a specified Type will simply be ignored.

In the following example, both the type `Coordinates` and the input type `CoordinatesInput` will be generated.
As fields on input type don't support resolvers, the field `elevation` will simply be ignored to generate the input type (it will only have two fields: `latitude` and `longitude`).
Expand All @@ -18,7 +18,7 @@ As fields on input type don't support resolvers, the field `elevation` will simp

/**
* @GQL\Type
* @GQL\InputType
* @GQL\Input
*/
class Coordinates {
/**
Expand Down Expand Up @@ -57,7 +57,7 @@ class Coordinates {

@FieldBuilder

@InputType
@Input

@IsPublic

Expand Down Expand Up @@ -174,7 +174,7 @@ class Hero {

## @Description

This annotation is used in conjonction with one of `@Enum`, `@Field`, `@InputType`, `@Scalar`, `@Type`, `@TypeInterface`, `@Union` to set a description for the GraphQL object.
This annotation is used in conjonction with one of `@Enum`, `@Field`, `@Input`, `@Scalar`, `@Type`, `@TypeInterface`, `@Union` to set a description for the GraphQL object.

Example

Expand Down Expand Up @@ -205,6 +205,8 @@ Optional attributes:
- **name** : The GraphQL name of the enum (default to the class name without namespace)
- **values** : An array of `@EnumValue`to define description or deprecated reason of enum values

The class will also be used by the `Input Builder` service when an `Enum` is encoutered in a Mutation or Query Input. A property accessor will try to populate a property name `value`.

Example:

```php
Expand All @@ -223,10 +225,14 @@ class Planet
const TATOUINE = "2";
const HOTH = "3";
const BESPIN = "4";

public $value;
}
?>
```

In the example above, if a query or mutation has this Enum as an argument, the value will be an instanceof the class with the enum value as the `value` property. (see [The Input Builder documentation](input-builder.md)).

## @EnumValue

This annotation is used in the `values` attribute on the `@Enum` annotation to add a description or deprecation reason on his value. See `@Enum` example above.
Expand Down Expand Up @@ -308,7 +314,7 @@ class Hero {
?>
```

## @InputType
## @Input

This annotation is used on a _class_ to define an input type.
An Input type is pretty much the same as an input except:
Expand All @@ -320,6 +326,8 @@ Optional attributes:
- **name** : The GraphQL name of the input field (default to classnameInput )
- **isRelay** : Set to true if you want your input to be relay compatible (ie. An extra field `clientMutationId` will be added to the input)

The corresponding class will also be used by the `Input Builder` service. A instance of the corresponding class will be use as the `input` value if it is an argument of a query or mutation. (see [The Input Builder documentation](input-builder.md)).

## @IsPublic

Added on a _class_ in conjonction with `@Type` or `@TypeInterface`, this annotation will define the defaut to set if fields are public or not.
Expand Down
27 changes: 27 additions & 0 deletions docs/annotations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
## Annotations reference
- [Annotations reference](annotations-reference.md)

## Input populating & validation
- [The Input Builder](input-builder.md)

## Annotations & type inheritance

As PHP classes naturally support inheritances (and so is the annotation reader), it doesn't make sense to allow classes to use the "inherits" option.
The type will inherits the annotations declared on parent classes properties and methods. The annotation on the class itself will not be herited.


## Annotations, Root Query & Root Mutation

If you define your Root Query, or Root Mutation as a class with annotations, it will allow you to define methods directly on the class itself to be expose as GraphQL.
Expand Down Expand Up @@ -48,6 +52,21 @@ The type can be auto-guess from :
- `@ORM\ManyToOne`, `@ORM\OneToOne` The generated type will also use the `@ORM\JoinColumn` annotation and his `nullable` attribute to generate either `Type` or `Type!`
- `@ORM\ManyToMany`, `@ORM\OneToMany` The generated type will always be not null, like `[Type]!` as you're supposed to initialize corresponding properties with an ArrayCollection

You can also provide your own doctrine / GraphQL types mappings in the bundle configuration.
For example:


```yaml (graphql.yaml)
overblog_graphql:
...
doctrine:
types_mapping:
text[]: "[String]"
datetime: DateTime # If you have registered this custom scalar

```


### @Field type auto-guessing when applied on a method with a return type hint

The type of a `@Field` annotation can be auto-guessed if it applies on a method with a return type hint.
Expand Down Expand Up @@ -104,3 +123,11 @@ As PHP type hinting doesn't support "array of instances of class", we cannot rel
In these case, you'll need to declare your types or arguments type manually.

For example, in PHP, a signature like this : `public function getArrayOfStrings(): string[] {}` is invalid.








102 changes: 102 additions & 0 deletions docs/annotations/input-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# The Input Builder service

When using annotation, as we use classes to describe our GraphQL objects, it is also possible to create and populate classes instances using GraphQL data.
If a class is used to describe a GraphQL Input, this same class can be instanciated to hold the corresponding GraphQL Input data.
This is where the `Input Builder` comes into play. Knowing the matching between GraphQL types and PHP classes, the service is able to instanciate a PHP classes and populate it with data based on the corresponding GraphQL type.
To invoke the Input Builder, we use the `input` expression function in our resolvers.

## the `input` function in expression language

The `input` function take two parameter, a GraphQL type (in GraphQL notation, eventually with the "[]" and "!") and a variable containing data.
This function will use the `Input Builder` service to create an instance of the PHP class matching the GraphQL type if it has one and using a property accessor, it will populate the instance, and will use the `validator` service to validate it.
The transformation is done recursively. If an Input include another Input as field, it will also be populated the same way.

For example:

```php
namespace App\GraphQL\Input;

use Overblog\GraphQLBundle\Annotation as GQL;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @GQL\Input
*/
class UserRegisterInput {
/**
* @GQL\Field(type="String!")
* @Assert\NotBlank
* @Assert\Length(min = 2, max = 50)
*/
public $username;

/**
* @GQL\Field(type="String!")
* @Assert\NotBlank
* @Assert\Email
*/
public $email;

/**
* @GQL\Field(type="String!")
* @Assert\NotBlank
* @Assert\Length(
* min = 5,
* minMessage="The password must be at least 5 characters long."
* )
*/
public $password;

/**
* @GQL\Field(type="Int!")
* @Assert\NotBlank
* @Assert\GreaterThan(18)
*/
public $age;
}

....

/**
* @GQL\Provider
*/
class UserRepository {
/**
* @GQL\Mutation
*/
public function createUser(UserRegisterInput $input) : User {
// Use the validated $input here
$user = new User();
$user->setUsername($input->username);
$user->setPassword($input->password);
...
}
}
```

When this Input is used in a mutation, the Symfony service `overblog_graphql.input_builder` is called in order to transform the received array of data into a `UserRegisterInput` instance using a property accessor.
Then the `validator` service is used to validate this instance against the configured constraints.
The mutation received the valid instance.

In the above example, everything is auto-guessed and a Provider is used. But this would be the same as :

```php
/**
* @GQL\Type
*/
class RootMutation {
/**
* @GQL\Field(
* type="User",
* args={
* @GQL\Arg(name="input", type="UserRegisterInput")
* },
* resolve="@=service('UserRepository').createUser(input(arg['input']))"
* )
*/
public $createUser;
}
```

So, the resolver (the `createUser` method) will receive an instance of the class `UserRegisterInput` instead of an array of data.

1 change: 1 addition & 0 deletions docs/definitions/expression-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All definition config entries can use expression language but it must be explici
| boolean **isTypeOf**(string $className) | Verified if `value` is instance of className | @=isTypeOf('AppBundle\\User\\User') |
| mixed **resolver**(string $alias, array $args = []) | call the method on the tagged service "overblog_graphql.resolver" with args | @=resolver('blog_by_id', [value['blogID']] | res |
| mixed **mutation**(string $alias, array $args = []) | call the method on the tagged service "overblog_graphql.mutation" with args | @=mutation('remove_post_from_community', [value]) | mut |
| mixed **input**(string $type, mixed $data) | Transform and validate an input using the `Input Builder` service. (see [The Input Builder ](input-builder.md)) | @=service('my_service').method(input(value)) |
| string **globalId**(string\|int id, string $typeName = null) | Relay node globalId | @=globalId(15, 'User') |
| array **fromGlobalId**(string $globalId) | Relay node fromGlobalId | @=fromGlobalId('QmxvZzox') |
| object **newObject**(string $className, array $args = []) | Instantiation $className object with $args | @=newObject('AppBundle\\User\\User', ['John', 15]) |
Expand Down
2 changes: 1 addition & 1 deletion src/Annotation/InputType.php → src/Annotation/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @Annotation
* @Target("CLASS")
*/
final class InputType implements Annotation
final class Input implements Annotation
{
/**
* Type name.
Expand Down
2 changes: 0 additions & 2 deletions src/Annotation/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ abstract class Operation implements Annotation
/**
* Operation Type.
*
* @required
*
* @var string
*/
public $type;
Expand Down
Loading

0 comments on commit be2dc20

Please sign in to comment.