studio-net / laravel-graphql
具有 Laravel 力量的 GraphQL 实现
Requires
- cache/array-adapter: ^1.0
- doctrine/dbal: ^2.5
- laravel/framework: ~5.6.0|~5.7.0
- webonyx/graphql-php: ^0.13.0
Requires (Dev)
- codeception/specify: ^1.1
- friendsofphp/php-cs-fixer: ^2.10
- orchestra/testbench-browser-kit: ~3.6.0|~3.7.0@dev
- phpmd/phpmd: @stable
- phpstan/phpstan: ^0.9.2
- phpunit/phpunit: ^7.0
This package is auto-updated.
Last update: 2024-09-15 03:03:30 UTC
README
使用 Facebook GraphQL 与 Laravel 5.2 及以上版本。它基于 PHP 实现 此处。您可以在 GraphQL 介绍(位于 React 博客)中找到更多关于 GraphQL 的信息,或者您可以阅读 GraphQL 规范。
安装
composer require studio-net/laravel-graphql @dev
如果您不是使用 Laravel 5.5>=,请务必将门面和服务提供者添加到您的 config/app.php 文件中。接下来,您必须发布供应商。
php artisan vendor:publish --provider="StudioNet\GraphQL\ServiceProvider"
用法
定义
必须为每个数据源定义相应的定义,以便检索可检索和可变字段。
# app/GraphQL/Definition/UserDefinition.php namespace App\GraphQL\Definition; use StudioNet\GraphQL\Definition\Type; use StudioNet\GraphQL\Support\Definition\EloquentDefinition; use StudioNet\GraphQL\Filter\EqualsOrContainsFilter; use App\User; use Auth; /** * Specify user GraphQL definition * * @see EloquentDefinition */ class UserDefinition extends EloquentDefinition { /** * Set a name to the definition. The name will be lowercase in order to * retrieve it with `\GraphQL::type` or `\GraphQL::listOf` methods * * @return string */ public function getName() { return 'User'; } /** * Set a description to the definition * * @return string */ public function getDescription() { return 'Represents a User'; } /** * Represents the source of the data. Here, Eloquent model * * @return string */ public function getSource() { return User::class; } /** * Which fields are queryable ? * * @return array */ public function getFetchable() { return [ 'id' => Type::id(), 'name' => Type::string(), 'last_login' => Type::datetime(), 'is_admin' => Type::bool(), 'permissions' => Type::json(), // Relationship between user and posts 'posts' => \GraphQL::listOf('post') ]; } /** * Which fields are filterable ? And how ? * * @return array */ public function getFilterable() { return [ 'id' => new EqualsOrContainsFilter(), "nameLike" => function($builder, $value) { return $builder->whereRaw('name like ?', $value), }, ]; } /** * Resolve field `permissions` * * @param User $user * @return array */ public function resolvePermissionsField(User $user) { return $user->getPermissions(); } /** * Which fields are mutable ? * * @return array */ public function getMutable() { return [ 'id' => Type::id(), 'name' => Type::string(), 'is_admin' => Type::bool(), 'permissions' => Type::array(), 'password' => Type::string() ]; } } # config/graphql.php return [ // ... 'definitions' => [ \App\GraphQL\Definition\UserDefinition::class, \App\GraphQL\Definition\PostDefinition::class ], // ... ]
定义是此过程中的一个重要部分。它定义了可查询和可变字段。此外,它还允许您使用 getTransformers 方法仅对某些数据应用转换器。有 5 种类型的转换器可以应用:
list:创建一个查询以获取多个对象(User => users)view:创建一个查询以检索一个对象(User => user)drop:创建一个突变以删除一个对象(User => deleteUser)store:创建一个突变以更新一个对象(User => user)batch:创建一个突变以一次性更新多个对象(User => users)restore:创建一个突变以恢复一个对象(User => restoreUser)
默认情况下,定义抽象类处理 Eloquent 模型转换。
定义由类型组成。我们的自定义类扩展了默认的 GraphQL\Type\Definition\Type 类,以实现 json 和 datetime 可用类型。
查询
如果您想手动创建查询,这是可能的。
# app/GraphQL/Query/Viewer.php namespace App\GraphQL\Query; use StudioNet\GraphQL\Support\Definition\Query; use Illuminate\Support\Facades\Auth; use App\User; use Auth; class Viewer extends Query { /** * {@inheritDoc} */ protected function authorize(array $args) { // check, that user is not a guest return !Auth::guest(); } /** * {@inheritDoc} */ public function getRelatedType() { return \GraphQL::type('user'); } /** * {@inheritdoc} */ public function getSource() { return User::class; } /** * Return logged user * * @return User|null */ public function getResolver($opts) { return Auth::user(); } } # config/graphql.php return [ 'schema' => [ 'definitions' => [ 'default' => [ 'query' => [ 'viewer' => \App\GraphQL\Query\Viewer::class ] ] ] ], 'definitions' => [ \App\GraphQL\Definition\UserDefinition::class ] ];
getResolver() 接收一个包含以下项的数组参数
root第一个参数由 webonyx 库提供 -GraphQL\Executor\Executor::resolveOrError()args第二个参数由 webonyx 库提供context第三个参数由 webonyx 库提供info第四个参数由 webonyx 库提供fields字段数组,这些字段是从查询中检索到的。在StudioNet\GraphQL\GraphQL::FIELD_SELECTION_DEPTH深度限制内with可以/应该预加载的关系数组。**注意**:只有在定义了getSource()时才会查找这些关系 - 此方法应返回查询中相关根类型的类名。如果没有定义getSource(),则with将始终为空。
突变
突变用于更新或创建数据。
# app/GraphQL/Mutation/Profile.php namespace App\GraphQL\Mutation; use StudioNet\GraphQL\Support\Definition\Mutation; use StudioNet\GraphQL\Definition\Type; use App\User; class Profile extends Mutation { /** * {@inheritDoc} */ protected function authorize(array $args) { // check, that user is not a guest return !Auth::guest(); } /** * {@inheritDoc} * * @return ObjectType */ public function getRelatedType() { return \GraphQL::type('user'); } /** * {@inheritDoc} */ public function getArguments() { return [ 'id' => ['type' => Type::nonNull(Type::id())], 'blocked' => ['type' => Type::string()] ]; }; /** * Update user * * @param mixed $root * @param array $args * * @return User * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getResolver($root, array $args) { $user = User::findOrFail($args['id']); $user->update($args); return $user; } } # config/graphql.php return [ 'schema' => [ 'definitions' => [ 'default' => [ 'query' => [ 'viewer' => \App\GraphQL\Query\Viewer::class ], 'mutation' => [ 'viewer' => \App\GraphQL\Mutation\Profile::class ] ] ] ], 'definitions' => [ \App\GraphQL\Definition\UserDefinition::class ] ];
管道
管道用于将定义转换为可查询和可变操作。但是,您可以轻松创建自己的并管理有用的案例,例如在执行任何操作之前断言 ACL 等。
管道使用与Laravel中间件相同的格式实现,但将Eloquent查询构建器作为第一个参数传递。
创建新管道
namespace App/GraphQL/Pipe; use Closure; use Illuminate\Database\Eloquent\Builder; class OnlyAuthored { /** * returns only posts that the viewer handle * * @param Builder $builder * @param Closure $next * @param array $opts * @return \Illuminate\Database\Eloquent\Model */ public function handle(Builder $builder, Closure $next, array $opts) { $builder->where('author_id', $opts['context']->getKey()); return $next($builder); } }
namespace App\GraphQL\Definition; class PostDefinition extends EloquentDefinition { // ... /** * {@inheritDoc} * * @return array */ public function getPipes(): array { return array_merge_recursive(parent::getPipes(), [ 'list' => [\App\GraphQL\Pipe\OnlyAuthored::class], ]); } // ... }
在此示例中,当您查询posts查询时,您将仅获取查看者的帖子,而不是所有帖子。您还可以在管道中指定参数,如下所示
namespace App/GraphQL/Pipe; use Closure; use Illuminate\Database\Eloquent\Builder; use GraphQL\Type\Definition\Type; use StudioNet\GraphQL\Support\Pipe\Argumentable; use StudioNet\GraphQL\Support\Definition\Definition; class FilterableGroups implements Argumentable { /** * returns only given groups * * @param Builder $builder * @param Closure $next * @param array $opts * @return \Illuminate\Database\Eloquent\Model */ public function handle(Builder $builder, Closure $next, array $opts) { if (array_get($opts, ['args.group_ids', false])) { $builder->whereIn('group_id', $opts['args']['group_ids']); } return $next($builder); } /** * @implements * * @param Definition $definition * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getArguments(Definition $definition): array { return [ 'groups_id' => [ 'type' => Type::json(), 'description' => 'Filtering by group IDs' ] ]; } }
需要授权
目前您有权限保护您的查询和突变。您必须在查询/突变中实现authorize()方法,该方法返回一个布尔值,指示请求的查询/突变是否需要执行。如果方法返回false,将抛出UNAUTHORIZED GraphQL错误。
用法示例在上面的查询和突变中。
定义转换器的保护目前尚未实现,但将来可能会实现。到目前为止,您必须自己定义查询/突变,然后使用authorize()中的逻辑来保护它。
自文档
该包实现了一个文档生成器。默认情况下,您可以通过导航到/doc/graphql来访问它。您可以在配置文件中更改此行为。内置文档是从这个存储库实现的。
示例
query { viewer { name email posts { title content } } } # is equivalent to (if user id exists) query { user (id: 1) { name email posts { title content } } }
使用过滤器
在声明getFilterable数组时,您可以定义字段的过滤器。
您可以使用闭包、数组或实现FilterInterface类的对象。
闭包(或FilterInterface::updateBuilder方法)将使用以下参数调用
- $builder : 当前Laravel查询构建器
- $value : 过滤器值
- $key : 过滤器键
您还可以为您可过滤的输入字段定义GraphQL类型。默认情况下使用Type::json()。有几种选项可以定义类型(以下代码块中列出了所有示例)
- 如果您使用实现
TypedFilterInterface的类,则使用方法TypedFilterInterface::getType返回的类型; - 如果您使用闭包,您必须定义一个包含键
type的数组,该键包含您希望的类型和包含闭包的resolver; - 如果您定义一个数组,并且在
resolver中传递了一个实现TypedFilterInterface的类的对象,则TypedFilterInterface::getType的类型将覆盖数组键type中的类型; - 在其他所有情况下,将使用默认类型
Type::json()
您还可以使用以下预定义的EqualsOrContainsFilter。
public function getFilterable() { return [ // Simple equality check (or "in" if value is an array). Type is Type::json() 'id' => new EqualsOrContainsFilter(), // Customized filter. Type is Type::json() "nameLike" => function($builder, $value) { return $builder->whereRaw('name like ?', $value); }, // type is Type::string() "anotherFilter" => [ "type" => Type::string(), "resolver" => function($builder, $value) { return $builder->whereRaw('anotherFilter like ?', $value); } ], // type is what is returned from `ComplexFilter::getType()`. // This is the preffered way to define filters, as it keeps definitions code clean "complexFilter" => new ComplexFilter(), // type in array will be overriden by what is returned from `ComplexFilter::getType()`. // this kind of difinition is not clear, but is implemented for backward compatibilities. Please don't use it "complexFilter2" => [ "type" => Type::int(), "resolver" => new ComplexFilter() ], ]; }
query { users (take: 2, filter: {"id", "1"}) { items { id name } } }
这将执行查询:WHERE id = 1
query { users (take: 2, filter: {"id", ["1,2"]}) { items { id name } } }
这将执行查询:WHERE id in (1,2)
query { users (take: 2, filter: {"nameLike", "%santiago%"}) { items { id name } } }
这将执行查询:WHERE name like '%santiago%'
排序(order_by)
您可以使用order_by参数(它是String[])指定结果顺序(这调用Eloquent的orderBy)。
query { users (order_by: ["name"]) { items { id, name } } }
您可以通过将asc(默认值)或desc附加到排序字段来指定方向
query { users (order_by: ["name_desc"]) { items { id, name } } }
您可以指定多个order_by
query { users (order_by: ["name_asc", "email_desc"]) { items { id, name } } }
分页:限制(take)、偏移量(skip)
您可以使用take(Int)限制结果数量
query { users (order_by: ["name"], take: 5) { items { id, name } } }
您可以使用skip(Int)跳过一些结果
query { users (order_by: ["name"], take: 5, skip: 10) { items { id, name } } }
您可以获取有用的分页信息
query { users (order_by: ["name"], take: 5, skip: 10) { pagination { totalCount page numPages hasNextPage hasPreviousPage } items { id name } } }
Where
totalCount是结果总数page是当前页(基于take,它用作页面大小)numPages是总页数hasNextPage,如果有下一页则为truehasPreviousPage,如果有上一页则为true
突变
mutation { # Delete object delete : deleteUser(id: 5) { first_name last_name }, # Update object update : user(id: 5, with : { first_name : "toto" }) { id first_name last_name }, # Create object create : user(with : { first_name : "toto", last_name : "blabla" }) { id first_name last_name }, # Update or create many objects at once batch : users(objects: [{with: {first_name: 'studio'}}, {with: {first_name: 'net'}}]) { id first_name } }
突变:自定义输入字段
您可以指定一个“可变”字段,该字段不在Eloquent模型中,并为其定义一个自定义方法。
对于名为 foo_bar 的字段,方法名称必须为 inputFooBarField,并且它以 Eloquent 模型和用户输入值作为参数。
示例(在 定义 中)
use Illuminate\Database\Eloquent\Model; /* ... */ public function getMutable() { return [ 'id' => Type::id(), 'name' => Type::string(), // ... // Define a custom input field, which will uppercase the value 'name_uppercase' => Type::string(), ]; } /* ... */ /** * Custom input field for name_uppercase * * @param Model $model * @param string $value */ public function inputNameUppercaseField(Model $model, $value) { $model->name = mb_strtoupper($value); }
输入方法在模型保存之前执行。
您可以返回一个包含 "saved" 回调的数组,该回调将在保存后执行(这对于 Eloquent 关联模型可能很有用)。
/** * Custom input field for name_uppercase * * @param Model $model * @param string $value */ public function inputNameUppercaseField(Model $model, $value) { $model->name = mb_strtoupper($value); return [ 'saved' => function() use ($model, $value) { // Executed after save } ]; }
N+1 问题
常见的问题是,GraphQL 库是否解决了 n+1 问题。这发生在 GraphQL 解析关系时。通常实体是在没有关系的情况下检索的,当 GraphQL 查询需要检索关系时,对于每个检索到的实体,关系将单独从 SQL 中检索。因此,您将得到 N+1 个查询,其中 N 是根实体的结果数。在示例中,您将查询一个关系。如果您查询更多关系,则它变成 N^2+1 问题。
为了解决这个问题,Eloquent 已经提供了预加载关系的选项。这个库中的转换器使用预加载,取决于您查询的内容。
目前,这种智能检测在视图和列表转换器上工作得非常好。其他转换器将很快进行重写。
贡献
如果您想参与这个项目,谢谢!为了正常工作,您应该安装所有开发依赖项,并在推送之前运行以下命令,以防止出现糟糕的 PR。
$> ./vendor/bin/phpmd src text phpmd.xml $> ./vendor/bin/phpmd tests text phpmd.xml $> ./vendor/bin/phpstan analyse --autoload-file=_ide_helper.php --level 1 src $> ./vendor/bin/php-cs-fixer fix