convenia/graphql-laravel

Laravel的PHP GraphQL包装器

资助包维护!
mfn

8.6.0 2023-03-13 14:48 UTC

This package is auto-updated.

Last update: 2024-09-14 17:05:59 UTC


README

Latest Stable Version License Tests Downloads Get on Slack

使用Facebook的GraphQL在PHP 7.4+和Laravel 8.0+上。它基于GraphQL参考实现的PHP端口。你可以在GraphQL介绍中找到更多关于GraphQL的信息,该介绍在React博客上,或者你可以阅读GraphQL规范

  • 允许创建作为请求端点的查询突变
  • 支持多个模式
    • 每个模式查询/突变/类型
    • 每个模式HTTP中间件
    • 每个模式GraphQL执行中间件
  • 可以为每个查询/突变定义自定义GraphQL 解析器中间件

当使用SelectFields类支持Eloquent时,可提供更多功能

  • 查询返回类型,可以具有自定义的隐私设置。
  • 查询的字段将具有从数据库中动态检索的选项。

它提供了以下功能和改进,优于Folklore的原始包

  • 操作级授权
  • 按字段回调定义其可见性(例如,从未认证用户中隐藏)
  • SelectFields抽象在resolve()中可用,允许进行高级预加载,从而解决n+1问题
  • 分页支持
  • 服务器端支持查询批处理
  • 支持文件上传

安装

依赖关系

安装

使用Composer安装包

composer require rebing/graphql-laravel

Laravel

发布配置文件

php artisan vendor:publish --provider="Rebing\GraphQL\GraphQLServiceProvider"

审查配置文件

config/graphql.php

用法

概念

在深入编写代码之前,了解围绕GraphQL的概念是有好处的。如果你已经熟悉GraphQL,可以跳过这部分。

  • “模式”
    GraphQL模式定义了所有与之相关的查询、突变和类型。
  • “查询”和“突变”
    你在GraphQL请求中调用的“方法”(想想你的REST端点)
  • “类型”
    除了int和string等原始标量外,还可以定义和返回自定义的“形状”,通过自定义类型。它们可以映射到你的数据库模型或基本上你想返回的任何数据。
  • “解析器”
    每次返回数据时,它都会被“解析”。通常在查询/突变中,这指定了检索数据的主要方式(例如使用SelectFieldsdataloaders

通常,所有查询/突变/类型都是使用$attributes属性以及args() / fields()方法和resolve()方法定义的。

args/fields再次返回每个它们支持的字段的配置数组。这些字段通常支持这些形状

  • “键”是字段的名称
  • type(必需):这里支持的类型GraphQL指定符

可选键包括

  • description:在查询GraphQL模式时提供
  • resolve:覆盖默认字段解析器
  • deprecationReason:说明为什么某些内容已弃用

关于声明字段nonNull的说明

在输入和/或输出字段上广泛使用Type::nonNull()是相当常见且实际上是一种良好的实践。

你的类型系统意图越具体,对消费者就越好。

一些例子

  • 如果你需要查询/突变参数中的某个特定字段,则声明它为非null
  • 如果你知道你的(例如模型)字段永远不会返回null(例如用户ID、电子邮件等),则声明它为null
  • 如果你返回列表,如标签,它始终是一个数组(即使为空)并且不应包含null值,则声明类型如下
    Type::nonNull(Type::listOf(Type::nonNull(Type::string())))

GraphQL生态系统中有许多工具,它们的好处在于你的类型系统越具体。

数据加载

在GraphQL中,加载数据/检索数据的操作称为“解析”。GraphQL本身定义“如何”,并将其留给实现者。

在Laravel的上下文中,自然假设数据的主要来源将是Eloquent。因此,这个库提供了一个方便的助手SelectFields,它尽力预加载关系避免n+1问题

请注意,这不是唯一的方法,使用称为“dataloaders”的概念也很常见。它们通常利用解析字段“延迟”执行的优点,如graphql-php解决n+1问题中所述。

关键是,你可以在解析器中使用任何类型的数据源(Eloquent、静态数据、ElasticSearch结果、缓存等),但你必须注意执行模型以避免重复检索并智能地预取你的数据。

中间件概述

以下中间件概念被支持

  • HTTP中间件(即来自Laravel)
  • GraphQL执行中间件
  • GraphQL解析器中间件

简而言之,中间件通常是一个类

  • 具有一个handle方法
  • 接收一组固定的参数以及一个用于下一个中间件的调用
  • 负责调用“下一个”中间件
    通常中间件只做这件事,但可能决定不这么做,只是返回
  • 可以自由修改传递的参数

HTTP中间件

任何兼容Laravel的HTTP中间件都可以在全局级别通过配置graphql.route.middleware应用于所有GraphQL端点,也可以在特定模式的基础上通过graphql.schemas.<yourschema>.middleware应用。特定模式的中间件会覆盖全局中间件。

GraphQL执行中间件

GraphQL请求的处理,即所谓的“执行”,会通过一系列中间件。

它们可以通过graphql.execution_middleware在全局级别设置,也可以通过graphql.schemas.<yourschema>.execution_middleware在特定模式级别设置。

默认情况下,全局级别提供了推荐的中间件集。

注意:GraphQL请求的执行本身也是通过中间件实现的,通常期望最后调用(并且不会调用其他中间件)。如果您对此感兴趣,请参阅\Rebing\GraphQL\GraphQL::appendGraphqlExecutionMiddleware

GraphQL解析器中间件

在应用HTTP中间件和执行中间件后,针对查询/变异的目标执行“解析器中间件”,在调用实际的resolve()方法之前。

有关更多信息,请参阅解析器中间件

模式

Schema是定义GraphQL端点所必需的。您可以为多个schema定义不同的HTTP中间件和执行中间件,除了全局中间件。例如

'schema' => 'default',

'schemas' => [
    'default' => [
        'query' => [
            ExampleQuery::class,
        ],
        'mutation' => [
            ExampleMutation::class,
        ],
        'types' => [
        
        ],
    ],
    'user' => [
        'query' => [
            App\GraphQL\Queries\ProfileQuery::class
        ],
        'mutation' => [

        ],
        'types' => [
        
        ],
        'middleware' => ['auth'],
        // Which HTTP methods to support; must be given in UPPERCASE!
        'method' => ['GET', 'POST'], 
        'execution_middleware' => [
            \Rebing\GraphQL\Support\ExecutionMiddleware\UnusedVariablesMiddleware::class,
        ],
    ],
],

在某种程度上,schema通过其可访问的路由来定义配置。根据默认配置prefix = graphql,默认schema可通过/graphql访问。

模式类

您还可以在实现ConfigConvertible的类中定义schema的配置。

在您的配置中,您可以引用类的名称,而不是数组。

'schemas' => [
    'default' => DefaultSchema::class
]
namespace App\GraphQL\Schemas;

use Rebing\GraphQL\Support\Contracts\ConfigConvertible;

class DefaultSchema implements ConfigConvertible
{
    public function toConfig(): array
    {
        return [
            'query' => [
                ExampleQuery::class,
            ],
            'mutation' => [
                ExampleMutation::class,
            ],
            'types' => [
            
            ],
        ];
    }
}

您可以使用php artisan make:graphql:schemaConfig命令自动创建新的schema配置类。

创建查询

首先通常创建一个您想要从查询中返回的类型。如果指定关系,则Eloquent的'model'是必需的。

注意:如果是一个非数据库字段或不是关系,则必须提供selectable键。

namespace App\GraphQL\Types;

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',
        // Note: only necessary if you use `SelectFields`
        'model'         => User::class,
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the user',
                // Use 'alias', if the database column is different from the type name.
                // This is supported for discrete values as well as relations.
                // - you can also use `DB::raw()` to solve more complex issues
                // - or a callback returning the value (string or `DB::raw()` result)
                'alias' => 'user_id',
            ],
            'email' => [
                'type' => Type::string(),
                'description' => 'The email of user',
                'resolve' => function($root, array $args) {
                    // If you want to resolve the field yourself,
                    // it can be done here
                    return strtolower($root->email);
                }
            ],
            // Uses the 'getIsMeAttribute' function on our custom User model
            'isMe' => [
                'type' => Type::boolean(),
                'description' => 'True, if the queried user is the current user',
                'selectable' => false, // Does not try to query this from the database
            ]
        ];
    }

    // You can also resolve a field by declaring a method in the class
    // with the following format resolve[FIELD_NAME]Field()
    protected function resolveEmailField($root, array $args)
    {
        return strtolower($root->email);
    }
}

最佳实践是从config/graphql.php开始定义schema,并直接将类型添加到schema中(例如,default)。

'schemas' => [
    'default' => [
        // ...
        
        'types' => [
            App\GraphQL\Types\UserType::class,
        ],

或者

  • 在“全局”级别添加类型,例如直接在根配置中。

    'types' => [
        App\GraphQL\Types\UserType::class,
    ],

    在全局级别添加它们允许在不同schema之间共享它们,但请注意,这可能会使理解哪些类型/字段在哪里使用变得更加困难。

  • 或者使用GraphQL Facade,例如在一个服务提供者中添加类型。

    GraphQL::addType(\App\GraphQL\Types\UserType::class);

然后您需要定义一个返回此类型(或列表)的查询。您还可以指定在resolve方法中可以使用的参数。

namespace App\GraphQL\Queries;

use Closure;
use App\User;
use Rebing\GraphQL\Support\Facades\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Query;

class UsersQuery extends Query
{
    protected $attributes = [
        'name' => 'users',
    ];

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

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

    public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        if (isset($args['id'])) {
            return User::where('id' , $args['id'])->get();
        }

        if (isset($args['email'])) {
            return User::where('email', $args['email'])->get();
        }

        return User::all();
    }
}

将查询添加到config/graphql.php配置文件中

'schemas' => [
    'default' => [
        'query' => [
            App\GraphQL\Queries\UsersQuery::class
        ],
        // ...
    ]
]

到此为止。您应该能够通过向URL /graphql(或您在配置中选择的任何内容)发送请求来查询GraphQL。尝试以下query输入的GET请求

query FetchUsers {
    users {
        id
        email
    }
}

例如,如果您使用homestead

http://homestead.app/graphql?query=query+FetchUsers{users{id,email}}

创建突变

变异与任何其他查询类似。它接受参数并返回特定类型的对象。变异旨在用于修改(变异)服务器上的状态(而查询不应执行的操作)。

这是常规抽象,技术上您可以在查询解析器中做任何想做的事情,包括变异状态。

例如,一个更新用户密码的变异。首先您需要定义变异

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 UpdateUserPasswordMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserPassword'
    ];

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

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

    public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $user = User::find($args['id']);
        if(!$user) {
            return null;
        }

        $user->password = bcrypt($args['password']);
        $user->save();

        return $user;
    }
}

如您在resolve()方法中所见,您使用参数更新您的模型并返回它。

然后您应该将变异添加到config/graphql.php配置文件中

'schemas' => [
    'default' => [
        'mutation' => [
            App\GraphQL\Mutations\UpdateUserPasswordMutation::class,
        ],
        // ...
    ]
]

然后您可以使用以下查询在您的端点上执行变异

mutation users {
    updateUserPassword(id: "1", password: "newpassword") {
        id
        email
    }
}

如果您使用homestead

http://homestead.app/graphql?query=mutation+users{updateUserPassword(id: "1", password: "newpassword"){id,email}}

文件上传

本库使用https://github.com/laragraph/utils,该库符合在https://github.com/jaydenseric/graphql-multipart-request-spec中定义的规范。

您需要先将\Rebing\GraphQL\Support\UploadType添加到您的config/graphql模式类型定义中(可以是全局的,也可以是在您的模式中)

'types' => [
    \Rebing\GraphQL\Support\UploadType::class,
],

确保您发送的请求格式为multipart/form-data

警告:当您上传文件时,Laravel将使用FormRequest - 这意味着更改请求的中间件将不会产生任何效果。

namespace App\GraphQL\Mutations;

use Closure;
use GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;

class UserProfilePhotoMutation extends Mutation
{
    protected $attributes = [
        'name' => 'userProfilePhoto',
    ];

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

    public function args(): array
    {
        return [
            'profilePicture' => [
                'name' => 'profilePicture',
                'type' => GraphQL::type('Upload'),
                'rules' => ['required', 'image', 'max:1500'],
            ],
        ];
    }

    public function resolve($root, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $file = $args['profilePicture'];

        // Do something with file here...
    }
}

注意:您可以使用Altair测试您的文件上传实现,具体方法见此处

Vue.js和Axios示例
<template>
  <div class="input-group">
    <div class="custom-file">
      <input type="file" class="custom-file-input" id="uploadFile" ref="uploadFile" @change="handleUploadChange">
      <label class="custom-file-label" for="uploadFile">
        Drop Files Here to upload
      </label>
    </div>
    <div class="input-group-append">
      <button class="btn btn-outline-success" type="button" @click="upload">Upload</button>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'FileUploadExample',
    data() {
      return {
        file: null,
      };
    },
    methods: {
      handleUploadChange() {
        this.file = this.$refs.uploadFile.files[0];
      },
      async upload() {
        if (!this.file) {
          return;
        }
        // Creating form data object
        let bodyFormData = new FormData();
        bodyFormData.set('operations', JSON.stringify({
                   // Mutation string
            'query': `mutation uploadSingleFile($file: Upload!) {
                        upload_single_file  (attachment: $file)
                      }`,
            'variables': {"attachment": this.file}
        }));
        bodyFormData.set('operationName', null);
        bodyFormData.set('map', JSON.stringify({"file":["variables.file"]}));
        bodyFormData.append('file', this.file);

        // Post the request to GraphQL controller
        let res = await axios.post('/graphql', bodyFormData, {
          headers: {
            "Content-Type": "multipart/form-data"
          }
        });

        if (res.data.status.code == 200) {
          // On success file upload
          this.file = null;
        }
      }
    }
  }
</script>

<style scoped>
</style>
jQuery或原生javascript
<input type="file" id="fileUpload">
// Get the file from input element
// In jQuery:
let file = $('#fileUpload').prop('files')[0];
// Vanilla JS:
let file = document.getElementById("fileUpload").files[0];

// Create a FormData object
let bodyFormData = new FormData();
bodyFormData.set('operations', JSON.stringify({
         // Mutation string
  'query': `mutation uploadSingleFile($file: Upload!) {
              upload_single_file  (attachment: $file)
            }`,
  'variables': {"attachment": this.file}
}));
bodyFormData.set('operationName', null);
bodyFormData.set('map', JSON.stringify({"file":["variables.file"]}));
bodyFormData.append('file', this.file);

// Post the request to GraphQL controller via Axios, jQuery.ajax, or vanilla XMLHttpRequest
let res = await axios.post('/graphql', bodyFormData, {
  headers: {
    "Content-Type": "multipart/form-data"
  }
});

验证

Laravel的验证支持查询、突变、输入类型和字段参数。

注意:这种支持是“糖衣”上的,提供了一种便利。在某些情况下,它可能存在限制,在这种情况下,您可以在各自的resolve()方法中使用常规的Laravel验证,就像在常规的Laravel代码中一样。

支持以下方式添加验证规则:

  • 支持字段配置键'rules'
    • 在声明在function args()中的字段查询/突变中
    • 在声明在function fields()中的字段输入类型中
    • 为字段声明的'args'
  • 在任何查询/突变/输入类型上重写\Rebing\GraphQL\Support\Field::rules
  • 或直接在您的resolve()方法中直接使用Laravel的Validator

使用配置键'rules'非常方便,因为它与GraphQL类型的声明位置相同。然而,您可能会遇到某些限制(例如使用*的多字段验证),在这种情况下,您可以重写rules()方法。

在每个参数中定义规则的示例

class UpdateUserEmailMutation extends Mutation
{
    //...

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

    //...
}

使用rules()方法

namespace App\GraphQL\Mutations;

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

class UpdateUserEmailMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserEmail'
    ];

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

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

    protected function rules(array $args = []): array
    {
        return [
            'id' => ['required'],
            'email' => ['required', 'email'],
            'password' => $args['id'] !== 1337 ? ['required'] : [],
        ];
    }

    public function resolve($root, array $args)
    {
        $user = User::find($args['id']);
        if (!$user) {
            return null;
        }

        $user->email = $args['email'];
        $user->save();

        return $user;
    }
}

直接使用Laravel验证器

下面的例子中调用validate()将抛出Laravel的ValidationException,该异常由本库的默认error_formatter处理

protected function resolve($root, array $args) {
    \Illuminate\Support\Facades\Validator::make($args, [
        'data.*.password' => 'string|nullable|same:data.*.password_confirmation',
    ])->validate();
}

'rules'配置键的格式,或由rules()方法返回的规则,遵循Laravel支持的同一种约定,例如:

  • 'rules' => 'required|string
  • 'rules' => ['required', 'string']
  • 'rules' => function (…) { … }
    等等。

对于args()方法或字段的'args'定义,字段名称直接用于验证。然而,对于可以嵌套且多次出现的输入类型,字段名称映射为例如data.0.fieldname。这在从rules()方法返回规则时非常重要。

处理验证错误

异常用于在GraphQL响应中传达验证错误。当使用内置支持时,将抛出\Rebing\GraphQL\Error\ValidationError异常。在您的自定义代码或直接使用Laravel Validator时,也支持Laravel的内置\Illuminate\Validation\ValidationException。在这两种情况下,GraphQL响应将转换为以下显示的错误格式。

为了在GraphQL错误响应中支持返回验证错误,使用'extensions',因为没有合适的等效项。

在客户端,您可以通过检查给定错误的message是否匹配'validation'来检查,您可以期望extensions.validation键,它将每个字段映射到相应的错误。

{
  "data": {
    "updateUserEmail": null
  },
  "errors": [
    {
      "message": "validation",
      "extensions": {
        "validation": {
          "email": [
            "The email is invalid."
          ]
        }
      },
      "locations": [
        {
          "line": 1,
          "column": 20
        }
      ]
    }
  ]
}

您可以通过提供自己的error_formatter来自定义处理方式,在配置中替换默认的。

自定义错误消息

返回的验证错误可以通过重写validationErrorMessages方法来自定义。此方法应返回一个与Laravel验证文档中相同方式记录的自定义验证消息数组。例如,要检查email参数是否与现有数据冲突,可以执行以下操作:

注意:键应采用field_name.validator_type格式,这样就可以针对每个验证类型返回特定的错误。

public function validationErrorMessages(array $args = []): array
{
    return [
        'name.required' => 'Please enter your full name',
        'name.string' => 'Your name must be a valid string',
        'email.required' => 'Please enter your email address',
        'email.email' => 'Please enter a valid email address',
        'email.exists' => 'Sorry, this email address is already in use',
    ];
}

自定义属性

可以通过重写validationAttributes方法来自定义验证属性。此方法应返回一个与Laravel验证文档中相同方式记录的自定义属性数组。

public function validationAttributes(array $args = []): array
{
    return [
        'email' => 'email address',
    ];
}

其他注意事项

GraphQL的某些类型声明可能会取消我们的验证或使某些验证变得不必要。一个好的例子是使用Type::nonNull()最终声明一个参数是必需的。在这种情况下,'rules' => 'required'配置可能永远不会触发,因为GraphQL执行引擎已经阻止了首先接受此字段。

或者更明确地说:如果发生GraphQL类型系统违规,则不会执行任何Laravel验证,因为代码没有走到那一步。

解析方法

resolve方法在查询和突变中都使用,并且在这里创建响应。

resolve方法的第一个三个参数是硬编码的

  1. 这个resolve方法所属的$root对象(可以是null
  2. 作为array $args传递的参数(可以是一个空数组)
  3. 特定的查询GraphQL上下文,可以通过重写\Rebing\GraphQL\GraphQLController::queryContext来自定义

之后的参数将尝试注入,类似于Laravel中控制器方法的工作方式。

您可以注解任何您需要实例化的类。

有两个硬编码的类依赖于查询的本地数据

  • GraphQL\Type\Definition\ResolveInfo包含有用的信息,用于字段解析过程。
  • Rebing\GraphQL\Support\SelectFields允许预加载相关的Eloquent模型,请参阅预加载关系

示例

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' => 'users',
    ];

    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, array $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();
    }
}

解析器中间件

这些都是GraphQL特定解析中间件,与Laravel的“HTTP中间件”概念相关。主要区别在于

  • Laravel的HTTP中间件
    • 在模式/路由级别工作
    • 与任何常规Laravel HTTP中间件兼容
    • 对于模式中的所有查询/突变都是相同的
  • 解析器中间件
    • 概念上类似
    • 但在查询/突变级别应用,即对于每个查询/突变可能不同
    • 在技术上与HTTP中间件不兼容
    • 接受不同的参数

定义中间件

要创建一个新的中间件,请使用make:graphql:middleware Artisan命令

php artisan make:graphql:middleware ResolvePage

此命令将在您的app/GraphQL/Middleware目录中放置一个新的ResolvePage类。在这个中间件中,我们将设置分页器的当前页面为我们通过PaginationType接受的参数

namespace App\GraphQL\Middleware;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Pagination\Paginator;
use Rebing\GraphQL\Support\Middleware;

class ResolvePage extends Middleware
{
    public function handle($root, array $args, $context, ResolveInfo $info, Closure $next)
    {
        Paginator::currentPageResolver(function () use ($args) {
            return $args['pagination']['page'] ?? 1;
        });

        return $next($root, $args, $context, $info);
    }
}

注册中间件

如果您想将中间件分配给特定的查询/突变,请将中间件类列在查询类的$middleware属性中。

namespace App\GraphQL\Queries;

use App\GraphQL\Middleware;
use Rebing\GraphQL\Support\Query;
use Rebing\GraphQL\Support\Query;

class UsersQuery extends Query
{
    protected $middleware = [
        Middleware\Logstash::class,
        Middleware\ResolvePage::class,
    ];
}

如果您想使中间件在每次查询/突变时运行,请将中间件类列在基查询类的$middleware属性中。

namespace App\GraphQL\Queries;

use App\GraphQL\Middleware;
use Rebing\GraphQL\Support\Query as BaseQuery;

abstract class Query extends BaseQuery
{
    protected $middleware = [
        Middleware\Logstash::class,
        Middleware\ResolvePage::class,
    ];
}

或者,您可以覆盖getMiddleware来提供自己的逻辑

    protected function getMiddleware(): array
    {
        return array_merge([...], $this->middleware);
    }

可终止中间件

有时中间件可能需要在将响应发送到浏览器之后执行一些工作。如果您的中间件定义了terminate方法,并且您的Web服务器使用FastCGI,则terminate方法将在响应发送到浏览器后自动调用

namespace App\GraphQL\Middleware;

use Countable;
use GraphQL\Language\Printer;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\AbstractPaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Rebing\GraphQL\Support\Middleware;

class Logstash extends Middleware
{
    public function terminate($root, array $args, $context, ResolveInfo $info, $result): void
    {
        Log::channel('logstash')->info('', (
            collect([
                'query' => $info->fieldName,
                'operation' => $info->operation->name->value ?? null,
                'type' => $info->operation->operation,
                'fields' => array_keys(Arr::dot($info->getFieldSelection($depth = PHP_INT_MAX))),
                'schema' => Arr::first(Route::current()->parameters()) ?? Config::get('graphql.default_schema', 'default'),
                'vars' => $this->formatVariableDefinitions($info->operation->variableDefinitions),
            ])
                ->when($result instanceof Countable, function ($metadata) use ($result) {
                    return $metadata->put('count', $result->count());
                })
                ->when($result instanceof AbstractPaginator, function ($metadata) use ($result) {
                    return $metadata->put('per_page', $result->perPage());
                })
                ->when($result instanceof LengthAwarePaginator, function ($metadata) use ($result) {
                    return $metadata->put('total', $result->total());
                })
                ->merge($this->formatArguments($args))
                ->toArray()
        ));
    }

    private function formatArguments(array $args): array
    {
        return collect(Arr::sanitize($args))
            ->mapWithKeys(function ($value, $key) {
                return ["\${$key}" => $value];
            })
            ->toArray();
    }

    private function formatVariableDefinitions(?iterable $variableDefinitions = []): array
    {
        return collect($variableDefinitions)
            ->map(function ($def) {
                return Printer::doPrint($def);
            })
            ->toArray();
    }
}

terminate方法接收解析参数和查询结果。

一旦定义了一个可终止的中间件,应将其添加到查询和突变中中间件的列表。

授权

对于类似于Laravel的请求(或中间件)功能的授权,我们可以在查询或突变中重写authorize()函数。以下是一个Laravel的'auth'中间件的示例

namespace App\GraphQL\Queries;

use Auth;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;

class UsersQuery extends Query
{
    public function authorize($root, array $args, $ctx, ResolveInfo $resolveInfo = null, Closure $getSelectFields = null): bool
    {
        // true, if logged in
        return ! Auth::guest();
    }

    // ...
}

或者我们可以利用通过GraphQL查询传递的参数

namespace App\GraphQL\Queries;

use Auth;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;

class UsersQuery extends Query
{
    public function authorize($root, array $args, $ctx, ResolveInfo $resolveInfo = null, Closure $getSelectFields = null): bool
    {
        if (isset($args['id'])) {
            return Auth::id() == $args['id'];
        }

        return true;
    }

    // ...
}

当授权失败时,您也可以提供自定义的错误消息(默认为未授权)

namespace App\GraphQL\Queries;

use Auth;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;

class UsersQuery extends Query
{
    public function authorize($root, array $args, $ctx, ResolveInfo $resolveInfo = null, Closure $getSelectFields = null): bool
    {
        if (isset($args['id'])) {
            return Auth::id() == $args['id'];
        }

        return true;
    }

    public function getAuthorizationMessage(): string
    {
        return 'You are not authorized to perform this action';
    }

    // ...
}

隐私

注意:这仅适用于使用SelectFields类查询Eloquent模型时!

您可以为每个类型的字段设置自定义的隐私属性。如果不允许字段,则返回null。例如,如果您想用户的电子邮件只对自己可访问

class UserType extends GraphQLType
{
    // ...

    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',
                'privacy'       => function(array $args, $ctx): bool {
                    return $args['id'] == Auth::id();
                }
            ]
        ];
    }

    // ...

}

或者您可以创建一个扩展抽象GraphQL隐私类的类

use Auth;
use Rebing\GraphQL\Support\Privacy;

class MePrivacy extends Privacy
{
    public function validate(array $queryArgs, $queryContext = null): bool
    {
        return $queryArgs['id'] == Auth::id();
    }
}
use MePrivacy;

class UserType extends GraphQLType
{

    // ...

    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',
                'privacy'       => MePrivacy::class,
            ]
        ];
    }

    // ...

}

查询变量

GraphQL允许您在查询中使用变量,因此您不需要“硬编码”值。这样做的方式如下

query FetchUserByID($id: String)
{
    user(id: $id) {
        id
        email
    }
}

当您查询GraphQL端点时,您可以传递一个JSON编码的variables参数。

http://homestead.app/graphql?query=query+FetchUserByID($id:Int){user(id:$id){id,email}}&variables={"id":123}

自定义字段

如果您想在一个类型中多次使用字段,也可以将其定义为类。

namespace App\GraphQL\Fields;

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

class PictureField extends Field
{
    protected $attributes = [
        'description'   => 'A picture',
    ];

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

    public function args(): array
    {
        return [
            'width' => [
                'type' => Type::int(),
                'description' => 'The width of the picture'
            ],
            'height' => [
                'type' => Type::int(),
                'description' => 'The height of the picture'
            ]
        ];
    }

    protected function resolve($root, array $args)
    {
        $width = isset($args['width']) ? $args['width']:100;
        $height = isset($args['height']) ? $args['height']:100;

        return 'http://placehold.it/'.$width.'x'.$height;
    }
}

然后您可以在类型声明中使用它

namespace App\GraphQL\Types;

use App\GraphQL\Fields\PictureField;
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'
            ],
            //Instead of passing an array, you pass a class path to your custom field
            'picture' => PictureField::class
        ];
    }
}

更好的可重用字段

除了使用类名外,您还可以提供一个实际的Field实例。这不仅允许您重复使用字段,还可以打开重复使用解析器的可能性。

让我们想象我们想要一个可以以各种方式输出日期的字段类型。

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, 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'];
    }
}

您可以在类型中如下使用此字段

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',
            ])
        ];
    }
}

预加载关系

Rebing\GraphQL\Support\SelectFields类允许懒加载相关Eloquent模型。只有所需的字段将从数据库中查询。

您可以通过在解析方法中类型提示SelectFields $selectField来实例化该类。

您还可以通过类型提示一个Closure来构建类。闭包接受一个可选参数,用于分析查询的深度。

您的查询可能如下所示

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;

class UsersQuery extends Query
{
    protected $attributes = [
        'name' => 'users',
    ];

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

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

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        /** @var SelectFields $fields */
        $fields = $getSelectFields();
        $select = $fields->getSelect();
        $with = $fields->getRelations();

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

        return $users->get();
    }
}

您的用户类型可能如下所示。必须存在UserModel中的profileposts关系。如果某些字段对于关系加载或验证等是必需的,则可以定义一个always属性,该属性将给定的属性添加到选择中。

该属性可以是逗号分隔的字符串或包含始终包含的属性的数组。

namespace App\GraphQL\Types;

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

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

    /**
    * @return array
    */
    public function fields(): array
    {
        return [
            'uuid' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The uuid of the user'
            ],
            'email' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The email of user'
            ],
            'profile' => [
                'type' => GraphQL::type('Profile'),
                'description' => 'The user profile',
            ],
            'posts' => [
                'type' => Type::listOf(GraphQL::type('Post')),
                'description' => 'The user posts',
                // Can also be defined as a string
                'always' => ['title', 'body'],
            ]
        ];
    }
}

在此阶段,我们有一个符合预期的配置文件和帖子类型

class ProfileType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'Profile',
        'description'   => 'A user profile',
        'model'         => UserProfileModel::class,
    ];

    public function fields(): array
    {
        return [
            'name' => [
                'type' => Type::string(),
                'description' => 'The name of user'
            ]
        ];
    }
}
class PostType extends GraphQLType
{
    protected $attributes = [
        'name'          => 'Post',
        'description'   => 'A post',
        'model'         => PostModel::class,
    ];

    public function fields(): array
    {
        return [
            'title' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The title of the post'
            ],
            'body' => [
                'type' => Type::string(),
                'description' => 'The body the post'
            ]
        ];
    }
}

类型关系查询

注意:这仅适用于使用SelectFields类查询Eloquent模型时!

您还可以指定通过Eloquent的查询构造器包含在关系中的查询。

class UserType extends GraphQLType
{

    // ...

    public function fields(): array
    {
        return [
            // ...

            // Relation
            'posts' => [
                'type'          => Type::listOf(GraphQL::type('Post')),
                'description'   => 'A list of posts written by the user',
                'args'          => [
                    'date_from' => [
                        'type' => Type::string(),
                    ],
                 ],
                // $args are the local arguments passed to the relation
                // $query is the relation builder object
                // $ctx is the GraphQL context (can be customized by overriding `\Rebing\GraphQL\GraphQLController::queryContext`
                // The return value should be the query builder or void
                'query'         => function (array $args, $query, $ctx): void {
                    $query->addSelect('some_column')
                          ->where('posts.created_at', '>', $args['date_from']);
                }
            ]
        ];
    }
}

分页

如果查询或突变返回PaginationType,则将使用分页。

请注意,除非您使用解析器中间件,否则您必须手动提供限制和页面值。

namespace App\GraphQL\Queries;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class PostsQuery extends Query
{
    public function type(): Type
    {
        return GraphQL::paginate('posts');
    }

    // ...

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        $fields = $getSelectFields();

        return Post::with($fields->getRelations())
            ->select($fields->getSelect())
            ->paginate($args['limit'], ['*'], 'page', $args['page']);
    }
}

查询posts(limit:10,page:1){data{id},total,per_page}可能返回

{
    "data": {
        "posts: [
            "data": [
                {"id": 3},
                {"id": 5},
                ...
            ],
            "total": 21,
            "per_page": 10
        ]
    }
}

请注意,当请求分页资源时,需要添加额外的'data'对象,因为返回的数据将分页资源作为数据对象返回,与返回的分页元数据在同一级别。

如果查询或突变返回SimplePaginationType,将使用简单分页

namespace App\GraphQL\Queries;

use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;

class PostsQuery extends Query
{
    public function type(): Type
    {
        return Type::nonNull(GraphQL::simplePaginate('posts'));
    }

    // ...

    public function resolve($root, array $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        $fields = $getSelectFields();

        return Post::with($fields->getRelations())
            ->select($fields->getSelect())
            ->simplePaginate($args['limit'], ['*'], 'page', $args['page']);
    }
}

批处理

批量请求必须通过POST请求发送。

您可以通过将它们分组在一起一次性发送多个查询(或突变)。

POST
{
    query: "query postsQuery { posts { id, comment, author_id } }"
}

POST
{
    query: "mutation storePostMutation($comment: String!) { store_post(comment: $comment) { id } }",
    variables: { "comment": "Hi there!" }
}

因此,您不需要创建两个HTTP请求,可以将它们批处理为一个

POST
[
    {
        query: "query postsQuery { posts { id, comment, author_id } }"
    },
    {
        query: "mutation storePostMutation($comment: String!) { store_post(comment: $comment) { id } }",
        variables: { "comment": "Hi there!" }
    }
]

对于同时发送多个请求的系统,这可以通过将将在一定时间间隔内执行的查询分组在一起来提高性能。

有工具可以帮助您完成此操作,例如Apollo

关于查询批次的说明:虽然看起来这似乎是一个“只赚不赔”的情况,但使用批处理可能存在一些潜在的风险。

  • 所有查询/突变都在相同的“进程执行上下文”中执行。
    如果您的代码有副作用,而这些副作用在通常的FastCGI环境(单一请求/响应)中可能不会显示,那么这可能会导致问题。

  • “HTTP中间件”只在整个批次中执行一次。
    如果您预计它会在每个查询/突变中触发。这对于日志记录或速率限制可能特别相关。
    另一方面,“解析器中间件”将按预期工作(尽管它解决的是不同的问题)。

  • 查询/突变数量没有限制。
    目前还没有限制它的方法。

可以通过将配置 batching.enable 设置为 false 来禁用批处理支持。

标量类型

GraphQL附带内置的标量类型,如字符串、整数、布尔值等。您可以为特殊用途字段创建自定义标量类型。

例如,链接:您可以使用标量类型 Link 而不是 Type::string(),并使用 GraphQL::type('Link') 引用它。

好处包括

  • 一个专属的描述,这样您就可以给字段赋予比仅称其为字符串类型更多的意义/目的
  • 显式转换逻辑,用于以下步骤
    • 将内部逻辑转换为序列化的GraphQL输出(《序列化》)
    • 查询/字段输入参数转换(《解析文字》)
    • 当作为变量传递到您的查询中时(《解析值》)

这也意味着可以在这些方法中添加验证逻辑,以确保传递/接收的值例如是一个真正的链接。

标量类型必须实现所有方法;您可以使用 artisan make:graphql:scalar <typename> 快速启动,然后将标量添加到您的现有类型中。

对于更高级的使用,请参阅有关标量类型的官方文档此处

关于性能的说明:请注意您在标量类型方法中包含的代码。如果您返回大量字段,并使用自定义标量以及包含验证字段的复杂逻辑,可能会影响响应时间。

枚举

枚举类型是一种特殊类型的标量,其值被限制为特定的允许值集合。有关枚举的更多信息,请参阅此处

首先创建一个作为GraphQLType类的扩展的枚举。

namespace App\GraphQL\Enums;

use Rebing\GraphQL\Support\EnumType;

class EpisodeEnum extends EnumType
{
    protected $attributes = [
        'name' => 'episode',
        'description' => 'The types of demographic elements',
        'values' => [
            'NEWHOPE' => 'NEWHOPE',
            'EMPIRE' => 'EMPIRE',
            'JEDI' => 'JEDI',
        ],
    ];
}

注意:$attributes['values'] 数组中,键是枚举值,GraphQL客户端可以选择,而值是服务器将接收的(枚举将解析为)。

枚举将在 config/graphq.php 中像其他类型一样注册。

'schemas' => [
    'default' => [
        'types' => [
            EpisodeEnum::class,
        ],

然后可以这样使用它:

namespace App\GraphQL\Types;

use Rebing\GraphQL\Support\Type as GraphQLType;

class TestType extends GraphQLType
{
    public function fields(): array
    {
        return [
            'episode_type' => [
                'type' => GraphQL::type('EpisodeEnum')
            ]
        ];
    }
}

联合

Union是一种抽象类型,它简单地列举了其他Object类型。Union类型的值实际上是包含Object类型中的一个类型的值。

如果您需要在同一查询中返回不相关的类型,这很有用。例如,当实现对多个不同实体的搜索时。

定义UnionType的示例

namespace App\GraphQL\Unions;

use App\Post;
use GraphQL;
use Rebing\GraphQL\Support\UnionType;

class SearchResultUnion extends UnionType
{
    protected $attributes = [
        'name' => 'searchResult',
    ];

    public function types(): array
    {
        return [
            GraphQL::type('Post'),
            GraphQL::type('Episode'),
        ];
    }

    public function resolveType($value)
    {
        if ($value instanceof Post) {
            return GraphQL::type('Post');
        } elseif ($value instanceof Episode) {
            return GraphQL::type('Episode');
        }
    }
}

接口

您可以使用接口来抽象一组字段。有关接口的更多信息,请参阅此处

接口的实现

namespace App\GraphQL\Interfaces;

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

class CharacterInterface extends InterfaceType
{
    protected $attributes = [
        'name' => 'character',
        'description' => 'Character interface.',
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The id of the character.'
            ],
            'name' => Type::string(),
            'appearsIn' => [
                'type' => Type::nonNull(Type::listOf(GraphQL::type('Episode'))),
                'description' => 'A list of episodes in which the character has an appearance.'
            ],
        ];
    }

    public function resolveType($root)
    {
        // Use the resolveType to resolve the Type which is implemented trough this interface
        $type = $root['type'];
        if ($type === 'human') {
            return GraphQL::type('Human');
        } elseif  ($type === 'droid') {
            return GraphQL::type('Droid');
        }
    }
}

实现接口的类型

namespace App\GraphQL\Types;

use GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
use GraphQL\Type\Definition\Type;

class HumanType extends GraphQLType
{
    protected $attributes = [
        'name' => 'human',
        'description' => 'A human.'
    ];

    public function fields(): array
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The id of the human.',
            ],
            'name' => Type::string(),
            'appearsIn' => [
                'type' => Type::nonNull(Type::listOf(GraphQL::type('Episode'))),
                'description' => 'A list of episodes in which the human has an appearance.'
            ],
            'totalCredits' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The total amount of credits this human owns.'
            ]
        ];
    }

    public function interfaces(): array
    {
        return [
            GraphQL::type('Character')
        ];
    }
}

支持在接口关系上自定义查询

如果一个接口包含与自定义查询的关系,则必须实现 public function types() 返回一个数组,其中包含 GraphQL::type(),即它可能解析到的所有可能的类型(与Union的工作方式非常相似)以便它能够与 SelectFields 正确工作。

基于前面的代码示例,该方法看起来像这样:

    public function types(): array
    {
        return[
            GraphQL::type('Human'),
            GraphQL::type('Droid'),
        ];
    }

共享接口字段

由于您经常需要在具体类型中重复许多接口的字段定义,因此共享接口的定义是有意义的。您可以使用getField(string fieldName): FieldDefinition方法访问和重用特定的接口字段。要获取所有字段作为数组,请使用getFields(): array

有了这个,您可以像这样编写您的HumanType类的fields方法

public function fields(): array
{
    $interface = GraphQL::type('Character');

    return [
        $interface->getField('id'),
        $interface->getField('name'),
        $interface->getField('appearsIn'),

        'totalCredits' => [
            'type' => Type::nonNull(Type::int()),
            'description' => 'The total amount of credits this human owns.'
        ]
    ];
}

或者通过使用getFields方法

public function fields(): array
{
    $interface = GraphQL::type('Character');

    return array_merge($interface->getFields(), [
        'totalCredits' => [
            'type' => Type::nonNull(Type::int()),
            'description' => 'The total amount of credits this human owns.'
        ]
    ]);
}

输入对象

输入对象类型允许您创建复杂输入。字段没有参数或解析选项,并且其类型必须是InputType。如果需要验证输入数据,可以添加规则选项。有关输入对象的更多信息,请参阅这里

首先创建一个作为GraphQLType类的扩展的InputObjectType

namespace App\GraphQL\InputObject;

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

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

    public function fields(): array
    {
        return [
            'comment' => [
                'name' => 'comment',
                'description' => 'A comment (250 max chars)',
                'type' => Type::string(),
                // You can define Laravel Validation here
                'rules' => ['max:250']
            ],
            'score' => [
                'name' => 'score',
                'description' => 'A score (0 to 5)',
                'type' => Type::int(),
                // You must use 'integer' on rules if you want to validate if the number is inside a range
                // Otherwise it will validate the number of 'characters' the number can have.
                'rules' => ['integer', 'min:0', 'max:5']
            ]
        ];
    }
}

输入对象将像您的模式中的任何其他类型一样注册,在config/graphq.php

'schemas' => [
    'default' => [
        'types' => [
            'ReviewInput' => ReviewInput::class
        ],

然后在突变中使用它,例如

// app/GraphQL/Type/TestMutation.php
class TestMutation extends GraphQLType {

    public function args(): array
    {
        return [
            'review' => [
                'type' => GraphQL::type('ReviewInput')
            ]
        ]
    }

}

类型修饰符

可以通过将选择类型包裹在Type::nonNullType::listOf调用中来应用类型修饰符,或者您还可以使用通过GraphQL::type提供的简写语法来构建更复杂的类型。

GraphQL::type('MyInput!');
GraphQL::type('[MyInput]');
GraphQL::type('[MyInput]!');
GraphQL::type('[MyInput!]!');

GraphQL::type('String!');
GraphQL::type('[String]');
GraphQL::type('[String]!');
GraphQL::type('[String!]!');

字段和输入别名

可以别名为查询、突变参数以及输入对象字段。

对于将数据保存到数据库的突变,这特别有用。

在这种情况下,您可能希望输入名称与数据库中的列名不同。

例如,数据库列是first_namelast_name

namespace App\GraphQL\InputObject;

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

class UserInput extends InputType
{
    protected $attributes = [
        'name' => 'userInput',
        'description' => 'A user.'
    ];

    public function fields(): array
    {
        return [
            'firstName' => [
                'alias' => 'first_name',
                'description' => 'The first name of the user',
                'type' => Type::string(),
                'rules' => ['max:30']
            ],
            'lastName' => [
                'alias' => 'last_name',
                'description' => 'The last name of the user',
                'type' => Type::string(),
                'rules' => ['max:30']
            ]
        ];
    }
}
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, array $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $user = User::find($args['id']);
        $user->fill($args['input']));
        $user->save();

        return $user;
    }
}

JSON列

当在数据库中使用JSON列时,字段不会定义为“关系”,而是一个带有嵌套数据的简单列。要获取不是数据库关系的嵌套对象,请在使用Type时使用is_relation属性

class UserType extends GraphQLType
{
    // ...

    public function fields(): array
    {
        return [
            // ...

            // JSON column containing all posts made by this user
            'posts' => [
                'type'          => Type::listOf(GraphQL::type('Post')),
                'description'   => 'A list of posts written by the user',
                // Now this will simply request the "posts" column, and it won't
                // query for all the underlying columns in the "post" object
                // The value defaults to true
                'is_relation' => false
            ]
        ];
    }

    // ...
}

字段弃用

有时您可能想要弃用一个字段,但仍需维护向后兼容性,直到客户端完全停止使用该字段。您可以使用指令来弃用一个字段。如果您将deprecationReason添加到字段属性中,它将在GraphQL文档中标记为弃用。您可以使用Apollo Engine在客户端验证模式。

namespace App\GraphQL\Types;

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',
            ],
            'address' => [
                'type' => Type::string(),
                'description' => 'The address of user',
                'deprecationReason' => 'Deprecated due to address field split'
            ],
            'address_line_1' => [
                'type' => Type::string(),
                'description' => 'The address line 1 of user',
            ],
            'address_line_2' => [
                'type' => Type::string(),
                'description' => 'The address line 2 of user',
            ],
        ];
    }
}

默认字段解析器

可以使用defaultFieldResolver配置选项覆盖由底层webonyx/graphql-php库提供的默认字段解析器。

您可以为它定义任何有效的可调用对象(静态类方法、闭包等)

'defaultFieldResolver' => [Your\Klass::class, 'staticMethod'],

接收到的参数是您的常规“解析”函数签名。

如果您想要定义一些可以在各种查询、突变和类型中重复使用的辅助函数,您可以使用GraphQL外观上的宏方法。

例如,从服务提供者的boot方法中

namespace App\Providers;

use GraphQL\Type\Definition\Type;
use Illuminate\Support\ServiceProvider;
use Rebing\GraphQL\Support\Facades\GraphQL;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        GraphQL::macro('listOf', function (string $name): Type {
            return Type::listOf(GraphQL::type($name));
        });
    }
}

macro函数接受一个名称作为其第一个参数,一个Closure作为其第二个参数。

自动持久查询支持

自动持久化查询(APQ)通过发送更小的请求并无需构建时配置来提高网络性能。

APQ默认禁用,可以通过配置中的apq.enabled=true启用,或者通过设置环境变量GRAPHQL_APQ_ENABLE=true

持久化查询是一个ID或哈希,可以在客户端生成并发送到服务器,而不是整个GraphQL查询字符串。这个较小的签名减少了带宽利用率并加快了客户端加载时间。持久化查询与GET请求配对特别有用,可以启用浏览器缓存并集成CDN。

在幕后,APQ使用Laravel的缓存来存储/检索查询。在存储之前,它们会被GraphQL解析,因此不需要再次解析。请查看各种选项,了解要使用哪种缓存、前缀、TTL等。

注意:建议在部署后清除缓存,以适应您的模式变化!

更多信息请见

注意:APQ协议要求客户端发送的哈希与服务器上计算出的哈希进行比较。如果存在像TrimStrings这样的修改中间件,并且发送的查询包含前导或尾随空格,这些哈希永远不会匹配,从而导致错误。

在这种情况下,要么禁用中间件,要么在客户端在哈希之前修剪查询。

说明

客户端示例

以下是一个简单的Vue/Apollo集成示例,createPersistedQueryLink会自动管理APQ流程。

// [example app.js]

require('./bootstrap');

window.Vue = require('vue');

Vue.component('example-component', require('./components/ExampleComponent.vue').default);

import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { createHttpLink } from 'apollo-link-http';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { InMemoryCache } from 'apollo-cache-inmemory';
import VueApollo from 'vue-apollo';

const httpLinkWithPersistedQuery = createPersistedQueryLink().concat(createHttpLink({
    uri: '/graphql',
}));

// Create the apollo client
const apolloClient = new ApolloClient({
    link: ApolloLink.from([httpLinkWithPersistedQuery]),
    cache: new InMemoryCache(),
    connectToDevTools: true,
})

const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
});

Vue.use(VueApollo);

const app = new Vue({
    el: '#app',
    apolloProvider,
});
<!-- [example TestComponent.vue] -->

<template>
    <div>
        <p>Test APQ</p>
        <p>-> <span v-if="$apollo.queries.hello.loading">Loading...</span>{{ hello }}</p>
    </div>
</template>

<script>
    import gql from 'graphql-tag';
    export default {
        apollo: {
            hello: gql`query{hello}`,
        },
        mounted() {
            console.log('Component mounted.')
        }
    }
</script>

其他功能

检测未使用的变量

默认情况下,与GraphQL查询一起提供的且未使用的'variables'会被静默忽略。

如果您考虑这样一个假设情况:您的查询中有一个可选(可空)参数,您为其提供了一个变量参数,但您输入了拼写错误,这可能不会被注意到。

示例

mutation test($value:ID) {
  someMutation(type:"falbala", optional_id: $value)
}

提供的变量

{
  // Ops! typo in `values`
  "values": "138"
}

在这种情况下,不会发生任何事情,并且optional_id将被视为未提供。

为了防止此类情况,您可以将UnusedVariablesMiddleware添加到您的execution_middleware中。

配置选项

  • 路由
    包含路由组的所有配置。每个模式将通过其名称作为专用路由提供。
    • 前缀
      GraphQL端点的路由前缀,不包括前导/
      默认情况下,API通过/graphql提供。
    • 控制器
      允许覆盖默认控制器类,如果您想扩展或替换现有类(也支持array格式)。
    • 中间件
      在未提供特定模式中间件的情况下,应用全局GraphQL中间件。
    • group_attributes
      额外的路由组属性
  • default_schema
    默认方案的名称,当通过路由未提供任何方案时使用。
  • batching\
    • 'enable'
      是否支持GraphQL批处理。
  • error_formatter
    此可调用函数将传递每个错误对象给GraphQL捕获的错误。该方法应返回表示错误的数组。
  • errors_handler
    自定义错误处理。默认处理程序将异常传递给Laravel错误处理机制。
  • security
    各种选项以限制查询复杂性和深度,请参阅https://webonyx.github.io/graphql-php/security/文档。
    • query_max_complexity
    • query_max_depth
    • disable_introspection
  • pagination_type
    您可以定义自己的分页类型。
  • simple_pagination_type
    您可以定义自己的简单分页类型。
  • defaultFieldResolver
    覆盖默认字段解析器,请参阅http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver
  • headers
    任何要添加到默认控制器返回的响应中的头信息。
  • json_encoding_options
    从默认控制器返回响应时使用的任何JSON编码选项。
  • apq
    自动持久化查询(APQ)
    • enable
      默认情况下是禁用的。
    • cache_driver
      要使用的缓存驱动程序。
    • cache_prefix
      要使用的缓存前缀。
    • cache_ttl
      缓存查询的时间。
  • detect_unused_variables
    如果启用,查询未使用的变量将引发错误。

指南

从v1升级到v2

虽然版本2基于相同的代码库,并且没有从根本上改变库本身的工作方式,但许多方面得到了改进,有时会导致不兼容的更改。

  • 步骤 0:进行备份!
  • 重新发布配置文件以了解所有新设置
  • 解析器(resolvers)的顺序和参数/类型已更改
    • 之前:resolve($root, $array, SelectFields $selectFields, ResolveInfo $info)
    • 之后:resolve($root, $array, $context, ResolveInfo $info, Closure $getSelectFields)
    • 如果您现在想使用 SelectFields,您必须首先请求它:$selectFields = $getSelectFields();。这样做的主要原因是为了性能。SelectFields 是一个可选功能,但需要资源来遍历 GraphQL 请求 AST 并内省所有类型以应用其魔法。在过去,它总是被构建,因此消耗了资源,即使没有请求。这已被更改为显式形式。
  • 许多方法签名声明已更改以提高类型安全,这需要进行适配
    • 方法字段签名已更改
      • public function fields()
      • public function fields(): array
    • 方法 toType 的签名已更改
      • public function toType()
      • public function toType(): \GraphQL\Type\Definition\Type
    • 方法 getFields 的签名已更改
      • public function getFields()
      • public function getFields(): array
    • 方法 interfaces 的签名已更改
      • public function interfaces()
      • public function interfaces(): array
    • 方法 types 的签名已更改
      • public function types()
      • public function types(): array
    • 方法 type 的签名已更改
      • public function type()
      • public function type(): \GraphQL\Type\Definition\Type
    • 方法 args 的签名已更改
      • public function args()
      • public function args(): array
    • 方法 queryContext 的签名已更改
      • protected function queryContext($query, $variables, $schema)
      • protected function queryContext()
    • 控制器方法 query 的签名已更改
      • function query($query, $variables = [], $opts = [])
      • function query(string $query, ?array $variables = [], array $opts = []): array
    • 如果您正在使用自定义 Scalar 类型
      • 方法 parseLiteral 的签名已更改(由于 webonyx 库的升级)
        • public function parseLiteral($ast)
        • public function parseLiteral($valueNode, ?array $variables = null)
  • 如果您想使用 UploadType,现在必须手动将其添加到您的模式中的 types 中。::getInstance() 方法已消失,您可以像引用其他类型一样简单引用它:GraphQL::type('Upload')
  • 遵循 Laravel 惯例,使用复数形式命名空间(例如,新的查询现在放置在 App\GraphQL\Queries 中,而不是 App\GraphQL\Query);相应的 make 命令已调整。这不会破坏任何现有代码,但生成的代码将使用新模式。
  • 请务必阅读变更日志以获取更多详细信息

从Folklore迁移

https://github.com/folkloreinc/laravel-graphql,以前也称为 https://github.com/Folkloreatelier/laravel-graphql

这两个代码库非常相似,并且根据您的自定义程度,迁移可能非常快速。

注意:此迁移是为本库的版本 2.* 编写的。

以下列表不是万无一失的列表,但应作为指南。如果您不需要执行某些步骤,则不是错误。

在继续之前进行备份!

  • composer remove folklore/graphql
  • 如果您有一个自定义 ServiceProvider 或手动包含它,请将其删除。目的是不要触发现有 GraphQL 代码的运行。
  • 使用 composer 安装 rebing/graphql-laravel
  • 发布 config/graphql.php 并对其进行适配(前缀、中间件、模式、类型、突变、查询、安全设置)
    • 移除的设置
      • 域名
      • 解析器
    • 将默认模式(schema)重命名为 default_schema
    • middleware_schema 不存在了,现在在 schema.<name>.middleware 中定义
  • 更改命名空间引用
    • Folklore\
    • Rebing\
  • 查看从 v1 升级到 v2 的升级指南,其中包含所有函数签名更改
  • 特例 ShouldValidate 不再存在;提供的功能已集成到 Field
  • 查询/突变的解析方法的第一参数现在是 null(之前默认为空数组)

性能考虑

包装类型

你可以包装类型以向查询和突变添加更多信息。就像分页一样,你也可以对你的额外数据进行相同的操作,以注入你想要的数据(查看测试示例)。例如,在你的查询中

public function type(): Type
{
    return GraphQL::wrapType(
        'PostType',
        'PostMessageType',
        \App\GraphQL\Types\WrapMessagesType::class,
    );
}

public function resolve($root, array $args)
{
    return [
        'data' => Post::find($args['post_id']),
        'messages' => new Collection([
                new SimpleMessage("Congratulations, the post was found"),
                new SimpleMessage("This post cannot be edited", "warning"),
        ]),
    ];
}

GraphQL测试客户端