Skip to content
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

Allow using instance of Field #521

Merged
merged 18 commits into from
Nov 12, 2019
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ CHANGELOG

[Next release](https://github.com/rebing/graphql-laravel/compare/3.1.0...master)
--------------
### Added
- Allow passing through an instance of a `Field` [\#521 / georgeboot](https://github.com/rebing/graphql-laravel/pull/521/files)
### Fixed
- Fix validation rules for non-null list of non-null objects [\#511 / crissi](https://github.com/rebing/graphql-laravel/pull/511/files)
- Add morph type to returned models [\#503 / crissi](https://github.com/rebing/graphql-laravel/pull/503)
Expand Down
178 changes: 152 additions & 26 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![License](https://poser.pugx.org/rebing/graphql-laravel/license)](https://packagist.org/packages/rebing/graphql-laravel)
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://join.slack.com/t/rebing-graphql/shared_invite/enQtNTE5NjQzNDI5MzQ4LWVjMTMxNzIyZjBlNTFhZGQ5MDVjZDAwZDNjODA3ODE2NjdiOGJkMjMwMTZkZmNhZjhiYTE1MjEyNDk0MWJmMzk)

### Note: these are the docs for 2.*, [please see the `v1` branch for the 1.* docs](https://github.com/rebing/graphql-laravel/tree/v1#laravel-graphql)
### Note: these are the docs for the current release, [please see the `v1` branch for the 1.* docs](https://github.com/rebing/graphql-laravel/tree/v1#laravel-graphql)

Uses Facebook GraphQL with Laravel 5.5+. It is based on the PHP implementation [here](https://github.com/webonyx/graphql-php). You can find more information about GraphQL in the [GraphQL Introduction](http://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html) on the [React](http://facebook.github.io/react) blog or you can read the [GraphQL specifications](https://facebook.github.io/graphql/). This is a work in progress.

Expand Down Expand Up @@ -101,31 +101,44 @@ To work this around:

## Usage

- [Schemas](#schemas)
- [Creating a query](#creating-a-query)
- [Creating a mutation](#creating-a-mutation)
- [Adding validation to mutation](#adding-validation-to-mutation)
- [File uploads](#file-uploads)
- [Authorization](#authorization)
- [Privacy](#privacy)
- [Query variables](#query-variables)
- [Custom field](#custom-field)
- [Eager loading relationships](#eager-loading-relationships)
- [Type relationship query](#type-relationship-query)
- [Pagination](#pagination)
- [Batching](#batching)
- [Scalar Types](#scalar-types)
- [Enums](#enums)
- [Unions](#unions)
- [Interfaces](#interfaces)
- [Input Object](#input-object)
- [JSON Columns](#json-columns)
- [Field deprecation](#field-deprecation)
- [Default Field Resolver](#default-field-resolver)
- [Upgrading from v1 to v2](#upgrading-from-v1-to-v2)
- [Migrating from Folklore](#migrating-from-folklore)
- [Performance considerations](#performance-considerations)
- [Wrap Types](#wrap-types)
- [Laravel GraphQL](#laravel-graphql)
- [Note: these are the docs for 3.*, please see the `v1` branch for the 1.* docs](#note-these-are-the-docs-for-3-please-see-the-v1-branch-for-the-1-docs)
- [Installation](#installation)
- [Dependencies:](#dependencies)
- [Installation:](#installation)
- [Laravel 5.5+](#laravel-55)
- [Lumen (experimental!)](#lumen-experimental)
- [Usage](#usage)
- [Schemas](#schemas)
- [Creating a query](#creating-a-query)
- [Creating a mutation](#creating-a-mutation)
- [Adding validation to a mutation](#adding-validation-to-a-mutation)
- [File uploads](#file-uploads)
- [Authorization](#authorization)
- [Privacy](#privacy)
- [Query Variables](#query-variables)
- [Custom field](#custom-field)
- [Even better reusable fields](#even-better-reusable-fields)
- [Eager loading relationships](#eager-loading-relationships)
- [Type relationship query](#type-relationship-query)
- [Pagination](#pagination)
- [Batching](#batching)
- [Scalar Types](#scalar-types)
- [Enums](#enums)
- [Unions](#unions)
- [Interfaces](#interfaces)
- [Sharing Interface fields](#sharing-interface-fields)
- [Input Object](#input-object)
- [JSON Columns](#json-columns)
- [Field deprecation](#field-deprecation)
- [Default Field Resolver](#default-field-resolver)
- [Guides](#guides)
- [Upgrading from v1 to v2](#upgrading-from-v1-to-v2)
- [Migrating from Folklore](#migrating-from-folklore)
- [Performance considerations](#performance-considerations)
- [Lazy loading of types](#lazy-loading-of-types)
- [Example of aliasing **not** supported by lazy loading](#example-of-aliasing-not-supported-by-lazy-loading)
- [Wrap Types](#wrap-types)

### Schemas

Expand Down Expand Up @@ -810,7 +823,120 @@ class UserType extends GraphQLType
];
}
}
```

#### Even better reusable fields

Instead of using the class name, you can also supply an actual instance of the `Field`. This allows you to not only re-use the field, but will also open up the possibility to re-use the resolver.

Let's imagine we want a field type that can output dates formatted in all sorts of ways.

```php
<?php

namespace App\GraphQL\Fields;

use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Field;

class FormattableDate extends Field
{
protected $attributes = [
'description' => 'A field that can output a date in all sorts of ways.',
];

public function __construct(array $settings = [])
{
$this->attributes = \array_merge($this->attributes, $settings);
}

public function type(): Type
{
return Type::string();
}

public function args(): array
{
return [
'format' => [
'type' => Type::string(),
'defaultValue' => 'Y-m-d H:i',
'description' => 'Defaults to Y-m-d H:i',
],
'relative' => [
'type' => Type::boolean(),
'defaultValue' => false,
],
];
}

protected function resolve($root, $args): ?string
{
$date = $root->{$this->getProperty()};

if (!$date instanceof Carbon) {
return null;
}

if ($args['relative']) {
return $date->diffForHumans();
}

return $date->format($args['format']);
}

protected function getProperty(): string
{
return $this->attributes['alias'] ?? $this->attributes['name'];
}
}
```

You can use this field in your type as follows:

```php
<?php

namespace App\GraphQL\Types;

use App\GraphQL\Fields\FormattableDate;
use App\User;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Type as GraphQLType;

class UserType extends GraphQLType
{
protected $attributes = [
'name' => 'User',
'description' => 'A user',
'model' => User::class,
];

public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The id of the user'
],
'email' => [
'type' => Type::string(),
'description' => 'The email of user'
],

// You can simply supply an instance of the class
'dateOfBirth' => new FormattableDate,

// Because the constructor of `FormattableDate` accepts our the array of parameters,
// we can override them very easily.
// Imagine we want our field to be called `createdAt`, but our database column
// is called `created_at`:
'createdAt' => new FormattableDate([
'alias' => 'created_at',
])
];
}
}
```

### Eager loading relationships
Expand Down
3 changes: 3 additions & 0 deletions src/Support/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
use Rebing\GraphQL\Error\ValidationError;
use Validator;

/**
* @property string $name
*/
abstract class Field
{
protected $attributes = [];
Expand Down
5 changes: 4 additions & 1 deletion src/Support/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function attributes(): array
}

/**
* @return array<string,array|string|FieldDefinition>
* @return array<string,array|string|FieldDefinition|Field>
*/
public function fields(): array
{
Expand Down Expand Up @@ -89,6 +89,9 @@ public function getFields(): array
$field = app($field);
$field->name = $name;
$allFields[$name] = $field->toArray();
} elseif ($field instanceof Field) {
$field->name = $name;
$allFields[$name] = $field->toArray();
} elseif ($field instanceof FieldDefinition) {
$allFields[$field->name] = $field;
} else {
Expand Down
65 changes: 65 additions & 0 deletions tests/Unit/InstantiableTypesTest/FormattableDate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Tests\Unit\InstantiableTypesTest;

use Carbon\Carbon;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Field;

class FormattableDate extends Field
{
protected $attributes = [
'description' => 'A field that can format dates in all sorts of ways.',
];

protected $defaultFormat;

public function __construct(array $settings = [], string $defaultFormat = 'Y-m-d H:i')
{
$this->attributes = \array_merge($this->attributes, $settings);

$this->defaultFormat = $defaultFormat;
}

public function args(): array
{
return [
'format' => [
'type' => Type::string(),
'defaultValue' => $this->defaultFormat,
'description' => \sprintf('Defaults to %s', $this->defaultFormat),
],
'relative' => [
'type' => Type::boolean(),
'defaultValue' => false,
],
];
}

public function type(): Type
{
return Type::string();
}

public function resolve($root, array $args): ?string
{
$date = $root->{$this->getProperty()};

if (! $date instanceof Carbon) {
return null;
}

if ($args['relative']) {
return $date->diffForHumans();
}

return $date->format($args['format']);
}

protected function getProperty(): string
{
return $this->attributes['alias'] ?? $this->attributes['name'];
}
}
56 changes: 56 additions & 0 deletions tests/Unit/InstantiableTypesTest/InstantiableTypesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Tests\Unit\InstantiableTypesTest;

use Carbon\Carbon;
use Rebing\GraphQL\Tests\TestCase;

class InstantiableTypesTest extends TestCase
{
public function testSomething(): void
{
$query = <<<'GRAQPHQL'
{
user {
default: dateOfBirth,
formattedDifferent: dateOfBirth(format: "Y-m-d"),
relative: dateOfBirth(relative: true),
alias: createdAt
}
}
GRAQPHQL;

$result = $this->graphql($query);

$dateOfBirth = Carbon::now()->addMonth()->startOfDay();
$createdAt = Carbon::now()->startOfDay();

$expectedResult = [
'data' => [
'user' => [
'default' => $dateOfBirth->format('Y-m-d H:i'),
'formattedDifferent' => $dateOfBirth->format('Y-m-d'),
'relative' => '4 weeks from now',
'alias' => $createdAt->format('Y-m-d H:i'),
],
],
];
$this->assertSame($expectedResult, $result);
}

protected function getEnvironmentSetUp($app)
{
parent::getEnvironmentSetUp($app);

$app['config']->set('graphql.schemas.default', [
'query' => [
'user' => UserQuery::class,
],
]);
$app['config']->set('graphql.types', [
'UserType' => UserType::class,
]);
}
}
36 changes: 36 additions & 0 deletions tests/Unit/InstantiableTypesTest/UserQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Tests\Unit\InstantiableTypesTest;

use Carbon\Carbon;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class UserQuery extends Query
{
protected $attributes = [
'name' => 'postMessages',
];

public function type(): Type
{
return GraphQL::type('UserType');
}

public function args(): array
{
return [];
}

public function resolve($root, $args)
{
return (object) [
'id' => 1,
'dateOfBirth' => Carbon::now()->addMonth()->startOfDay(),
'created_at' => Carbon::now()->startOfDay(),
];
}
}
Loading