liquiddesign/lqgraphi

为 Liquid Design 生态系统提供的 GraphQL API 库


README

GraphQL API 库,用于 Liquid Design 生态系统。

功能

  • 从 PHP 类自动创建到 TypeRegister(Storm 实体)的类型
  • 从命名空间自动加载查询、变异和类型
  • 缓存模式、持久查询和变异
  • 通用的 CRUD 查询、变异和解析器,用于通用生成和解析
  • 基于请求查询从数据库递归获取 Storm 实体的数据检索器(高度优化 - 每个实体类只执行一个查询 - 模拟数据加载器)

推荐

此包与扩展包配合使用效果良好,这些扩展包为 LQD 包提供了类型

安装

此包需要 PHP 8.2 或更高版本。

composer require liquiddesign/lqgraphi

配置

extensions:
    typeRegister: LqGrAphi\LqGrAphiDI

typeRegister:
    resolvers: 
        - EshopApi\Resolvers
    queriesAndMutations:
        - EshopApi\Schema\Types
    types:
        output:
            - EshopApi\Schema\Outputs
        input:
            - EshopApi\Schema\Inputs

入口点

在您的入口点(可能是 index.php)中,您需要调用处理程序。您只需创建容器并将其传递给 \LqGrAphi\Handlers\IndexHandler::handle

index.php 的最小示例

require __DIR__ . '/vendor/autoload.php';

\LqGrAphi\Handlers\IndexHandler::handle(\EshopApi\Bootstrap::boot()->createContainer());

沙盒

默认启用 Apollo 沙盒,用于基于环境文件的调试连接。

您可以永久禁用它

require __DIR__ . '/vendor/autoload.php';

\LqGrAphi\Handlers\IndexHandler::handle(\EshopApi\Bootstrap::boot()->createContainer(), false);

查询和变异

查询和变异的位置通过配置 queryAndMutationsNamespace 设置。查询需要扩展 \LqGrAphi\Schema\BaseQuery,变异扩展 \LqGrAphi\Schema\BaseMutation

这些类型仅在首次创建模式时自动加载并缓存。所有其他请求都使用缓存的模式,因此脚本不需要为每个请求创建模式,性能不会降低。这种方法有一些限制:查询和变异未在容器中注册,因此您不能使用 DI。所有这些类都将接收容器作为第一个参数。

类型

类型的位置通过配置 types 设置。您需要在此处指定所有使用的输入和输出。

typeRegister:
    types:
        input:
            product: EshopApi\Schema\Inputs\ProductInput
        output:
            product: EshopApi\Schema\Outputs\ProductOutput

有关更多信息,请访问 webonyx/graphql-php 库的文档。

ClassOutput

存在一个具有 getClass 方法的接口 \LqGrAphi\Schema\ClassOutput。如果您使用它,TypeRegister 将保存此映射,当您调用 getOutputType 时,您只需传递类字符串而不是名称。

ClassInput

ClassOutput 相同,用于输入。

关系

在输出中,关系是对象或对象列表,深度可达 10 级。另一方面,输入始终有两个用于关系的字段。

一个带有后缀 ID 的字段用于单个关系,它接受字符串(如果可能则为 null)。对于多关系,有一个带有后缀 IDs 的字段,它是一个对象,具有 addremovereplace 字段。这些字段接受字符串列表。

其次,始终有带有后缀 OBJ 的字段,它直接接受输入对象并更新它。它也可以有 10 级深度。对于多关系,有一个带有后缀 OBJs 的字段,它接受输入对象列表。

由于 GraphQL 的限制,您无法使用联合输入类型,因此这些输入始终是 UpdateInput 变体,其中所有字段都是可选的。根据 ID 字段,对象被创建或仅更新。这种方法在创建对象时丢失了必需字段的类型安全,在这种情况下,错误仅在运行时引发。

input Object {
    fullName: String
    accountsIDs: SubObjectIDs
    accountsOBJs: [SubObjectUpdateInput]
}

存在一个名为 \LqGrAphi\Schema\ClassInput 的接口,其中包含 getClass 方法。如果您使用它,TypeRegister 将保存此映射,当您调用 getInputType 时,您可以直接传递类字符串而不是配置中的名称。此外,TypeRegister 还将此输入映射到输入对象的关联字段。

解析器

由于整个模式创建的缓存,解析器与模式隔离,并必须自行解析请求。
存在简单的路由机制。GraphQL 的查询和突变名称使用小驼峰命名法。
名称被解析为解析器类名称中的第一个单词,其余为函数名称。
例如:productGetMany 被解析为 ProductResolver 和函数 getMany

解析器名称到解析器和函数的路由器使用按需缓存。

每个解析器函数的签名必须是

/**
 * @param array<mixed> $rootValue
 * @param array<mixed> $args
 * @param \LqGrAphi\GraphQLContext $context
 * @param \GraphQL\Type\Definition\ResolveInfo|array<mixed> $resolveInfo
 * @return array<mixed>|null
 * @throws \LqGrAphi\Resolvers\Exceptions\BadRequestException
 * @throws \ReflectionException
 * @throws \StORM\Exception\GeneralException
 */
public function getMany(array $rootValue, array $args, GraphQLContext $context, ResolveInfo|array $resolveInfo): ?array
{
    ...
}

推荐配置方式

services:
	graphql_resolvers:
		in: %appDir%
		files: [Resolvers/*Resolver.php, Resolvers/*/*Resolver.php]
		implements:
		    - LqGrAphi\Resolvers\BaseResolver

缓存

处理程序使用缓存来记住查询和突变。如果您发送相同的查询两次,它将从缓存中解析并直接传递给解析器。这种方法显着提高了性能。从解析器返回的数据仅与第一个请求进行验证。当解析器未完全测试时,这种方法不安全。

您还可以直接传递 queryId 而不是查询。查询通过 md5 进行散列,因此您可以仅对查询进行 md5 散列并将其作为查询Id发送。但请记住,您仍然需要发送至少一个带有查询的请求来验证它。

CRUD

您可以自己编写类型和查询,但大多数时候,您只想使用现有实体并对其进行 CRUD 操作。

此时出现了 \LqGrAphi\Schema\CrudQuery\LqGrAphi\Schema\CrudMutation

首先,创建查询类

class CustomerQuery extends CrudQuery
{
	public function getClass(): string
	{
		return Customer::class;
	}
}

然后创建输出、创建和更新类型,使用助手

class CustomerOutput extends BaseOutput
{
	public function __construct(TypeRegister $typeRegister)
	{
		$config = [
			'fields' => $typeRegister->createOutputFieldsFromClass(Customer::class, exclude: ['account']),
		];

		parent::__construct($config);
	}
}
class CustomerCreateInput extends BaseInput
{
	public function __construct(TypeRegister $typeRegister)
	{
		$config = [
			'fields' => $typeRegister->createInputFieldsFromClass(Customer::class, includeId: false),
		];

		parent::__construct($config);
	}
}
class CustomerUpdateInput extends BaseInput
{
	public function __construct(TypeRegister $typeRegister)
	{
		$config = [
			'fields' => $typeRegister->createInputFieldsFromClass(Customer::class, forceAllOptional: true),
		];

		parent::__construct($config);
	}
}

在配置中注册您的类型命名空间,最后创建解析器

class CustomerResolver extends CrudResolver
{
	public function getClass(): string
	{
		return Customer::class;
	}
}

就这样,您可以查询一个、多个或集合,并执行创建、更新和删除操作。

CRUD 辅助函数

为了帮助处理 API,有一些自动改进

  • 所有数组输出都被封装在具有键 dataonPageCount 的对象中
  • 存在用于排序、分页和过滤的通用输入对象

过滤

过滤器是类型为 JSON 的输入字段,它们被解析为存储库允许的 filter 函数。因此,过滤器是动态类型化的。

获取结果

存在通用的获取函数。您只需传递带有 ResolveInfo 的集合。它将负责以最有效的方式检索您请求的数据。

语言突变

如果您需要使用不同的语言突变,可以使用 HTTP 头部 "Accept-Language"。系统将检测到的语言(如果受支持)设置为所有 SQL 查询的主要语言。如果 "Accept-Language" 头部中没有受支持的语言,则使用设置中的主要语言。

路线图

2023

  • ✅ 带缓存的持久查询
  • ❗新的类型加载系统以提供更好的 DX
    • 无需注册类型
    • 从命名空间加载类型
    • 重构 TypeRegister
  • 安全 - 防护、登录
  • 自动测试

开发者信息

项目使用 PHPStan(级别 8)和 PHP-CS-Fixer 进行代码质量检查。