rebing/graphql-laravel

Laravel 的 PHP GraphQL 包装器

资助包维护!
mfn

9.6.0 2024-08-23 07:42 UTC

README

Latest Stable Version License Tests Downloads Get on Slack

使用 Facebook 的 GraphQL 在 PHP 8.1+ 和 Laravel 10.0+ 上。它基于 GraphQL 参考实现的 PHP 版本。您可以在 React 博客的 GraphQL 介绍 中找到有关 GraphQL 的更多信息,或者您也可以阅读 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,请随意跳过这部分。

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

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

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

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

可选键包括

  • description:当检查GraphQL模式时可用
  • resolve:覆盖默认字段解析器
  • deprecationReason:记录为什么某些内容已弃用

关于声明字段 nonNull 的说明

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

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

一些例子

  • 如果您需要查询/突变参数中的某个字段,请声明它非空
  • 如果您知道您的(例如,模型)字段永远不会返回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端点提供,通过配置graphql.route.middleware,或者通过graphql.schemas.<yourschema>.middleware在特定模式的基础上。特定模式的中间件会覆盖全局中间件。

GraphQL 执行中间件

GraphQL请求的处理,以下简称“执行”,通过一系列中间件。

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

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

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

GraphQL 解析器中间件

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

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

模式

需要模式来定义GraphQL端点。您可以定义多个模式,并将不同的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,
        ],
    ],
],

与配置一样,模式也以某种方式定义了它可访问的路线。按照默认配置prefix = graphql,默认模式可以通过/graphql访问。

模式类

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

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

'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命令自动创建新的模式配置类。

创建查询

通常首先创建您想要从查询返回的类型。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中以模式开始,并将类型直接添加到模式中(例如default)。

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

或者

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

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

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

  • 或者使用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}}

创建突变

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

这是传统的抽象,技术上您可以在查询解析中做任何您想做的事情,包括修改状态。

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

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 或 vanilla 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\Support\ExecutionMiddleware\AddAuthUserContextValueMiddleware

后面的参数将被尝试注入,类似于 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,
    ];
}

如果您想在每次 GraphQL 查询/变异到您的应用程序时运行中间件,请在基础查询类的 $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);
    }

如果您想全局注册中间件,请使用 config/graphql.php 中的 resolver_middleware_append

return [
    ...
    'resolver_middleware_append' => [YourMiddleware::class],
];

您也可以在任何 ServiceProvider 中使用 appendGlobalResolverMiddleware 方法

    ...
    public function boot()
    {
        ...
        GraphQL::appendGlobalResolverMiddleware(YourMiddleware::class);
        // Or with new instance
        GraphQL::appendGlobalResolverMiddleware(new YourMiddleware(...));
    }

可终止中间件

有时中间件在响应发送到浏览器后可能需要进行一些工作。如果你在中间件上定义了一个终止方法,并且你的Web服务器使用FastCGI,那么在响应发送到浏览器后,终止方法将自动被调用。

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

终止方法接收解析器参数和查询结果。

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

授权

对于类似于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的查询构造器包含在关系中的query

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 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 包含内置的标量类型,如字符串、整数、布尔值等。您可以创建自定义标量类型来处理特殊字段。

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

好处包括

  • 一个专用的描述,这样您可以为字段提供比只是将其称为字符串类型更多的意义/目的
  • 显式转换逻辑,用于以下步骤
    • 将内部逻辑转换为序列化的 GraphQL 输出 (serialize)
    • 查询/字段输入参数转换 (parseLiteral)
    • 当作为变量传递给您的查询时 (parseValue)

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

标量类型必须实现所有这些方法;您可以使用 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')
        ];
    }
}

支持自定义接口关系的查询

如果一个接口与自定义查询相关联,则需要实现返回一个包含GraphQL::type()数组的public function types(),即所有可能解析到的类型(与联合类似),以确保与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列时,字段不会定义为“关系”,而是一个带有嵌套数据的简单列。要获取不是数据库关系的嵌套对象,请在您的类型中使用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:进行备份!
  • 重新发布配置文件以了解所有新的设置
  • 解析器(resolver)的顺序和参数/类型发生了变化
    • 之前: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
    • 如果您正在使用自定义标量类型
      • 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 require 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"),
        ]),
    ];
}

已知限制

SelectFields相关

  • 通过别名解析字段只会解析一次,即使字段具有不同的参数(问题)。

GraphQL测试客户端