使用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仍将会更加普及,并提出各种实现方法。我也将继续努力尝试。

为了下一次

我打算在下一次寫作時,關於對本次實施的東西添加自己的錯誤處理進行描述。

bannerAds