wedrix / watchtower

封装了 graphql-php,用于从基于 Doctrine 的框架(如 Symfony)中提供 GraphQL。

v5.0.11 2024-09-20 04:46 UTC

README

封装了graphql-php,用于从Doctrine框架(如 Symfony)中提供 GraphQL。

目录

特性

  • 首先定义 SDL。
  • 提供开箱即用的分页支持。
  • 支持计算字段、过滤、排序、突变、订阅、授权和通过用户生成插件的自定义解析器。
  • 支持所有类型系统特性,包括枚举、抽象类型(例如联合体和接口)、自定义标量和自定义指令。
  • 基于项目的当前 Doctrine 模型生成和更新查询的模式。
  • 为插件和标量类型定义生成代码。

动机

支持 GraphQL API 通常涉及编写大量冗余样板代码。通过抽象这些样板代码,您可以节省宝贵的开发和维护时间,让您能够专注于 API 的更独特方面。

本库受到为不同平台创建的类似库的启发

要求

  • php >= v8.1
  • doctrine/orm >= v2.8
  • graphql-php >= 14.4

安装

composer require wedrix/watchtower

Symfony

Symfony 扩展包的文档可在此处找到。请查看有关 Symfony 的适当安装步骤。

演示应用

演示应用是用 Symfony 编写的,允许您测试此包的各种功能。文档可在此处找到。

使用方法

本库由两个主要组件组成

  1. 执行器组件 Wedrix\Watchtower\Executor,负责自动解析查询。
  2. 控制台组件 Wedrix\Watchtower\Console,负责代码生成、模式管理和插件管理。

应在某些控制器类或回调函数中使用执行器组件来为您的服务的 GraphQL 端点提供动力。下面的示例用法适用于 Slim 4 应用程序

#index.php

<?php
use App\Doctrine\EntityManager;
use GraphQL\Error\DebugFlag;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Wedrix\Watchtower\Executor;

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

$app = AppFactory::create();

$app->addRoutingMiddleware();

$errorMiddleware = $app->addErrorMiddleware(true, true, true);

$app->post(
 '/graphql.json', 
 function (Request $request, Response $response, $args) {
  /**
  * Instantiating the executor.
  * Pass the entity manager and other config options using DI or 
  * configuration objects. 
  **/
  $executor = new Executor(
   entityManager: $entityManager, // Either as a Singleton or from some DI container
   schemaFile: __DIR__ . '/resources/graphql/schema.graphql',
   pluginsDirectory: __DIR__ . '/resources/graphql/plugins',
   scalarTypeDefinitionsDirectory: __DIR__. '/resources/graphql/scalar_type_definitions',
   optimize: true, // Should be false in dev environment
   cacheDirectory: __DIR__ . '/var/cache'
  );
   
  $response->getBody()
   ->write(
    is_string(
     $responseBody = json_encode(
      /**
      * Call executeQuery() on the request to 
      * generate the GraphQL response.
      **/
      $executor->executeQuery(
       source: ($input = (array) $request->getParsedBody())['query'] ?? '',
       rootValue: [],
       contextValue: [
        'request' => $request, 
        'response' => $response, 
        'args' => $args
       ],
       variableValues: $input['variables'] ?? null,
       operationName: $input['operationName'] ?? null,
       validationRules: null
      )
      ->toArray(
       debug: DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag:INCLUDE_TRACE
      )
     )
    ) 
    ? $responseBody
    : throw new \Exception("Unable to encode GraphQL result")
   );

  return $response->withHeader('Content-Type', 'application/json; charset=utf-8');
});

$app->run();

另一方面,应使用控制台组件在 CLI 工具中提供便利服务,如开发过程中的代码生成。请查看Symfony 扩展包的示例用法。

模式

本库依赖于用模式定义语言(SDL)编写的模式文件来描述服务的类型系统。有关 SDL 的快速入门,请参阅 Hafiz Ismail 的这篇文章

库通过 SDL 支持完整的 GraphQL 类型系统,并能够自动解析 Doctrine 实体和关系,甚至是集合。但是,某些功能要完全功能正常,则需要一些额外的步骤。

自定义标量

为了支持用户定义的标量类型(自定义标量),必须指导 GraphQL 引擎如何解析、验证和序列化该类型的值。这些指令通过标量类型定义提供给了引擎。

标量类型定义

标量类型定义是自动加载的文件,包含相应的函数定义:serialize()parseValue()parseLiteral(),在常规命名空间下,这些定义指导GraphQL引擎如何处理自定义标量值。由于它们是自动加载的,因此标量类型定义必须遵守以下规则:

  1. 标量类型定义必须包含在其自己的脚本文件中。
  2. 脚本文件必须遵循以下命名格式:
    {以snake_case命名的标量类型名称}_type_definition.php
  3. 脚本文件必须包含在Executor和Console组件的scalarTypeDefinitionsDirectory参数指定的目录中。
  4. 相应的函数serialize()parseValue()parseLiteral()必须具有以下函数签名:
/**
 * Serializes an internal value to include in a response.
 */
function serialize(
 mixed $value
): string // You can replace 'mixed' with a more specific type 
{
}

/**
 * Parses an externally provided value (query variable) to use as an input
 */
function parseValue(
 string $value
): mixed // You can replace 'mixed' with a more specific type
{
}

/**
 * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input.
 * 
 * @param array<string,mixed>|null $variables
 */
function parseLiteral(
 \GraphQL\Language\AST\Node $value, 
 ?array $variables = null
): mixed // You can replace 'mixed with a more specific type
{
}
  1. 相应的函数serialize()parseValue()parseLiteral()必须按照以下格式进行命名空间:Wedrix\Watchtower\ScalarTypeDefinition\{以PascalCase命名的标量类型名称}TypeDefinition

以下代码片段是一个自定义DateTime标量类型的示例标量类型定义:

#resources/graphql/scalar_type_definitions/date_time_type_definition.php

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\ScalarTypeDefinition\DateTimeTypeDefinition;

use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Utils\Utils;

function serialize(
 \DateTime $value
): string
{
    return $value->format(\DateTime::ATOM);
}

function parseValue(
 string $value
): \DateTime
{
    try {
        return new \DateTime($value);
    }
    catch (\Exception $e) {
        throw new Error(
            message: "Cannot represent the following value as DateTime: " . Utils::printSafeJson($value),
            previous: $e
        );
    }
}

/**
 * @param array<string,mixed>|null $variables
 */
function parseLiteral(
 Node $value, 
 ?array $variables = null
): \DateTime
{
    if (!$value instanceof StringValueNode) {
        throw new Error(
            message: "Query error: Can only parse strings got: $value->kind",
            nodes: $value
        );
    }

    try {
        return parseValue($value->value);
    }
    catch (\Exception $e) {
        throw new Error(
            message: "Not a valid DateTime Type", 
            nodes: $value,
            previous: $e
        );
    }
}

为了方便快速开发,Console组件提供了一个便捷的方法addScalarTypeDefinition(),可以用来自动生成必要的模板代码。

生成模式

Console组件提供了一个辅助方法generateSchema(),可以用来根据项目的Doctrine模型生成初始的模式文件。

在使用模式生成器时请注意以下事项:

  1. 生成器仅生成查询操作。它不会生成任何变异或订阅操作——这些操作必须手动添加。

  2. 如果不存在,生成器将自动生成自定义类型DateTimePageLimit的标量类型定义。

  3. 生成器只能解析以下Doctrine类型:

  4. 生成器跳过所有具有上述提及类型之外标量类型的字段。您必须手动添加这些字段,并为其提供相应的标量类型定义。

  5. 生成器仅解析实际对应于数据库列的实际字段。所有其他字段必须手动添加,作为计算字段或解析字段。

  6. 生成器无法正确确定内嵌类型和关系的可空性,因此必须手动设置。目前,所有内嵌字段类型默认为可空,所有关系不可为空。

更新模式

Console组件提供了一个辅助方法updateSchema(),可以用来更新模式文件中的查询,以匹配项目的Doctrine模型。更新将与原始模式合并,并不会覆盖标量、变异、订阅、指令等模式定义。

使用多个模式

使用多个模式与实例化Executor和Console组件的不同对象,并配置不同的模式文件一样简单。然后您可以使用它们与适当的控制器、路由、cli脚本等。

查询

查找实体

要查找特定实体,您必须将对应于其实体任何唯一键的参数传递到文档中的相应字段。例如,对于给定的模式

type Query {
    product(id: ID!): Product!
}

type Product {
    id: ID!
    name: String!
    listings: [Listing!]!
}

查询

query {
    product(id: 1) {
        name
    }
}

返回id为1的产品的结果。

此外,对于给定的模式

type Query {
    productLine(product: ID!, order: ID!): ProductLine!
}

type ProductLine {
    product: Product!
    order: Order!
    quantity: Int!
}

type Product {
 id: ID!
 name: String!
}

type Order {
 id: ID!
}

查询

query {
    productLine(product: 1, order: 1) {
        product {
            name
        }
        order {
            id
        }
        quantity
    }
}

返回产品线的结果,该产品线的产品ID为1,订单ID为1

请注意,在上面的示例中,ProductLine的唯一键是由关联productuser组成的复合键。您可以使用任何组合的字段/关联,只要它们组合在一起构成有效的唯一键,就可以作为查找查询的参数。

注意,由于解析器自动将子级字段作为关系相关联,因此查找查询只能由顶级查询字段表示。

关系

此库还能解析您模型的关系。例如,给定以下模式定义

type Query {
    product(id: ID!): Product!
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
    listings: [Listing!]!
}

type Listing {
    id: ID!
    sellingPrice: Float!
}

查询

query {
    product(id: 1) {
        name
        bestSeller
        listings
    }
}

解析id为1的产品,其畅销产品以及由产品实体中bestSellerlistings关联描述的所有相应列表。有关Doctrine关系的更多详细信息,请参阅文档

分页

默认情况下,返回集合关系的完整结果集。要为特定关系启用分页,您只需将queryParams参数传递到文档中的相应字段。例如

type Query {
    product(id: ID!): Product!
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
    listings(queryParams: ListingsQueryParams!): [Listing!]!
}

type Listing {
    id: ID!
    sellingPrice: Float!
}

input ListingsQueryParams {
    limit: Int # items per page
    page: Int # page
}

queryParams指定的类型无关紧要。唯一的要求是它必须定义两个字段limitpage作为整数类型。您还可以选择将它们设为非空,以强制对特定查询字段进行分页。

queryParams也可以用来分页查询结果。例如,给定以下模式

type Query {
    products: [Product!]!
    paginatedProducts(queryParams: ProductsQueryParams!): [Product!]!
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
}

input ProductsQueryParams {
    limit: Int! # items per page
    page: Int! # page
}

查询

query {
    products {
        name
    }
}

返回所有产品的名称,而

query {
    paginatedProducts(queryParams: {page: 1, limit: 5}) {
        name
    }
}

分页结果,只返回前五个元素。

您可以为自己查询字段命名。这也适用于突变和订阅。另一方面,其他类型字段必须对应于实际的实体/可嵌入属性,或者必须有关联的插件来解析它们的值。

此包还支持别名。例如

query {
    queryAlias: paginatedProducts(queryParams: {page: 1, limit: 3}) {
        nameAlias: name
    }
}

返回

{
  "data": {
    "queryAlias": [
      {
        "nameAlias": "IPhone 6S"
      },
      {
        "nameAlias": "Samsung Galaxy Pro"
      },
      {
        "nameAlias": "MacBook Pro"
      }
    ]
  }
}

为了加快开发速度,控制台组件提供了方便的方法来生成更新基于项目Doctrine模型的模式文件。

唯一查询

要返回唯一结果,请将distinct参数添加到queryParams参数中。例如

type Query {
    paginatedProducts(queryParams: ProductsQueryParams!): [Product!]!
}

input ProductsQueryParams {
    distinct: Boolean # Must be boolean
    limit: Int!
    page: Int!
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
}

插件

插件是您定义的特殊自动加载函数,允许您向解析器添加自定义逻辑。由于它们是自动加载的,因此插件必须遵循某些约定以正确发现和使用包

  1. 插件必须包含在其自己的脚本文件中。
  2. 脚本文件名必须与插件的名称对应。
    示例:function apply_listings_ids_filter(...){...}应该对应于apply_listings_ids_filter.php
  3. 脚本文件必须包含在Executor和Console组件的pluginsDirectory参数指定的目录中,在特定插件类型指定的文件夹中(请参阅后续部分以获取更多详细信息)。
  4. 插件函数名称必须遵循特定插件类型的指定命名约定(请参阅后续部分以获取更多详细信息)。
  5. 插件函数签名必须遵循特定插件类型的指定签名(请参阅后续部分以获取更多详细信息)。
  6. 插件功能必须根据特定插件类型的指定约定进行命名空间(有关更多详细信息,请参阅后续章节)。

插件可以实现过滤、排序、计算字段、突变、订阅和授权等功能。以下是一个按给定ID过滤列表的示例过滤器插件。

# resources/graphql/plugins/filters/apply_listings_ids_filter.php

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\FilterPlugin;

use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\QueryBuilder;

function apply_listings_ids_filter(
    QueryBuilder $queryBuilder,
    Node $node
): void
{
    $entityAlias = $queryBuilder->rootAlias();

    $ids = $node->args()['queryParams']['filters']['ids'];

    $idsValueAlias = $queryBuilder->reconciledAlias('idsValue');

    $queryBuilder->andWhere("{$entityAlias}.id IN (:$idsValueAlias)")
                ->setParameter($idsValueAlias, $ids);
}

控制台组件提供了以下方便的方法来生成插件文件:addFilterPlugin()addOrderingPlugin()addSelectorPlugin()addResolverPlugin()addAuthorizorPlugin()addMutationPlugin()addSubscriptionPlugin()

计算字段

有时您的API可能包含与数据库中的实际列不对应的字段。例如,您可能有一个产品实体,它持久化markedPricediscount字段,但使用这两个持久化字段实时计算sellingPrice字段。为了解决这样的字段,您可以使用选择器或解析器插件。

选择器插件

选择器插件允许您将选择语句链接到查询构建器。对于完全可由数据库计算的字段,它们非常有用。下面的代码片段是用于计算销售价格字段的示例选择器插件。

#resources/graphql/plugins/selectors/apply_product_selling_price_selector.php

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\SelectorPlugin;

use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\QueryBuilder;

function apply_product_selling_price_selector(
    QueryBuilder $queryBuilder,
    Node $node
): void
{
    $entityAlias = $queryBuilder->rootAlias();
    
    $queryBuilder->addSelect("
        $entityAlias.markedPrice - $entityAlias.discount AS sellingPrice
    ");
}

规则

选择器插件的规则如下

  1. 插件的脚本文件必须包含在Executor和控制台组件的pluginsDirectory参数指定的目录中,位于selectors子目录下。
  2. 脚本文件的名称必须遵循以下命名格式
    apply_{父类型蛇形名称}_{字段蛇形名称}_selector.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    apply_{父类型蛇形名称}_{字段蛇形名称}_selector
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\QueryBuilder $queryBuilder,
 \Wedrix\Watchtower\Resolver\Node $node
): void;
  1. 插件函数必须在Wedrix\Watchtower\Plugin\SelectorPlugin下命名空间。

有用的工具

第一个函数参数$queryBuilder代表可以链式查询以解决计算字段的查询构建器。它扩展了\Doctrine\ORM\QueryBuilder接口,并添加了以下功能以帮助您构建查询

  1. 使用$queryBuilder->rootAlias()获取查询的根实体别名。
  2. 使用$queryBuilder->reconciledAlias(string $alias)获取与查询中其他别名兼容的别名。使用它来防止名称冲突。

第二个函数参数$node代表在查询图中解决的特定查询节点。使用它来确定应链接到构建器的适当查询。

解析器插件

解析器插件允许您使用数据库中的其他服务来解决字段。与选择器插件不同,它们允许您返回结果,而不是强迫您链式查询到构建器。下面的代码片段是用于货币类型计算汇率字段的示例解析器插件。

#resources/graphql/plugins/resolvers/resolve_currency_exchange_rate_field.php

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\ResolverPlugin;

use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\QueryBuilder;

function resolve_currency_exchange_rate_field(
    Node \$node
): mixed
{
 $exchangeRateResolver = $node->context()['exchangeRateResolver']; // Assuming the service was added to $contextValue when Executor::executeQuery() was called

 return $exchangeRateResolver->getExchangeRate(
  currencyCode: $node->root()['isoCode']
 );
}

规则

解析器插件的规则如下

  1. 插件的脚本文件必须包含在Executor和控制台组件的pluginsDirectory参数指定的目录中,位于resolvers子目录下。
  2. 脚本文件的名称必须遵循以下命名格式
    resolve_{父类型蛇形名称}_{字段蛇形名称}_field.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    resolve_{父类型蛇形名称}_{字段蛇形名称}_field
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\Node $node
): mixed;
  1. 插件函数必须在Wedrix\Watchtower\Plugin\ResolverPlugin下命名空间。

有效的返回类型

请注意,从解析器函数返回的值必须能被库解析。本库能够自动解析以下PHP原始类型:nullintboolfloatstringarray。任何其他返回类型都必须有一个相关的标量类型定义,以便能够被本库解析。表示用户自定义对象类型的值必须以关联数组的形式返回。对于集合,返回一个以0为索引的列表。

解析抽象类型

使用实用函数$node->type()$node->isAbstractType()$node->concreteFieldSelection()$node->abstractFieldSelection()来确定你正在解析的类型:是否为抽象类型,以及选定的具体和抽象字段。

在解析抽象类型时,始终在结果中添加一个__typename字段,指示正在解析的具体类型。例如

function resolve_user(
    Node \$node
): mixed
{
 return [
  '__typename' => 'Customer', // This indicates the concrete type, i.e., Customer
  'name' => 'Sylvester',
  'age' => '20 yrs',
  'total_spent' => '40000' // This probably could only be applicable to the Customer type
 ];
}

抽象类型可以与其他操作类型(如突变和订阅)一起使用。

过滤

本库允许您通过在构建器上链式添加where条件来过滤查询。您可以通过实体属性或关系来过滤查询——只要构建器允许即可。过滤插件用于实现过滤。

过滤插件

过滤插件允许您在查询构建器上链式添加where条件。下面的代码片段是一个示例过滤插件,用于通过给定的ID过滤列表。

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\FilterPlugin;

use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\QueryBuilder;

function apply_listings_ids_filter(
    QueryBuilder $queryBuilder,
    Node $node
): void
{
    $entityAlias = $queryBuilder->rootAlias();

    $ids = $node->args()['queryParams']['filters']['ids'];

    $idsValueAlias = $queryBuilder->reconciledAlias('idsValue');

    $queryBuilder->andWhere("{$entityAlias}.id IN (:$idsValueAlias)")
                ->setParameter($idsValueAlias, $ids);
}

规则

过滤插件的规定如下

  1. 插件的脚本文件必须包含在Executor和Console组件的pluginsDirectory参数指定的目录中,位于filters子文件夹下。
  2. 脚本文件的名称必须遵循以下命名格式
    apply_parent类型的复数蛇形名称_过滤器的蛇形名称_filter.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    apply_parent类型的复数蛇形名称_过滤器的蛇形名称_filter
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\QueryBuilder $queryBuilder,
 \Wedrix\Watchtower\Resolver\Node $node
): void;
  1. 插件函数必须在Wedrix\Watchtower\Plugin\FilterPlugin命名空间下。

要使用过滤插件,请将它们添加到queryParams参数的filters参数中。例如

type Query {
    paginatedProducts(queryParams: ProductsQueryParams!): [Product!]!
}

input ProductsQueryParams {
    filters: ProductsQueryFiltersParam # Can be any user-defined input type
    limit: Int!
    page: Int!
}

input ProductsQueryFiltersParam {
    ids: [String!]
    isStocked: Boolean # Another filter
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
}

然后您可以在查询中使用它们,如下所示

query {
    products: paginatedProducts(queryParams:{filters:{ids:[1,2,3]}}) {
        name
        bestSeller
    }
}

请参阅选择器插件下的有用工具部分,了解使用构建器的有用方法。

排序

本库允许您通过在构建器上链式添加order by语句来对查询进行排序。它还支持多级排序,其中一种排序应用于另一种排序后重新排序匹配元素。要实现排序,请使用排序插件。

排序插件

排序插件允许您在查询构建器上链式添加order by语句。下面的代码片段是一个示例排序插件,用于按最新顺序排序列表。

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\OrderingPlugin;

use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\QueryBuilder;

function apply_listings_newest_ordering(
    QueryBuilder $queryBuilder,
    Node $node
): void
{
    $entityAlias = $queryBuilder->rootAlias();

    $dateCreatedAlias = $queryBuilder->reconciledAlias('dateCreated');

    $queryBuilder->addSelect("$entityAlias.dateCreated AS HIDDEN $dateCreatedAlias")
            ->addOrderBy($dateCreatedAlias, 'DESC');
}

规则

排序插件的规定如下

  1. 插件的脚本文件必须包含在Executor和Console组件的pluginsDirectory参数指定的目录中,位于orderings子文件夹下。
  2. 脚本文件的名称必须遵循以下命名格式
    apply_parent类型的复数蛇形名称_排序的蛇形名称_ordering.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    apply_parent类型的复数蛇形名称_排序的蛇形名称_ordering
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\QueryBuilder $queryBuilder,
 \Wedrix\Watchtower\Resolver\Node $node
): void;
  1. 插件函数必须在Wedrix\Watchtower\Plugin\OrderingPlugin命名空间下。

要使用排序,请将它们添加到queryParams参数的ordering参数中。例如

type Query {
    paginatedProducts(queryParams: ProductsQueryParams!): [Product!]!
}

input ProductsQueryParams {
    ordering: ProductsQueryOrderingParam # Can be any user-defined input type
    limit: Int!
    page: Int!
}

input ProductsQueryOrderingParam {
    closest: ProductsQueryOrderingClosestParam # Can also be any user-defined input type
    oldest: ProductsQueryOrderingOldestParam # Another ordering
}

input ProductsQueryOrderingClosestParam {
    rank: Int! # This parmeter is required for all orderings
    params: ProductsQueryOrderingClosestParamsParam! # This optional for parameterized orderings
}

input ProductsQueryOrderingClosestParamsParam {
    location: Coordinates!
}

input ProductsQueryOrderingOldestParam {
    rank: Int!
}

type Product {
    id: ID!
    name: String!
    bestSeller: Listing
}

然后您可以在查询中使用它们,如下所示

query {
    products: paginatedProducts(
        queryParams:{
            ordering:{
                oldest:{
                    rank:1
                },
                closest:{
                    rank:2,
                    params:{
                        locaton:"40.74684111541018,-73.98518096794233"
                    }
                }
            }
        }
    ) {
        name
        bestSeller
    }
}

请注意,所有排序都需要一个必需的rank参数。它用于确定多个排序的顺序。排名最高的排序首先应用,然后按该顺序应用下一个。

您还可以使用params参数向排序传递参数。

请参阅选择器插件下的有用工具部分,了解使用构建器的有用方法。

突变

变异 是一种不同的操作类型,用于在应用程序中可靠地更改状态。与查询不同,变异保证按顺序运行,防止任何潜在的竞争条件。然而,就像查询一样,它们也可以返回数据图。这个库通过变异插件支持变异。

变异插件

变异插件允许您创建变异来可靠地在应用程序中更改状态。下面的代码片段是一个用于用户登录的示例变异。

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\MutationPlugin;

use App\Server\Sessions\Session;
use Wedrix\Watchtower\Resolver\Node;

function call_log_in_user_mutation(
    Node $node
): mixed
{
    $request = $node->context()['request'] ?? throw new \Exception("Invalid context value! Unset request.");
    $response = $node->context()['response'] ?? throw new \Exception("Invalid context value! Unset response.");

    $session = new Session(
        request: $request,
        response: $response
    );

    $session->login(
        email: $node->args()['email'],
        password: $node->args()['password']
    );

    return $session->toArray();
}

规则

变异插件的规则如下:

  1. 插件的脚本文件必须包含在 Executor 和 Console 组件的 pluginsDirectory 参数指定的目录中,在 mutations 子目录下。
  2. 脚本文件的名称必须遵循以下命名格式
    call_{蛇形命名变异名称}_mutation.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    call_{蛇形命名变异名称}_mutation
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\Node $node
): mixed;
  1. 插件函数必须在 Wedrix\Watchtower\Plugin\MutationPlugin 命名空间下。

有效的返回类型

与 Resolver 插件函数一样,变异函数返回的值必须由库解析。这个库能够自动解析以下原始 PHP 类型: nullintboolfloatstringarray。任何其他返回类型都必须有一个关联的标量类型定义,以便由这个库解析。表示用户定义的对象类型的值必须作为关联数组返回。对于集合,返回一个 0 索引列表。

订阅

订阅 是另一种 GraphQL 操作类型,用于订阅来自服务器的事件流。与查询和变异不同,订阅在较长时间内发送多个结果。因此,它们需要与常规 HTTP 请求流不同的管道。这使得它们的实现高度依赖于超出此库范围的架构选择。尽管如此,该库通过充当连接到底层应用程序实现(用于传输、消息经纪等)的连接器的订阅插件支持订阅。

订阅插件

订阅插件作为连接器连接到您的应用程序的订阅实现。创建订阅插件的规则如下:

规则

  1. 插件的脚本文件必须包含在 Executor 和 Console 组件的 pluginsDirectory 参数指定的目录中,在 subscriptions 子目录下。
  2. 脚本文件的名称必须遵循以下命名格式
    call_{蛇形命名订阅名称}_subscription.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    call_{蛇形命名订阅名称}_subscription
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\Node $node
): mixed;
  1. 插件函数必须在 Wedrix\Watchtower\Plugin\SubscriptionPlugin 命名空间下。

请参阅 GraphQL 规范 了解订阅实现的规范要求。

授权

授权允许您批准结果。这个库根据用户定义的节点/集合类型的规则处理授权。这些规则适用于所有操作类型的结果,包括查询、变异和订阅。您只需编写一次授权,就可以保证它将应用于所有结果。这个库通过授权插件支持授权。

授权插件

授权插件允许您为单个节点/集合类型创建授权。下面的代码片段是一个应用于用户结果的示例授权。

<?php

declare(strict_types=1);

namespace Wedrix\Watchtower\Plugin\AuthorizorPlugin;

use App\Server\Sessions\Session;
use Wedrix\Watchtower\Resolver\Node;
use Wedrix\Watchtower\Resolver\Result;

use function array\any_in_array;

function authorize_customer_result(
 Result $result,
    Node $node
): void
{
    $user = new Session(
  request: $node->context()['request'] ?? throw new \Exception("Invalid context value! Unset request."),
  response: $node->context()['response'] ?? throw new \Exception("Invalid context value! Unset response.")
 )
 ->user();

    if (
        any_in_array(
            needles: \array_keys($node->concreteFieldsSelection()),
            haystack: $user->hiddenFields()
        )
    ) {
        throw new \Exception("Unauthorized! Hidden field requested.");
    }
}

规则

授权插件的规则如下:

  1. 插件的脚本文件必须包含在 Executor 和 Console 组件的 pluginsDirectory 参数指定的目录中,在 authorizors 子目录下。
  2. 脚本文件的名称必须遵循以下命名格式
    authorize_{节点(如果是集合则为复数)的蛇形命名}_result.php
  3. 在脚本文件中,插件函数的名称必须遵循以下命名格式
    authorize_{节点(如果是集合则为复数)的蛇形命名}_result
  4. 插件函数必须具有以下签名
function function_name(
 \Wedrix\Watchtower\Resolver\Result $result,
 \Wedrix\Watchtower\Resolver\Node $node
): void;
  1. 插件函数必须在 Wedrix\Watchtower\Plugin\AuthorizorPlugin 命名空间下。
  2. 当授权失败时,授权插件函数必须抛出异常。

优化

为了优化生产环境中的执行器,将 true 作为 Executor 的 optimize 参数的参数传递,并使用 Console::generateCache() 方法预先生成缓存。
在“优化”模式下运行时,执行器仅依赖于缓存作为模式文件、插件文件和标量类型定义文件的权威来源。
请注意,缓存在运行时永远不会更新,因此必须在之前生成并使用Console::generateCache()与源代码的变化保持同步。

安全

请参考graphql-php手册,了解如何确保GraphQL API的安全性。该库的大多数安全API与这个库兼容,因为它们大多是静态的,允许外部配置。

已知问题

本节详细介绍了与该库使用相关的一些已知问题及其可能的解决方案。

N + 1 问题

此库容易受到N + 1 问题的影响。然而,对于大多数用例,当前数据库解决方案不会造成太大的问题。但是,当使用Resolver插件进行外部API调用时,可能会开始遇到性能问题。对于此类用例,我们建议使用具有查询批处理解决方案(如Dataloader)的异步HTTP客户端来缓解网络延迟瓶颈。

大小写敏感性和命名

规范中所述,GraphQL名称是区分大小写的。然而,由于PHP名称不区分大小写,我们无法遵循此规范要求。请注意,使用此库时使用区分大小写的名称可能会导致不可预测的未定义行为。

别名参数化字段

目前,在 graphql-php 中存在一个开放问题,防止该库正确解析传递不同参数的参数化字段。这可能在 graphql-php 的下一个主要版本中修复。在此期间,请在使用别名时注意此问题。

版本控制

本项目遵循语义版本化2.0.0

预期公开API元素使用@api PHPDoc标记,并在次要版本更改中保证稳定性。所有其他元素均不包含在此向后兼容承诺中,可能在次要或补丁版本之间更改。

请在此查看所有已发布版本。

贡献

对于新功能或建议重大破坏性更改的贡献,请在ideas类别下发起讨论以获取贡献者反馈。

对于涉及错误修复和补丁的小型贡献

  • 分支项目。
  • 进行您的更改。
  • 创建一个Pull Request。

报告漏洞

如果您发现安全漏洞,请通过wedamja@gmail.com向维护者发送电子邮件。安全漏洞将得到及时处理。

许可

这是一个免费和开源软件,根据MIT许可证分发。