Skip to content

Commit c1a6502

Browse files
authored
Merge pull request #87 from moufmouf/ID_class
Adding an ID PHP class to map ID GraphQL type easily
2 parents 9240120 + 4eac9bf commit c1a6502

File tree

10 files changed

+297
-57
lines changed

10 files changed

+297
-57
lines changed

docs/mutations.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
id: mutations
3+
title: Writing mutations
4+
sidebar_label: Mutations
5+
---
6+
7+
In GraphQL-Controllers, mutations are created [just like queries](my_first_query.md).
8+
9+
To create a mutation, you annotate a method in a controller with the `@Mutation` annotation.
10+
11+
Here is a sample of a "saveProduct" query:
12+
13+
```php
14+
namespace App\Controllers;
15+
16+
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
17+
18+
class ProductController
19+
{
20+
/**
21+
* @Mutation
22+
*/
23+
public function saveProduct(int $id, string $name, ?float $price = null): Product
24+
{
25+
// Some code that saves a product.
26+
}
27+
}
28+
```

docs/my_first_query.md

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,26 @@ class MyController
2828
}
2929
```
3030

31-
- The `MyController` class does not need to extend any base class. For GraphQL-Controllers, a controller is simply a
32-
simple class.
31+
- The `MyController` class does not need to extend any base class. For GraphQL-Controllers, a controller can be any
32+
class.
3333
- The query method is annotated with a `@Query` annotation
3434
- The `MyController` class must be in the controllers namespace. You configured this namespace when you installed
35-
GraphqlControllers. By default, in Symfony, the controllers namespace is `App\Controller`.
35+
GraphQL-Controllers. By default, in Symfony, the controllers namespace is `App\Controller`.
3636

3737
<div class="alert alert-warning"><strong>Heads up!</strong> The <code>MyController</code> class must exist in the container of your
38-
application and the container identifier MUST be the fully qualified class name.</div>
39-
40-
<div class="alert alert-info">If you are using the Symfony bundle (or a framework with autowiring like Laravel), this
38+
application and the container identifier MUST be the fully qualified class name.<br/><br/>
39+
If you are using the Symfony bundle (or a framework with autowiring like Laravel), this
4140
is usually not an issue as the container will automatically create the controller entry if you do not explicitly
42-
declare it.</div>
41+
declare it.</div>
4342

4443
## Testing the query
4544

46-
By default, the GraphQL endpoint is "/graphql".
45+
By default, the GraphQL endpoint is "/graphql". You can send HTTP requests to this endpoint and get responses.
4746

4847
The easiest way to test a GraphQL endpoint is to use [GraphiQL](https://github.com/graphql/graphiql) or
4948
[Altair](https://altair.sirmuel.design/) test clients.
5049

51-
These clients come with Chrome and Firefox plugins.
50+
These clients are available as Chrome or Firefox plugins.
5251

5352
<div class="alert alert-info"><strong>Symfony users:</strong> If you are using the Symfony bundle, GraphiQL is also directly embedded.
5453
Simply head to <code>http://[path-to-my-app]/graphiql</code></div>
@@ -180,7 +179,7 @@ We are now ready to run our test query:
180179
<td style="width:50%">
181180
<strong>Query</strong>
182181
<pre><code>{
183-
products {
182+
product(id: 42) {
184183
name
185184
}
186185
}</code></pre>
@@ -189,11 +188,9 @@ We are now ready to run our test query:
189188
<strong>Answer</strong>
190189
<pre><code class="hljs css language-json">{
191190
"data": {
192-
"products": [
193-
{
191+
"product": {
194192
"name": "Mouf"
195-
}
196-
]
193+
}
197194
}
198195
}</code></pre>
199196
</td>
@@ -222,9 +219,9 @@ If you have never worked with annotations before, here are a few things you shou
222219
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
223220
```
224221
- Doctrine Annotations are hugely popular and used in many other libraries. They are widely supported in PHP IDEs.
225-
We highly recommend you add support for Doctrine annotations in your preferred IDE:
226-
- use [*PHP Annotations* if you use PHPStorm](https://plugins.jetbrains.com/plugin/7320-php-annotations)
227-
- use [*Doctrine plugin* if you use Eclipse](https://marketplace.eclipse.org/content/doctrine-plugin)
222+
We highly recommend you add support for Doctrine annotations in your favorite IDE:
223+
- use [*"PHP Annotations"* if you use PHPStorm](https://plugins.jetbrains.com/plugin/7320-php-annotations)
224+
- use [*"Doctrine plugin"* if you use Eclipse](https://marketplace.eclipse.org/content/doctrine-plugin)
228225
- Netbeans has native support
229226
- ...
230227

docs/type_mapping.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
id: type_mapping
3+
title: Type mapping
4+
sidebar_label: Type mapping
5+
---
6+
7+
The job of GraphQL-Controllers is to create GraphQL types from PHP types.
8+
9+
Internally, GraphQL-Controllers uses a "type mapper".
10+
11+
## Mapping a PHP class to a GraphQL type
12+
13+
The ["my first query"](my_first_query.md) documentation page
14+
already explains how to use the `@Type` annotation to map a PHP class to a GraphQL type. Please refer to this documentation
15+
for class mapping
16+
17+
## Mapping of scalar types
18+
19+
Scalar PHP types can be type-hinted to the corresponding GraphQL types:
20+
21+
- string
22+
- int
23+
- bool
24+
- float
25+
26+
## Mapping of ID type
27+
28+
GraphQL comes with a native "ID" type. PHP has no such type.
29+
30+
If you want to expose an "ID" type in your GraphQL model, you have 2 solutions:
31+
32+
### Solution 1: force the outputType
33+
34+
```php
35+
/**
36+
* @Field(outputType="ID")
37+
*/
38+
public function getId(): string
39+
{
40+
41+
}
42+
```
43+
44+
Using the "outputType" attribute of the `@Field` annotation, you can force the output type to "ID".
45+
You can learn more about [forcing output types in the "custom output types" documentation](custom_output_types.md).
46+
47+
### Solution 2: use the "ID" class
48+
49+
```php
50+
use TheCodingMachine\GraphQL\Controllers\Types\ID;
51+
52+
/**
53+
* @Field
54+
*/
55+
public function getId(): ID
56+
{
57+
58+
}
59+
```
60+
61+
Note that you can also use the "ID" class as an input type:
62+
63+
```php
64+
use TheCodingMachine\GraphQL\Controllers\Types\ID;
65+
66+
/**
67+
* @Mutation
68+
*/
69+
public function save(ID $id, string $name): Product
70+
{
71+
72+
}
73+
```
74+
75+
## Mapping of dates
76+
77+
Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty to add one, with sensible defaults.
78+
79+
When used as an output type (i.e. in a "return type"), `DateTimeImmutable` or `DateTimeInterface` PHP classes are
80+
automatically mapped to this `DateTime` GraphQL type.
81+
82+
```php
83+
/**
84+
* @Field
85+
*/
86+
public function getDate(): \DateTimeInterface
87+
{
88+
89+
}
90+
```
91+
92+
The "date" field will be of type "DateTime". In the returned JSON response to a query, the date is formatted as a string
93+
in the ISO8601 format (aka ATOM format).
94+
95+
When used in an "input type" (i.e. in arguments of a method), the <code>DateTime</code> PHP class is not supported.
96+
Only the <code>DateTimeImmutable</code> PHP class is mapped.
97+
98+
<div class="alert alert-success">This is ok:</div>
99+
100+
```php
101+
/**
102+
* @Query
103+
* @return Product[]
104+
*/
105+
public function getProducts(\DateTimeImmutable $fromDate): array
106+
{
107+
108+
}
109+
```
110+
111+
<div class="alert alert-error">But <code>DateTime</code> input type is not supported:</div>
112+
113+
```php
114+
/**
115+
* @Query
116+
* @return Product[]
117+
*/
118+
public function getProducts(\DateTime $fromDate): array // BAD
119+
{
120+
121+
}
122+
```
123+
124+
125+
126+
TODO: ID
127+
128+
TODO: Union type
129+
130+
TODO: External type
131+
TODO: Extend class
132+
TODO: Sourcefield (other doc)
133+

src/FieldsBuilder.php

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeExceptionInterface;
2020
use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory;
2121
use TheCodingMachine\GraphQL\Controllers\Types\CustomTypesRegistry;
22+
use TheCodingMachine\GraphQL\Controllers\Types\ID;
2223
use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver;
2324
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
2425
use Iterator;
@@ -654,19 +655,23 @@ private function toGraphQlType(Type $type, ?GraphQLType $subType, bool $mapToInp
654655
return GraphQLType::float();
655656
} elseif ($type instanceof Object_) {
656657
$fqcn = (string) $type->getFqsen();
657-
if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
658-
return DateTimeType::getInstance();
659-
} elseif ($fqcn === '\\'.UploadedFileInterface::class) {
660-
return CustomTypesRegistry::getUploadType();
661-
} elseif ($fqcn === '\\DateTime') {
662-
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
663-
}
664-
665-
$className = ltrim($type->getFqsen(), '\\');
666-
if ($mapToInputType) {
667-
return $this->typeMapper->mapClassToInputType($className);
668-
} else {
669-
return $this->typeMapper->mapClassToInterfaceOrType($className, $subType);
658+
switch ($fqcn) {
659+
case '\\DateTimeImmutable':
660+
case '\\DateTimeInterface':
661+
return DateTimeType::getInstance();
662+
case '\\'.UploadedFileInterface::class:
663+
return CustomTypesRegistry::getUploadType();
664+
case '\\DateTime':
665+
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
666+
case '\\'.ID::class:
667+
return GraphQLType::id();
668+
default:
669+
$className = ltrim($type->getFqsen(), '\\');
670+
if ($mapToInputType) {
671+
return $this->typeMapper->mapClassToInputType($className);
672+
} else {
673+
return $this->typeMapper->mapClassToInterfaceOrType($className, $subType);
674+
}
670675
}
671676
} elseif ($type instanceof Array_) {
672677
return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType)));

src/QueryField.php

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@
55

66
use function get_class;
77
use GraphQL\Type\Definition\FieldDefinition;
8+
use GraphQL\Type\Definition\IDType;
89
use GraphQL\Type\Definition\InputObjectType;
10+
use GraphQL\Type\Definition\InputType;
911
use GraphQL\Type\Definition\ListOfType;
1012
use GraphQL\Type\Definition\NonNull;
1113
use GraphQL\Type\Definition\OutputType;
1214
use GraphQL\Type\Definition\ScalarType;
1315
use GraphQL\Type\Definition\Type;
16+
use InvalidArgumentException;
17+
use function is_array;
1418
use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface;
1519
use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType;
20+
use TheCodingMachine\GraphQL\Controllers\Types\ID;
1621

1722
/**
1823
* A GraphQL field that maps to a PHP method automatically.
@@ -50,26 +55,7 @@ public function __construct(string $name, OutputType $type, array $arguments, ?c
5055
foreach ($arguments as $name => $arr) {
5156
$type = $arr['type'];
5257
if (isset($args[$name])) {
53-
$val = $args[$name];
54-
55-
$type = $this->stripNonNullType($type);
56-
if ($type instanceof ListOfType) {
57-
$subtype = $this->stripNonNullType($type->getWrappedType());
58-
$val = array_map(function ($item) use ($subtype, $hydrator) {
59-
if ($subtype instanceof DateTimeType) {
60-
return new \DateTimeImmutable($item);
61-
} elseif ($subtype instanceof InputObjectType) {
62-
return $hydrator->hydrate($item, $subtype);
63-
}
64-
return $item;
65-
}, $val);
66-
} elseif ($type instanceof DateTimeType) {
67-
$val = new \DateTimeImmutable($val);
68-
} elseif ($type instanceof InputObjectType) {
69-
$val = $hydrator->hydrate($val, $type);
70-
} elseif (!$type instanceof ScalarType) {
71-
throw new \RuntimeException('Unexpected type: '.get_class($type));
72-
}
58+
$val = $this->castVal($args[$name], $type, $hydrator);
7359
} elseif (array_key_exists('defaultValue', $arr)) {
7460
$val = $arr['defaultValue'];
7561
} else {
@@ -100,4 +86,33 @@ private function stripNonNullType(Type $type): Type
10086
}
10187
return $type;
10288
}
89+
90+
/**
91+
* Casts a value received from GraphQL into an argument passed to a method.
92+
*
93+
* @param mixed $val
94+
* @param InputType $type
95+
* @return mixed
96+
*/
97+
private function castVal($val, InputType $type, HydratorInterface $hydrator)
98+
{
99+
$type = $this->stripNonNullType($type);
100+
if ($type instanceof ListOfType) {
101+
if (!is_array($val)) {
102+
throw new InvalidArgumentException('Expected GraphQL List but value passed is not an array.');
103+
}
104+
return array_map(function($item) use ($type, $hydrator) {
105+
return $this->castVal($item, $type->getWrappedType(), $hydrator);
106+
}, $val);
107+
} elseif ($type instanceof DateTimeType) {
108+
return new \DateTimeImmutable($val);
109+
} elseif ($type instanceof IDType) {
110+
return new ID($val);
111+
} elseif ($type instanceof InputObjectType) {
112+
return $hydrator->hydrate($val, $type);
113+
} elseif (!$type instanceof ScalarType) {
114+
throw new \RuntimeException('Unexpected type: '.get_class($type));
115+
}
116+
return $val;
117+
}
103118
}

src/Types/ID.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
namespace TheCodingMachine\GraphQL\Controllers\Types;
3+
4+
5+
use InvalidArgumentException;
6+
use TheCodingMachine\GraphQL\Controllers\GraphQLException;
7+
8+
/**
9+
* A class that maps to the GraphQL ID type.
10+
*/
11+
class ID
12+
{
13+
private $value;
14+
15+
public function __construct($value)
16+
{
17+
if (! is_scalar($value) && (! is_object($value) || ! method_exists($value, '__toString'))) {
18+
throw new InvalidArgumentException('ID constructor cannot be passed a non scalar value.');
19+
}
20+
$this->value = $value;
21+
}
22+
23+
/**
24+
* @return bool|float|int|string
25+
*/
26+
public function val()
27+
{
28+
return $this->value;
29+
}
30+
31+
public function __toString()
32+
{
33+
return (string) $this->value;
34+
}
35+
}

0 commit comments

Comments
 (0)