Skip to content

Commit

Permalink
Merge 216dc98 into 9240120
Browse files Browse the repository at this point in the history
  • Loading branch information
moufmouf committed Jan 24, 2019
2 parents 9240120 + 216dc98 commit 3d952ba
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 37 deletions.
28 changes: 28 additions & 0 deletions docs/mutations.md
@@ -0,0 +1,28 @@
---
id: mutations
title: Writing mutations
sidebar_label: Mutations
---

In GraphQL-Controllers, mutations are created [just like queries](my_first_query.md).

To create a mutation, you annotate a method in a controller with the `@Mutation` annotation.

Here is a sample of a "saveProduct" query:

```php
namespace App\Controllers;

use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;

class ProductController
{
/**
* @Mutation
*/
public function saveProduct(int $id, string $name, ?float $price = null): Product
{
// Some code that saves a product.
}
}
```
31 changes: 14 additions & 17 deletions docs/my_first_query.md
Expand Up @@ -28,27 +28,26 @@ class MyController
}
```

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

<div class="alert alert-warning"><strong>Heads up!</strong> The <code>MyController</code> class must exist in the container of your
application and the container identifier MUST be the fully qualified class name.</div>

<div class="alert alert-info">If you are using the Symfony bundle (or a framework with autowiring like Laravel), this
application and the container identifier MUST be the fully qualified class name.<br/><br/>
If you are using the Symfony bundle (or a framework with autowiring like Laravel), this
is usually not an issue as the container will automatically create the controller entry if you do not explicitly
declare it.</div>
declare it.</div>

## Testing the query

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

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

These clients come with Chrome and Firefox plugins.
These clients are available as Chrome or Firefox plugins.

<div class="alert alert-info"><strong>Symfony users:</strong> If you are using the Symfony bundle, GraphiQL is also directly embedded.
Simply head to <code>http://[path-to-my-app]/graphiql</code></div>
Expand Down Expand Up @@ -180,7 +179,7 @@ We are now ready to run our test query:
<td style="width:50%">
<strong>Query</strong>
<pre><code>{
products {
product(id: 42) {
name
}
}</code></pre>
Expand All @@ -189,11 +188,9 @@ We are now ready to run our test query:
<strong>Answer</strong>
<pre><code class="hljs css language-json">{
"data": {
"products": [
{
"product": {
"name": "Mouf"
}
]
}
}
}</code></pre>
</td>
Expand Down Expand Up @@ -222,9 +219,9 @@ If you have never worked with annotations before, here are a few things you shou
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
```
- Doctrine Annotations are hugely popular and used in many other libraries. They are widely supported in PHP IDEs.
We highly recommend you add support for Doctrine annotations in your preferred IDE:
- use [*PHP Annotations* if you use PHPStorm](https://plugins.jetbrains.com/plugin/7320-php-annotations)
- use [*Doctrine plugin* if you use Eclipse](https://marketplace.eclipse.org/content/doctrine-plugin)
We highly recommend you add support for Doctrine annotations in your favorite IDE:
- use [*"PHP Annotations"* if you use PHPStorm](https://plugins.jetbrains.com/plugin/7320-php-annotations)
- use [*"Doctrine plugin"* if you use Eclipse](https://marketplace.eclipse.org/content/doctrine-plugin)
- Netbeans has native support
- ...

91 changes: 91 additions & 0 deletions docs/type_mapping.md
@@ -0,0 +1,91 @@
---
id: type_mapping
title: Type mapping
sidebar_label: Type mapping
---

The job of GraphQL-Controllers is to create GraphQL types from PHP types.

Internally, GraphQL-Controllers uses a "type mapper".

## Mapping a PHP class to a GraphQL type

The ["my first query"](my_first_query.md) documentation page
already explains how to use the `@Type` annotation to map a PHP class to a GraphQL type. Please refer to this documentation
for class mapping

## Mapping of scalar types

Scalar PHP types can be type-hinted to the corresponding GraphQL types:

- string
- int
- bool
- float

## Mapping of ID type

GraphQL comes with a native "ID" type. PHP has no such type.

TODO: develop a ID PHP class type? (for input types?)


## Mapping of dates

Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty to add one, with sensible defaults.

When used as an output type (i.e. in a "return type"), `DateTimeImmutable` or `DateTimeInterface` PHP classes are
automatically mapped to this `DateTime` GraphQL type.

```php
/**
* @Field
*/
public function getDate(): \DateTimeInterface
{

}
```

The "date" field will be of type "DateTime". In the returned JSON response to a query, the date is formatted as a string
in the ISO8601 format (aka ATOM format).

When used in an "input type" (i.e. in arguments of a method), the <code>DateTime</code> PHP class is not supported.
Only the <code>DateTimeImmutable</code> PHP class is mapped.

<div class="alert alert-success">This is ok:</div>

```php
/**
* @Query
* @return Product[]
*/
public function getProducts(\DateTimeImmutable $fromDate): array
{

}
```

<div class="alert alert-error">But <code>DateTime</code> input type is not supported:</div>

```php
/**
* @Query
* @return Product[]
*/
public function getProducts(\DateTime $fromDate): array // BAD
{

}
```



TODO: ID

TODO: Union type

TODO: External type
TODO: Extend class
TODO: Sourcefield (other doc)

31 changes: 18 additions & 13 deletions src/FieldsBuilder.php
Expand Up @@ -19,6 +19,7 @@
use TheCodingMachine\GraphQL\Controllers\Mappers\CannotMapTypeExceptionInterface;
use TheCodingMachine\GraphQL\Controllers\Reflection\CachedDocBlockFactory;
use TheCodingMachine\GraphQL\Controllers\Types\CustomTypesRegistry;
use TheCodingMachine\GraphQL\Controllers\Types\ID;
use TheCodingMachine\GraphQL\Controllers\Types\TypeResolver;
use TheCodingMachine\GraphQL\Controllers\Types\UnionType;
use Iterator;
Expand Down Expand Up @@ -654,19 +655,23 @@ private function toGraphQlType(Type $type, ?GraphQLType $subType, bool $mapToInp
return GraphQLType::float();
} elseif ($type instanceof Object_) {
$fqcn = (string) $type->getFqsen();
if ($fqcn === '\\DateTimeImmutable' || $fqcn === '\\DateTimeInterface') {
return DateTimeType::getInstance();
} elseif ($fqcn === '\\'.UploadedFileInterface::class) {
return CustomTypesRegistry::getUploadType();
} elseif ($fqcn === '\\DateTime') {
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
}

$className = ltrim($type->getFqsen(), '\\');
if ($mapToInputType) {
return $this->typeMapper->mapClassToInputType($className);
} else {
return $this->typeMapper->mapClassToInterfaceOrType($className, $subType);
switch ($fqcn) {
case '\\DateTimeImmutable':
case '\\DateTimeInterface':
return DateTimeType::getInstance();
case '\\'.UploadedFileInterface::class:
return CustomTypesRegistry::getUploadType();
case '\\DateTime':
throw new GraphQLException('Type-hinting a parameter against DateTime is not allowed. Please use the DateTimeImmutable type instead.');
case '\\'.ID::class:
return GraphQLType::id();
default:
$className = ltrim($type->getFqsen(), '\\');
if ($mapToInputType) {
return $this->typeMapper->mapClassToInputType($className);
} else {
return $this->typeMapper->mapClassToInterfaceOrType($className, $subType);
}
}
} elseif ($type instanceof Array_) {
return GraphQLType::listOf(GraphQLType::nonNull($this->toGraphQlType($type->getValueType(), $subType, $mapToInputType)));
Expand Down
6 changes: 6 additions & 0 deletions src/QueryField.php
Expand Up @@ -5,6 +5,7 @@

use function get_class;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Type\Definition\IDType;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
Expand All @@ -13,6 +14,7 @@
use GraphQL\Type\Definition\Type;
use TheCodingMachine\GraphQL\Controllers\Hydrators\HydratorInterface;
use TheCodingMachine\GraphQL\Controllers\Types\DateTimeType;
use TheCodingMachine\GraphQL\Controllers\Types\ID;

/**
* A GraphQL field that maps to a PHP method automatically.
Expand Down Expand Up @@ -58,13 +60,17 @@ public function __construct(string $name, OutputType $type, array $arguments, ?c
$val = array_map(function ($item) use ($subtype, $hydrator) {
if ($subtype instanceof DateTimeType) {
return new \DateTimeImmutable($item);
} elseif ($subtype instanceof ID) {
return new ID($item);
} elseif ($subtype instanceof InputObjectType) {
return $hydrator->hydrate($item, $subtype);
}
return $item;
}, $val);
} elseif ($type instanceof DateTimeType) {
$val = new \DateTimeImmutable($val);
} elseif ($type instanceof IDType) {
$val = new ID($val);
} elseif ($type instanceof InputObjectType) {
$val = $hydrator->hydrate($val, $type);
} elseif (!$type instanceof ScalarType) {
Expand Down
34 changes: 34 additions & 0 deletions src/Types/ID.php
@@ -0,0 +1,34 @@
<?php
namespace TheCodingMachine\GraphQL\Controllers\Types;


use TheCodingMachine\GraphQL\Controllers\GraphQLException;

/**
* A class that maps to the GraphQL ID type.
*/
class ID
{
private $value;

public function __construct($value)
{
if (! is_scalar($value) && (! is_object($value) || ! method_exists($value, '__toString'))) {
throw new GraphQLException('ID constructor cannot be passed a non scalar value.');
}
$this->value = $value;
}

/**
* @return bool|float|int|string
*/
public function val()
{
return $this->value;
}

public function __toString()
{
return (string) $this->value;
}
}
10 changes: 6 additions & 4 deletions tests/FieldsBuilderTest.php
Expand Up @@ -58,7 +58,7 @@ public function testQueryProvider()
$usersQuery = $queries[0];
$this->assertSame('test', $usersQuery->name);

$this->assertCount(8, $usersQuery->args);
$this->assertCount(9, $usersQuery->args);
$this->assertSame('int', $usersQuery->args[0]->name);
$this->assertInstanceOf(NonNull::class, $usersQuery->args[0]->getType());
$this->assertInstanceOf(IntType::class, $usersQuery->args[0]->getType()->getWrappedType());
Expand All @@ -72,6 +72,7 @@ public function testQueryProvider()
$this->assertInstanceOf(DateTimeType::class, $usersQuery->args[4]->getType());
$this->assertInstanceOf(DateTimeType::class, $usersQuery->args[5]->getType());
$this->assertInstanceOf(StringType::class, $usersQuery->args[6]->getType());
$this->assertInstanceOf(IDType::class, $usersQuery->args[8]->getType());
$this->assertSame('TestObjectInput', $usersQuery->args[1]->getType()->getWrappedType()->getWrappedType()->getWrappedType()->name);

$context = ['int' => 42, 'string' => 'foo', 'list' => [
Expand All @@ -81,19 +82,20 @@ public function testQueryProvider()
'boolean' => true,
'float' => 4.2,
'dateTimeImmutable' => '2017-01-01 01:01:01',
'dateTime' => '2017-01-01 01:01:01'
'dateTime' => '2017-01-01 01:01:01',
'id' => 42
];

$resolve = $usersQuery->resolveFn;
$result = $resolve('foo', $context);

$this->assertInstanceOf(TestObject::class, $result);
$this->assertSame('foo424212true4.22017010101010120170101010101default', $result->getTest());
$this->assertSame('foo424212true4.22017010101010120170101010101default42', $result->getTest());

unset($context['string']); // Testing null default value
$result = $resolve('foo', $context);

$this->assertSame('424212true4.22017010101010120170101010101default', $result->getTest());
$this->assertSame('424212true4.22017010101010120170101010101default42', $result->getTest());
}

public function testMutations()
Expand Down
6 changes: 4 additions & 2 deletions tests/Fixtures/TestController.php
Expand Up @@ -8,6 +8,7 @@
use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation;
use TheCodingMachine\GraphQL\Controllers\Annotations\Query;
use TheCodingMachine\GraphQL\Controllers\Annotations\Right;
use TheCodingMachine\GraphQL\Controllers\Types\ID;

class TestController
{
Expand All @@ -21,9 +22,10 @@ class TestController
* @param \DateTime|\DateTimeInterface|null $dateTime
* @param string $withDefault
* @param null|string $string
* @param ID|null $id
* @return TestObject
*/
public function test(int $int, array $list, ?bool $boolean, ?float $float, ?\DateTimeImmutable $dateTimeImmutable, ?\DateTimeInterface $dateTime, string $withDefault = 'default', ?string $string = null): TestObject
public function test(int $int, array $list, ?bool $boolean, ?float $float, ?\DateTimeImmutable $dateTimeImmutable, ?\DateTimeInterface $dateTime, string $withDefault = 'default', ?string $string = null, ID $id = null): TestObject
{
$str = '';
foreach ($list as $test) {
Expand All @@ -32,7 +34,7 @@ public function test(int $int, array $list, ?bool $boolean, ?float $float, ?\Dat
}
$str .= $test->getTest();
}
return new TestObject($string.$int.$str.($boolean?'true':'false').$float.$dateTimeImmutable->format('YmdHis').$dateTime->format('YmdHis').$withDefault);
return new TestObject($string.$int.$str.($boolean?'true':'false').$float.$dateTimeImmutable->format('YmdHis').$dateTime->format('YmdHis').$withDefault.($id !== null ? $id->val() : ''));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion website/sidebars.json
@@ -1,6 +1,6 @@
{
"docs": {
"Installation": ["getting-started", "symfony-bundle", "other-frameworks"],
"Usage": ["my-first-query", "input-types", "file-uploads", "custom-output-types"]
"Usage": ["my-first-query", "mutations", "type_mapping", "input-types", "file-uploads", "custom-output-types"]
}
}

0 comments on commit 3d952ba

Please sign in to comment.