使用GraphQL+Lighthouse(+Laravel)进行API开发2(开发部分1- 实现非Eloquent模型)
这次我们将继续介绍在Laravel中实现GraphQL+Lighthouse的方法。请务必也查看使用GraphQL+Lighthouse(+Laravel)进行API开发的第一部分(安装方法和设置篇)。
政策方向
正如之前的文章所述,这次我们决定在现有系统中途开始使用GraphQL实现API。此外,由于Laravel应用程序的实现也沿用了旧系统的方针,所以数据库设计无法完全享受ORM的好处。
因此,這一次我們決定不使用Eloquent來實現GraphQL的服務器端。另外,GraphQL有兩種方法,即Query和Mutation,但這次的重點是介紹Query的實現。
履行
1. 模式定义
我认为在上一篇文章中,routes/graphql/schema.graphql文件已经创建好了模式定义文件(以下简称类型文件)的框架。
让我们在这里写下查询的类型定义。
首先,我們將刪除與本次無關的型別定義。
#"A datetime string with format 'Y-m-d H:i:s', e.g. '2018-01-01 13:00:00'."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
#"A date string with format 'Y-m-d', e.g. '2011-05-23'."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")
# ここから下削除
# ...
既提供轻房价的两个标量(scalar)类型,可以保留或删除,都没有问题。
接下来让我们定义查询的类型。
type {
top: Top
}
查询将返回一个Top类型的值。
下面定义Top型。
对于小型应用,可以在一个类型文件中写下所有类型。但随着开发的进行,类型文件可能会变得非常庞大,因此我们需要准备另一个类型文件top.graphql。这次我们随便准备了一些类型。
type Top {
# nullを許容しない
user: User!
purchaseHistory: PurchaseHistory
# Shopが複数入った配列型
favoriteShops: [Shop]
}
type User {
id: Int!
name: String!
point: Int!
}
type PurchaseHistory {
timestamp: Date
histories: [History]
}
type Date {
year: Int
month: Int
day: Int
hour: Int
minute: Int
second: Int
}
type History {
shop: Shop
purchasedDate: Date
purchasedAmount: Int
}
type Shop {
shopId: Int!
shopName: String!
}
为了在routes/graphql/schema.graphql中加载此Top类型,请添加以下句子。
#import ./top.graphql
type {
top: Top
}
请注意,在 “import” 后面加一个空格后,可能无法正常工作。
以上是关于模式的定义已经结束了。
2. 生成查询模板
先执行下面的命令,创建一个Query的模板吧。
php artisan lighthouse:query Top
# docker環境の場合 (dc=docker-compose, hoge=${service_name})
dc exec hoge php artisan lighthouse:query Top
然后,将生成以下类型的文件。
<?php
declare(strict_types = 1);
namespace App\Http\GraphQL\Queries;
class Top
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param array $args The arguments that were passed into the field.
* @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query.
* @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
*
* @return mixed
*/
public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo): array
{
}
}
我认为可以将此视为普通的控制器。
名称空间
在Http目录下,默认生成一个名为GraphQL的新目录。
/*
|--------------------------------------------------------------------------
| Namespaces
|--------------------------------------------------------------------------
|
| These are the default namespaces where Lighthouse looks for classes
| that extend functionality of the schema.
|
*/
'namespaces' => [
'models' => 'App\\Models',
'queries' => 'App\\Http\\GraphQL\\Queries',
'mutations' => 'App\\Http\\GraphQL\\Mutations',
'interfaces' => 'App\\Http\\GraphQL\\Interfaces',
'unions' => 'App\\Http\\GraphQL\\Unions',
'scalars' => 'App\\Http\\GraphQL\\Scalars',
],
如果在上述的配置文件中指定了命名空间,文件将会在任意位置生成。
自定义查询/解析器
此外,我们还可以使用php artisan lighthouse query:CustomQuery命令在schema.graphql中为任何自定义的类生成模板,这样我们就可以为名为top的查询生成相应的类模板。在这种情况下,我们需要使用@field指令在schema.graphql中为目标查询指定解析程序。
type {
top: Top @field(resolver: "App\\GraphQL\\Queries\\CustomQuery@resolverMethodName")
# config/lighthouse.phpで指定した名前空間と同じ場合、省略記法が使える
# @field("CustomQuery@resolverMethodName")
}
3. 查询的实现
接下来,我们将实现处理请求并返回响应的部分。
当执行`php artisan lighthouse query:Top`时,将会生成Laravel的控制器部分对应的Query类。当向GraphQL的端点发送请求时,将会执行resolve方法。该方法的参数包含请求的上下文信息等。
准备虚拟数据。
首先,让我们准备一个作为返回值的关联数组,与定义的数据类型相对应。
<?php
declare(strict_types = 1);
namespace App\Http\GraphQL\Queries;
class Top
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param array $args The arguments that were passed into the field.
* @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query.
* @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
*
* @return mixed
*/
public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo)
{
return [
'user' => [
'id' => 10,
'name' => 'GraphQL Taro',
'point' => 3000,
],
'purchaseHistory' => [
'timestamp' => [
'year' => 2019,
'month' => 2,
'day' => 10,
'hour' => 22,
'minute' => 10,
'second' => 0,
],
'histories' => [
[
'shop' => [
'shopId' => 100,
'shopName' => 'shop1',
],
'purchasedDate' => [
'year' => 2019,
'month' => 1,
'day' => 1,
'hour' => 10,
'minute' => 0,
'second' => 0,
],
'purchasedAmount' => 1000,
],
],
],
'favoriteShops' => [
['shopId' => 100, 'shopName' => 'shop1'],
['shopId' => 200, 'shopName' => 'shop2'],
],
];
}
}
只要能够返回类似于这样的响应,无论采用什么样的类设计,都不会有问题。
在Laravel中,您可以使用Repository模式从数据库或其他存储中获取数据,即使没有使用Eloquent也可以自由设计。根据DDD设计,您可以为每个领域准备Service类,然后返回History、Shop等模型类。
只要最终确定响应的形式,GraphQL的服务器端开发几乎就完成了。
对每个列进行处理,每列处理的方式为3.2。
如果要实现GraphQL的特点,即根据每个查询返回所需数据的功能,比起执行所有逻辑处理然后再进行数据过滤,只执行所需的处理更受欢迎。我们这次准备了一个名为columnResolver的方法,用于执行与查询中包含的列对应的逻辑处理的实现。
<?php
declare(strict_types = 1);
namespace App\Http\GraphQL\Queries;
use Throwable;
class Top
{
public const USER = 'user';
public const PURCHASE_HISTORY = 'purchaseHistory';
public const FAVORITE_SHOPS = 'favoriteShops';
public const COLUMNS = [
self::USER,
self::PURCHASE_HISTORY,
self::FAVORITE_SHOPS,
];
/** @var string[] */
protected $columns = [];
/** @var mixed[] */
protected $response = [];
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param array $args The arguments that were passed into the field.
* @param GraphQLContext|null $context Arbitrary data that is shared between all fields of a single query.
* @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
*
* @return mixed
*/
public function resolve($rootValue, array $args, GraphQLContext $context = null, ResolveInfo $resolveInfo)
{
$this->columns = $resolveInfo->getFieldSelection();
foreach (self::COLUMNS as $column) {
$this->resolveColumn($column);
}
return $this->response;
}
public function resolveColumn(string $name): void
{
$resolver = $this->getResolverName($name);
// リクエストに含まれるカラムのみを処理するためのフィルタ
if (!isset($this->columns[$name])) {
return;
}
try {
$this->response[$name] = $resolver();
} catch (Throwable $throwable) {
// Log残すなどの処理を含めても良い
$this->response[$name] = null;
}
}
public function getUser(): array
{
return [
'id' => 10,
'name' => 'GraphQL Taro',
'point' => 3000,
];
}
public function getPurchaseHistory(): array
{
// 省略
return [];
}
public function getFavoriteShops(): array
{
// 省略
return [];
}
private function getResolverName(string $name): string
{
return 'get' . ucwords($name);
}
}
以上则是查询的实现工作的结束。然而,GraphQL仍将会更加普及,并提出各种实现方法。我也将继续努力尝试。
为了下一次
我打算在下一次寫作時,關於對本次實施的東西添加自己的錯誤處理進行描述。