studio-net/laravel-graphql

具有 Laravel 力量的 GraphQL 实现

v0.9.7 2019-03-14 14:39 UTC

README

使用 Facebook GraphQL 与 Laravel 5.2 及以上版本。它基于 PHP 实现 此处。您可以在 GraphQL 介绍(位于 React 博客)中找到更多关于 GraphQL 的信息,或者您可以阅读 GraphQL 规范

Latest Stable Version Latest Unstable Version Total Downloads Monthly Downloads Daily Downloads License Build Status

安装

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 类,以实现 jsondatetime 可用类型。

查询

如果您想手动创建查询,这是可能的。

# 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

您可以使用takeInt)限制结果数量

query {
	users (order_by: ["name"], take: 5) { items { id, name } }
}

您可以使用skipInt)跳过一些结果

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,如果有下一页则为true
  • hasPreviousPage,如果有上一页则为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