tdmhai/graphql-laravel

Laravel 对 PHP GraphQL 的封装

安装: 5

依赖者: 0

建议者: 0

安全性: 0

星标: 0

关注者: 1

分叉: 266

类型:项目

3.1.0 2019-10-22 22:53 UTC

README

Latest Stable Version codecov Build Status Style CI License Get on Slack

注意:这是 2.* 版本的文档,请参阅 v1 分支以获取 1.* 版本的文档

使用 Facebook GraphQL,支持 Laravel 5.5+。它基于此处的 PHP 实现 here。您可以在 GraphQL Introduction 了解更多关于 GraphQL 的信息,该文章发布在 React 博客上,或者您可以阅读 GraphQL specifications。这是一个持续进行中的项目。

此软件包与 Eloquent 模型或任何其他数据源兼容。

  • 允许创建作为请求端点的 查询突变
  • 可以为每个查询/突变定义自定义 中间件
  • 查询返回 类型,可以设置自定义的 隐私 设置。
  • 查询的字段可以选择通过 SelectFields 类从数据库动态检索。

它提供了以下功能和改进,比 Folklore 的原始软件包有更多优势

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

安装

依赖项

安装

使用 Composer 安装软件包

composer require rebing/graphql-laravel
Laravel 5.5+

1. Laravel 5.5+ 将自动发现该软件包,对于旧版本,请在您的 config/app.php 文件中添加以下服务提供程序

Rebing\GraphQL\GraphQLServiceProvider::class,

和服务别名

'GraphQL' => 'Rebing\GraphQL\Support\Facades\GraphQL',

2. 发布配置文件

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

3. 查看配置文件

config/graphql.php
Lumen(实验性!)

1. 将以下服务提供程序添加到 bootstrap/app.php 文件中

$app->register(Rebing\GraphQL\GraphQLLumenServiceProvider::class);

2. 发布配置文件

$ php artisan graphql:publish

3. 将配置添加到 bootstrap/app.php 文件中 重要:这需要在注册服务提供程序之前完成

$app->configure('graphql');
...
$app->register(Rebing\GraphQL\GraphQLLumenServiceProvider::class);

4. 查看配置文件

config/graphql.php

默认的 GraphiQL 视图使用全局 csrf_token() 辅助函数。在 Lumen 中,此函数默认不可用。

为了解决这个问题

  • 指向您的本地 GraphiQL 视图:将 graphql.view 更改为 'vendor/graphql/graphiql'
  • 修改您的文件 resources/views/vendor/graphql/graphiql.php 并移除调用

使用方法

模式

定义GraphQL端点需要使用模式。您可以定义多个模式,并将不同的中间件分配给它们,除了全局中间件。例如

'schema' => 'default_schema',

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

        ],
        'middleware' => ['auth'],
    ],
],

创建查询

首先您需要创建一个类型。如果指定关系,则需要Eloquent模型。

注意:如果是一个非数据库字段或不是关系,则需要selectable键。

<?php

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

    // If you want to resolve the field yourself, you can declare a method
    // with the following format resolve[FIELD_NAME]Field()
    protected function resolveEmailField($root, $args)
    {
        return strtolower($root->email);
    }    
}

将类型添加到config/graphql.php配置文件

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

您也可以通过服务提供商等在GraphQL外观中添加类型。

GraphQL::addType(\App\GraphQL\Types\UserType::class, 'user');

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

<?php

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 query'
    ];

    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, $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' => [
            'users' => App\GraphQL\Queries\UsersQuery::class
        ],
        // ...
    ]
]

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

query FetchUsers {
    users {
        id
        email
    }
}

例如,如果您使用homestead

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

创建突变

变异类似于其他查询。它接受参数(这些参数将用于执行变异)并返回一个特定类型的对象。

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

<?php

namespace App\GraphQL\Mutations;

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

class UpdateUserPasswordMutation extends Mutation
{
    protected $attributes = [
        'name' => 'UpdateUserPassword'
    ];

    public function type(): Type
    {
        return 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, $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' => [
            'updateUserPassword' => 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}}

向变异添加验证

可以向变异添加验证规则。它使用Laravel的Validator$args执行验证。

在创建变异时,您可以通过以下方式添加一个方法来定义应用的验证规则

<?php

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

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

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

        return $user;
    }
}

或者,您可以为每个参数定义规则

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

    //...
}

在执行变异时,它将返回任何发生的验证错误。由于GraphQL规范定义了错误的一定格式,验证错误将作为额外的validation属性添加到错误对象中。要找到验证错误,您应该检查具有等于'validation'message的错误,然后validation属性将包含Laravel Validator返回的正常错误消息

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

返回的验证错误可以通过覆盖变异上的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',                     
    ];
}

文件上传

此库提供符合规范的中介件,规范位于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 - 这意味着更改请求的中介件将不起作用。

<?php

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

    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, $args, $context, ResolveInfo $resolveInfo, Closure $getSelectFields)
    {
        $file = $args['profilePicture'];

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

注意:您可以使用Altair测试您的文件上传实现,方法如此处所述。

授权

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

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查询传递的参数

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

    // ...
}

隐私

您可以为每种类型的字段设置自定义隐私属性。如果字段不允许,将返回 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): bool {
                    return $args['id'] == Auth::id();
                }
            ]
        ];
    }

    // ...

}

或者您可以创建一个继承自抽象GraphQL隐私类的类

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

class MePrivacy extends Privacy
{
    public function validate(array $queryArgs): bool
    {
        return $args['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端点时,您可以传递一个 params(或您在配置中定义的任何内容)参数。

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

请注意,您的客户端框架可能使用与 params 不同的参数名称。您可以通过调整 graphql.php 配置文件中的 params_key 来自定义参数名称,以匹配客户端使用的任何名称。

自定义字段

如果您想在不同类型中重用字段,也可以将其定义为类。

<?php

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, $args)
    {
        $width = isset($args['width']) ? $args['width']:100;
        $height = isset($args['height']) ? $args['height']:100;

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

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

<?php

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

预加载关系

传递给查询的解析方法的第五个参数是一个闭包,该闭包返回一个 Rebing\GraphQL\Support\SelectFields 实例,您可以使用它来检索请求中的键。以下是一个使用此信息来预加载相关Eloquent模型的示例。

这样,数据库中只会查询所需的字段。

闭包可以接受一个可选参数,用于分析查询的深度。

您的查询可能如下所示

<?php

namespace App\GraphQL\Queries;

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

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

    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, $args, $context, ResolveInfo $info, Closure $getSelectFields)
    {
        // $info->getFieldSelection($depth = 3);

        // If your GraphQL query exceeds the default nesting query, you can increase it here:
        // $fields = $getSelectFields(11);

        /** @var SelectFields $fields */
        $fields = $getSelectFields();
        $select = $fields->getSelect();
        $with = $fields->getRelations();

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

        return $users->get();
    }
}

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

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

<?php

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

类型关系查询

您还可以通过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`
                'query'         => function(array $args, $query, $ctx) {
                    return $query->where('posts.created_at', '>', $args['date_from']);
                }
            ]
        ];
    }
}

分页

如果查询或突变返回 PaginationType,将使用分页。请注意,您必须手动处理限制和页面值。

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

    // ...

    public function resolve($root, $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' 对象,因为返回的数据在返回的分区元数据相同级别上提供了分页资源。

批处理

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

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

标量类型

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

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

好处包括

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

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

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

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

枚举

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

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

<?php

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客户端可以从中选择的枚举值,而值是您的服务器将接收的值(枚举将被解析为什么)。

graphql.php配置文件的types数组中注册枚举

'types' => [
    'EpisodeEnum' => EpisodeEnum::class
];

然后使用它

<?php

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

联合体

联合类型是一个简单的枚举其他对象类型的抽象类型。联合类型的值实际上是包含对象类型之一的一个值。

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

定义联合类型的示例

<?php

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

接口

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

接口的实现

<?php

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

实现接口的类型

<?php

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

共享接口字段

由于您通常需要在具体类型中重复许多接口的字段定义,因此共享接口的定义是有意义的。您可以使用方法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.'
        ]
    ]);
}

输入对象

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

首先将输入对象类型作为一个扩展的GraphQLType类创建

<?php

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(),
                'rules' => ['min:0', 'max:5']
            ]
        ];
    }
}

graphql.php配置文件的types数组中注册输入对象

'types' => [
    'ReviewInput' => ReviewInput::class
];

然后在突变中使用它

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

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

}

JSON列

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

class UserType extends GraphQLType
{

    // ...

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

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

    // ...
}

字段弃用

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

<?php

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

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

指南

从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 方法的签名已更改
      • public function toType()
      • public function toType(): \GraphQL\Type\Definition\Type
    • getFields 方法的签名已更改
      • public function getFields()
      • public function getFields(): array
    • interfaces 方法的签名已更改
      • public function interfaces()
      • public function interfaces(): array
    • types 方法的签名已更改
      • public function types()
      • public function types(): array
    • type 方法的签名已更改
      • public function type()
      • public function type(): \GraphQL\Type\Definition\Type
    • args 方法的签名已更改
      • public function args()
      • public function args(): array
    • queryContext 方法的签名已更改
      • protected function queryContext($query, $variables, $schema)
      • protected function queryContext()
    • 控制器方法的 query 签名已更改
      • function query($query, $variables = [], $opts = [])
      • function query(string $query, ?array $variables = [], array $opts = []): array
    • 如果您正在使用自定义 Scalar 类型
      • parseLiteral 方法的签名已更改(由于 webonxy 库的升级)
        • 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
  • 查询/突变的解析方法的第一个参数现在是 null(之前默认为空数组)

性能考虑

类型的延迟加载

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

如果您使用别名声明类型,则不支持。如果不是这种情况,您可以通过将 lazyload_types 设置为 true 来启用它。

不支持的延迟加载的别名示例

例如,您不能有一个具有 $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, $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"),
        ]),
    ];
}