Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeboot committed Nov 25, 2019
2 parents 29b00a8 + 7940255 commit f1001ac
Show file tree
Hide file tree
Showing 15 changed files with 868 additions and 22 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ matrix:
- php: '7.4snapshot'
env: LARAVEL='dev-master'
allow_failures:
- php: '7.2'
env: LARAVEL='dev-master'
- php: '7.3'
env: LARAVEL='dev-master'
- php: '7.4snapshot'
env: LARAVEL='5.8.*'
- php: '7.4snapshot'
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ CHANGELOG
--------------
### Added
- Allow passing through an instance of a `Field` [\#521 / georgeboot](https://github.com/rebing/graphql-laravel/pull/521/files)
- Add the ability to alias query and mutations arguments as well as input objects [\#517 / crissi](https://github.com/rebing/graphql-laravel/pull/517/files)
- Classes can now be injected in the Resolve method from the query/mutation similarly to Laravel controller methods [\#520 / crissi](https://github.com/rebing/graphql-laravel/pull/520/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)
### Changed
- Switch Code Style handling from StyleCI to PHP-CS Fixer [\#502 / crissi](https://github.com/rebing/graphql-laravel/pull/502)
- Implemented [ClientAware](https://webonyx.github.io/graphql-php/error-handling/#default-error-formatting) interface on integrated exceptions [\#530 / georgeboot](https://github.com/rebing/graphql-laravel/pull/530)
- More control over validation through optional user-generated validator by introducing `getValidator()` [\#531 / mailspice](https://github.com/rebing/graphql-laravel/pull/531)

2019-10-23, 3.1.0
-----------------
Expand Down
171 changes: 165 additions & 6 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ To work this around:
## Usage

- [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)
- [Note: these are the docs for the current release, please see the `v1` branch for the 1.* docs](#note-these-are-the-docs-for-the-current-release-please-see-the-v1-branch-for-the-1-docs)
- [Installation](#installation)
- [Dependencies:](#dependencies)
- [Installation:](#installation)
Expand All @@ -113,6 +113,7 @@ To work this around:
- [Creating a mutation](#creating-a-mutation)
- [Adding validation to a mutation](#adding-validation-to-a-mutation)
- [File uploads](#file-uploads)
- [Resolve method](#resolve-method)
- [Authorization](#authorization)
- [Privacy](#privacy)
- [Query Variables](#query-variables)
Expand All @@ -128,6 +129,7 @@ To work this around:
- [Interfaces](#interfaces)
- [Sharing Interface fields](#sharing-interface-fields)
- [Input Object](#input-object)
- [Input Alias](#input-alias)
- [JSON Columns](#json-columns)
- [Field deprecation](#field-deprecation)
- [Default Field Resolver](#default-field-resolver)
Expand Down Expand Up @@ -595,6 +597,70 @@ class UserProfilePhotoMutation extends Mutation

Note: You can test your file upload implementation using [Altair](https://altair.sirmuel.design/) as explained [here](https://sirmuel.design/working-with-file-uploads-using-altair-graphql-d2f86dc8261f).


### Resolve method
The resolve method is used in both queries and mutations and it here the response are created.

The first three parameters to the resolve method are hard-coded:
1. The `$root` object this resolve method belongs to (can be `null`)
2. The arguments passed as `array $args` (can be an empty array)
3. The query specific GraphQL context, can be customized by overriding `\Rebing\GraphQL\GraphQLController::queryContext`

Arguments here after will be attempted to be injected, similar to how controller methods works in Laravel.

You can typehint any class that you will need an instance of.

There are two hardcoded classes which depend on the local data for the query:
- `GraphQL\Type\Definition\ResolveInfo` has information useful for field resolution process.
- `Rebing\GraphQL\Support\SelectFields` allows eager loading of related models, see [Eager loading relationships](#eager-loading-relationships).

Example:
```php
<?php

namespace App\GraphQL\Queries;

use Closure;
use App\User;
use GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use Rebing\GraphQL\Support\SelectFields;
use Rebing\GraphQL\Support\Query;
use SomeClassNamespace\SomeClassThatDoLogging;

class UsersQuery extends Query
{
protected $attributes = [
'name' => 'User query'
];

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

public function args(): array
{
return [
'id' => ['name' => 'id', 'type' => Type::string()]
];
}

public function resolve($root, $args, $context, ResolveInfo $info, SelectFields $fields, SomeClassThatDoLogging $logging)
{
$logging->log('fetched user');

$select = $fields->getSelect();
$with = $fields->getRelations();

$users = User::select($select)->with($with);

return $users->get();
}
}
```

### Authorization

For authorization similar to Laravel's Request (or middleware) functionality, we can override the `authorize()` function in a Query or Mutation.
Expand Down Expand Up @@ -940,13 +1006,13 @@ class UserType extends GraphQLType

### Eager loading relationships

The fifth argument passed to a query's resolve method is a Closure which returns
an instance of `Rebing\GraphQL\Support\SelectFields` which you can use to retrieve keys
from the request. The following is an example of using this information
to eager load related Eloquent models.
The `Rebing\GraphQL\Support\SelectFields` class allows to eager load related Eloquent models.

This way only the required fields will be queried from the database.
Only the required fields will be queried from the database.

The class can be instanciated by typehinting `SelectFields $selectField` in your resolve method.

You can also construct the class by typehinting a `Closure`.
The Closure accepts an optional parameter for the depth of the query to analyse.

Your Query would look like:
Expand Down Expand Up @@ -1556,6 +1622,99 @@ class TestMutation extends GraphQLType {
}
```

### Input Alias

It is possible to alias query and mutation arguments as well as input object fields.

It can be especially useful for mutations saving data to the database.

Here you might want the input names to be different from the column names in the database.

Example, where the database columns are `first_name` and `last_name`:

```php
<?php

namespace App\GraphQL\InputObject;

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

class UserInput extends InputType
{
protected $attributes = [
'name' => 'UserInput',
'description' => 'A review with a comment and a score (0 to 5)'
];

public function fields(): array
{
return [
'firstName' => [
'alias' => 'first_name',
'description' => 'A comment (250 max chars)',
'type' => Type::string(),
'rules' => ['max:250']
],
'lastName' => [
'alias' => 'last_name',
'description' => 'A score (0 to 5)',
'type' => Type::int(),
'rules' => ['min:0', 'max:5']
]
];
}
}
```


```php
<?php

namespace App\GraphQL\Mutations;

use CLosure;
use App\User;
use GraphQL;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use Rebing\GraphQL\Support\Mutation;

class UpdateUserMutation extends Mutation
{
protected $attributes = [
'name' => 'UpdateUser'
];

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

public function args(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::string())
],
'input' => [
'type' => GraphQL::type('UserInput')
]
];
}

public function resolve($root, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
{
$user = User::find($args['id']);
$user->fill($args['input']));
$user->save();

return $user;
}
}
```


### JSON Columns

When using JSON columns in your database, the field won't be defined as a "relationship",
Expand Down
84 changes: 84 additions & 0 deletions src/Support/AliasArguments/AliasArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Support\AliasArguments;

use GraphQL\Type\Definition\InputObjectField;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;

class AliasArguments
{
private $typedArgs;
private $arguments;

public function get(array $typedArgs, array $arguments): array
{
$pathsWithAlias = $this->getAliasesInFields($typedArgs, '');

return (new ArrayKeyChange())->modify($arguments, $pathsWithAlias);
}

private function getAliasesInFields(array $fields, $prefix = '', $parentType = null): array
{
$pathAndAlias = [];
foreach ($fields as $name => $arg) {
// $arg is either an array DSL notation or an InputObjectField
$arg = $arg instanceof InputObjectField ? $arg : (object) $arg;

$type = $arg->type ?? null;

if (null === $type) {
continue;
}

$newPrefix = $prefix ? $prefix.'.'.$name : $name;

if (isset($arg->alias)) {
$pathAndAlias[$newPrefix] = $arg->alias;
}

if ($this->isWrappedInList($type)) {
$newPrefix .= '.*';
}

$type = $this->getWrappedType($type);

if (! ($type instanceof InputObjectType)) {
continue;
}

if ($parentType && $type->toString() === $parentType->toString()) {
// in case the field is a self reference we must not do
// a recursive call as it will never stop
continue;
}

$pathAndAlias = $pathAndAlias + $this->getAliasesInFields($type->getFields(), $newPrefix, $type);
}

return $pathAndAlias;
}

private function isWrappedInList(Type $type): bool
{
if ($type instanceof NonNull) {
$type = $type->getWrappedType();
}

return $type instanceof ListOfType;
}

private function getWrappedType(Type $type): Type
{
if ($type instanceof WrappingType) {
$type = $type->getWrappedType(true);
}

return $type;
}
}
62 changes: 62 additions & 0 deletions src/Support/AliasArguments/ArrayKeyChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Rebing\GraphQL\Support\AliasArguments;

class ArrayKeyChange
{
public function modify(array $array, array $pathKeyMappings): array
{
$pathKeyMappings = $this->orderPaths($pathKeyMappings);

foreach ($pathKeyMappings as $path => $replaceKey) {
$array = $this->changeKey($array, explode('.', $path), $replaceKey);
}

return $array;
}

/**
* @return array<string, string>
*/
private function orderPaths(array $paths): array
{
uksort($paths, function (string $a, string $b): int {
return $this->pathLevels($b) <=> $this->pathLevels($a);
});

return $paths;
}

private function pathLevels(string $path): int
{
return substr_count($path, '.');
}

private function changeKey(array $target, array $segments, string $replaceKey): array
{
$segment = array_shift($segments);

if (empty($segments)) {
if (isset($target[$segment])) {
$target[$replaceKey] = $target[$segment];
unset($target[$segment]);
}

return $target;
}

if ('*' === $segment) {
foreach ($target as $index => $inner) {
$target[$index] = $this->changeKey($inner, $segments, $replaceKey);
}

return $target;
}

$target[$segment] = $this->changeKey($target[$segment], $segments, $replaceKey);

return $target;
}
}
Loading

0 comments on commit f1001ac

Please sign in to comment.