asmud/graphql-lumen

Laravel的PHP GraphQL包装器 - 添加Lumen支持

维护者

详细信息

github.com/asmud/graphql-lumen

源代码

资助包维护!
mfn

dev-main 2023-04-05 09:44 UTC

This package is auto-updated.

Last update: 2024-09-05 12:48:57 UTC


README

Latest Stable Version License Tests Downloads Get on Slack

在Laravel 6.0 & 8.0+上使用PHP 7.4+的Facebook GraphQL。它基于GraphQL参考实现的PHP端口。你可以在GraphQL Introduction上找到更多关于GraphQL的信息,该信息位于React博客,或者你可以阅读GraphQL specifications

  • 允许创建作为请求端点的查询突变
  • 支持多个模式
    • 每个模式查询/突变/类型
    • 每个模式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

默认GraphiQL视图使用全局csrf_token()辅助函数。

使用

概念

在一头扎进代码之前,了解围绕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结果、缓存等),但您必须注意执行模型,以避免重复检索并智能地预取数据。

GraphiQL

GraphiQL 是在浏览器中轻量级的 "GraphQL IDE"。它利用 GraphQL 类型系统,并允许自动完成所有查询/变更/类型和字段。

GraphiQL 在功能和复杂性方面有所发展,因此为方便起见,此库中直接包含了一个较旧版本。

默认配置启用后,它可通过 /graphiql 路由访问。

如果您使用多个模式,您可以通过 /graphiql/<模式名称> 访问它们。

中间件概述

以下中间件概念被支持

  • HTTP 中间件(例如 Laravel)
  • GraphQL执行中间件
  • GraphQL解析器中间件

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

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

HTTP中间件

任何 Laravel 兼容的 HTTP 中间件 都可以通过全局配置 graphql.route.middleware 或基于模式的配置 graphql.schemas.<您的模式>.middleware 提供给所有 GraphQL 端点。基于模式的中间件会覆盖全局中间件。

GraphQL执行中间件

GraphQL 请求的处理(以下称为 "执行"),通过一系列中间件进行。

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

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

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

GraphQL解析器中间件

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

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

模式

模式是定义 GraphQL 端点所必需的。您可以定义多个模式,并将不同的 HTTP 中间件执行中间件 分配给它们,除了全局中间件。例如

'schema' => 'default',

'schemas' => [
    'default' => [
        'query' => [
            ExampleQuery::class,
            // It's possible to specify a name/alias with the key
            // but this is discouraged as it prevents things
            // like improving performance with e.g. `lazyload_types=true`
            // It's recommended to specify just the class here and
            // rely on the `'name'` attribute in the query / type.
            'someQuery' => AnotherExampleQuery::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);

与查询/变更一样,您可以使用别名(尽管这会阻止它利用延迟类型加载)

'schemas' => [
    'default' => [
        // ...
        
        'types' => [
            'Useralias' => 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
        ],
        // ...
    ]
]

就这样。你应该能够通过向 /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或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_namevalidator_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,
    ];
}

如果您希望中间件在每次对应用程序的 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);
    }

终止中间件

有时中间件可能需要在将响应发送到浏览器后执行一些工作。如果您在中间件中定义了一个终止方法,并且您的 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();
    }
}

您的用户类型可能如下所示。profileposts 关系也必须存在于 UserModel 的关系中。如果某些字段对于加载关系或验证等是必需的,则可以定义一个 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 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自带了字符串、整数、布尔值等内置标量类型。您可以创建自定义标量类型来特殊用途字段。

一个例子可能是链接:而不是使用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')
            ]
        ];
    }
}

联合

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

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

定义联合类型的示例

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() 的数组,即它可能解析到的所有可能的类型(与联合类型的工作方式非常相似),以便与 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。如果您想验证输入数据,可以添加规则选项。了解更多关于输入对象 这里

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

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批处理
  • lazyload_types
    类型将在需要时加载。默认启用,这可以提高性能。不能与类型别名一起使用。
  • error_formatter
    这个可调用函数将为每个GraphQL捕获的错误对象传递错误。该方法应返回表示错误的数组。
  • errors_handler
    自定义错误处理。默认处理程序将异常传递给Laravel错误处理机制。
  • security
    限制查询复杂性和深度的各种选项,请参阅https://webonyx.github.io/graphql-php/security/文档。
    • query_max_complexity
    • 查询最大深度
    • 禁用内省
  • 分页类型
    您可以定义自己的分页类型。
  • 简单分页类型
    您可以定义自己的简单分页类型。
  • graphiql
    GraphiQL配置(见(《https://github.com/graphql/graphiql》)
    • 前缀
      路由前缀
    • 控制器
      处理该路由的控制器/方法
    • 中间件
      在调用控制器之前运行的任何中间件
    • 视图
      要使用哪个视图
    • 显示
      是否启用。
      注意:建议在生产环境中禁用此功能!
  • 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:进行备份!
  • 重新发布配置文件以了解所有新设置
  • 解析器的顺序和参数/类型已更改
    • 之前: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方法的签名已更改
      • from public function toType()
      • to public function toType(): \GraphQL\Type\Definition\Type
    • getFields方法的签名已更改
      • from public function getFields()
      • to public function getFields(): array
    • interfaces方法的签名已更改
      • from public function interfaces()
      • to public function interfaces(): array
    • types方法的签名已更改
      • from public function types()
      • to public function types(): array
    • type方法的签名已更改
      • from public function type()
      • to public function type(): \GraphQL\Type\Definition\Type
    • args方法的签名已更改
      • from public function args()
      • to public function args(): array
    • queryContext方法的签名已更改
      • from protected function queryContext($query, $variables, $schema)
      • to protected function queryContext()
    • 控制器方法的query签名已更改
      • from function query($query, $variables = [], $opts = [])
      • to 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 并对其进行调整(前缀、中间件、模式、类型、突变、查询、安全设置、graphiql)
    • 已删除设置
      • 域名
      • 解析器
    • schema(默认模式)已重命名为 default_schema
    • middleware_schema 不存在,它现在在 schema.<name>.middleware 中定义
  • 更改命名空间引用
    • Folklore\
    • Rebing\
  • 请参阅从 v1 升级到 v2 的升级指南,了解所有函数签名更改。
  • ShouldValidate 特性不再存在;提供的功能已内置到 Field 中。
  • 查询/突变的 resolve 方法的第一个参数现在是 null(之前其默认值为空数组)。

性能考虑

类型懒加载

类型延迟加载是一种提高启动性能的方法。

如果你使用别名声明类型,这不受支持,你需要将 lazyload_types 设置为 false

懒加载不支持别名示例

例如,你不能有一个具有 $name 属性 example 的查询类 ExampleQuery,但用不同的名称注册它;这 不会 工作。

'query' => [
    'aliasedExample' => ExampleQuery::class,
],

包装类型

你可以包装类型以向查询和突变添加更多信息。类似于分页的工作方式,你可以用你想要注入的额外数据做同样的事情(请参阅测试示例)。例如,在你的查询中

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测试客户端

Lumen 注释 ***

  • 修改并确保 bootstrap/app.php 中已启用 facade($app->withFacades())并注册了 Auth Service($app->register(App\Providers\AuthServiceProvider::class)
  • 将 GraphQL Service $app->register(Rebing\GraphQL\GraphQLServiceProvider::class) 手动注册到 bootstrap/app.php 文件中
  • 可选地复制此库的配置文件(graphql.php)到 config 文件夹中