t3n / graphql

此软件包最新版本(3.1.0)的许可证信息不可用。

Neos Flow适配器,用于graphql

维护者

详细信息

github.com/t3n/graphql

源代码

问题

安装量: 79,213

依赖项: 4

建议者: 0

安全: 0

星标: 15

关注者: 12

分支: 8

开放问题: 5

类型:neos-package

3.1.0 2022-05-05 10:16 UTC

README

CircleCI Latest Stable Version Total Downloads

t3n.GraphQL

此软件包将GraphQL API添加到Neos和Flow,并支持高级功能,如schema拼接、验证规则、schema指令等。此软件包不提供用于测试API的GraphQL客户端。我们建议使用GraphlQL Playground

通过composer安装此软件包

composer require "t3n/graphql"

版本2.x支持neos/flow >= 6.0.0

配置

为了使用您的GraphQL API端点,需要进行一些配置。

端点

假设API应通过以下URL访问: http://localhost/api/my-endpoint

为了实现这一点,您首先需要在您的 Routes.yaml 中添加路由

- name: 'GraphQL API'
  uriPattern: 'api/<GraphQLSubroutes>'
  subRoutes:
    'GraphQLSubroutes':
      package: 't3n.GraphQL'
      variables:
        'endpoint': 'my-endpoint'

别忘了加载所有路由

Neos:
  Flow:
    mvc:
      routes:
        'Your.Package':
          position: 'start'

现在路由已激活并可用。

Schema

下一步是定义一个可查询的schema。

创建一个 schema.graphql 文件

/Your.Package/Resources/Private/GraphQL/schema.root.graphql

type Query {
  ping: String!
}

type Mutation {
  pong: String!
}

schema {
  query: Query
  mutation: Mutation
}

底层我们使用 t3n/graphql-tools。此软件包是从Apollos graphql-tools的PHP端口。这使得您可以使用一些高级功能,如schema拼接。因此,您可以针对每个端点配置多个schema。所有schema将在内部合并为一个schema。

按如下方式添加schema到您的端点

t3n:
  GraphQL:
    endpoints:
      'my-endpoint': # use your endpoint variable here
        schemas:
          root: # use any key you like here
            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.root.graphql'

要添加另一个schema,只需在端点的 schemas 索引下添加一个新条目。

您还可以使用扩展功能

/Your.Package/Resources/Private/GraphQL/schema.yeah.graphql

extend type Query {
  yippie: String!
}
t3n:
  GraphQL:
    endpoints:
      'my-endpoint': #
        schemas:
          yeah:
            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.yeah.graphql'

解析器

现在您需要添加一些解析器。您可以为每个类型添加解析器。给定此schema

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

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

您可能希望为这两种类型配置解析器

t3n:
  GraphQL:
    endpoints:
      'my-endpoint':
        schemas:
          mySchema:
            resolvers:
              Query: 'Your\Package\GraphQL\Resolver\QueryResolver'
              Product: 'Your\Package\GraphQL\Resolver\ProductResolver'

每个解析器都必须实现 t3n\GraphQL\ResolverInterface !

您还可以动态添加解析器,这样您就不必分别配置每个解析器

t3n:
  GraphQL:
    endpoints:
      'my-endpoint':
        schemas:
          mySchema:
            resolverPathPattern: 'Your\Package\GraphQL\Resolver\Type\{Type}Resolver'
            resolvers:
              Query: 'Your\Package\GraphQL\Resolver\QueryResolver'

使用此配置,类 Your\Package\GraphQL\Resolver\Type\ProductResolver 将负责Product类型的查询。{Type} 将评估为您的类型名称。

作为第三种选择,您可以编程创建解析器。因此,您可以注册一个实现 t3n\GraphQL\ResolverGeneratorInterface 的类。这可能对自动生成解析器映射很有用

t3n:
  GraphQL:
    endpoints:
      'my-endpoint':
        schemas:
          mySchema:
            resolverGenerator: 'Your\Package\GraphQL\Resolver\ResolverGenerator'

生成器必须返回一个具有此结构的数组:['typeName' => \Resolver\Class\Name]

☝️ 注意:您的解析器可以相互覆盖。所有解析器配置都按此顺序应用

  • 解析器生成器
  • 动态的 "{Type}Resolver"
  • 特定的解析器

解析器实现

我们的示例实现可能如下(伪代码)

<?php

namespace Your\Package\GraphQL\Resolver;

use Neos\Flow\Annotations as Flow;
use t3n\GraphQL\ResolverInterface;

class QueryResolver implements ResolverInterface
{
    protected $someServiceToFetchProducts;

    public function products($_, $variables): array
    {
        // return an array with products
        return $this->someServiceToFetchProducts->findAll();
    }

    public function product($_, $variables): ?Product
    {
        $id = $variables['id'];
        return $this->someServiceToFetchProducts->getProductById($id);
    }
}
<?php

namespace Your\Package\GraphQL\Resolver\Type;

use Neos\Flow\Annotations as Flow;
use t3n\GraphQL\ResolverInterface;

class ProductResolver implements ResolverInterface
{
    public function name(Product $product): array
    {
        // this is just an overload example
        return $product->getName();
    }
}

一个示例查询如下

query {
  products {
    id
    name
    price
  }
}

首先会调用QueryResolver,并调用products()方法。此方法返回一个包含Product对象的数组。对于每个对象,都会使用ProductResolver。为了获取实际值,存在一个DefaultFieldResolver。如果您没有配置与请求属性同名的命名方法,则将使用它来获取值。DefaultFieldResolver会尝试通过ObjectAccess::getProperty($source, $fieldName)自行获取数据。因此,如果您的Product对象具有getName(),则将使用它。您仍然可以像示例中那样覆盖实现。

所有解析器方法具有相同的签名

method($source, $args, $context, $info)

接口类型

当与接口一起工作时,您还需要为您的接口配置解析器。给定此架构

interface Person {
    firstName: String!
    lastName: String!
}

type Customer implements Person {
    firstName: String!
    lastName: String!
    email: String!
}

type Supplier implements Person {
    firstName: String!
    lastName: String!
    phone: String
}

您需要配置Person解析器以及具体实现的解析器。

虽然具体类型解析器不需要特别注意,但Person解析器实现了返回类型名称的__resolveType函数。

<?php

namespace Your\Package\GraphQL\Resolver\Type;

use t3n\GraphQL\ResolverInterface;

class PersonResolver implements ResolverInterface
{
    public function __resolveType($source): ?string
    {
        if ($source instanceof Customer) {
            return 'Customer';
        } elseif ($source instanceof Supplier) {
            return 'Supplier';
        }
        return null;
    }
}

联合类型

联合的解析方式与接口相同。对于上面的示例,相应的架构如下所示

union Person = Customer | Supplier

它再次使用实现__resolveType函数的PersonResolver进行解析。

枚举类型

枚举的解析是自动的。解析器方法简单地返回一个与枚举值匹配的字符串。对于架构

enum Status {
  ACTIVE
  INACTIVE
}

type Customer {
    status: Status!
}

CustomerResolver方法将值作为字符串返回

public function status(Customer $customer): string
{
    return $customer->isActive() ? 'ACTIVE' : 'INACTIVE';
}

标量类型

Json树的最叶类型由标量定义。自定义标量由实现serialize()parseLiteral()parseValue()函数的解析器实现。

此示例显示了DateTime标量类型的实现。对于给定的架构定义

scalar DateTime

DateTimeResolver在处理Unix时间戳时的样子如下

<?php

namespace Your\Package\GraphQL\Resolver\Type;

use DateTime;
use GraphQL\Language\AST\IntValueNode;
use GraphQL\Language\AST\Node;
use t3n\GraphQL\ResolverInterface;

class DateTimeResolver implements ResolverInterface
{
    public function serialize(DateTime $value): ?int
    {
        return $value->getTimestamp();
    }

    public function parseLiteral(Node $ast): ?DateTime
    {
        if ($ast instanceof IntValueNode) {
            $dateTime = new DateTime();
            $dateTime->setTimestamp((int)$ast->value);
            return $dateTime;
        }
        return null;
    }

    public function parseValue(int $value): DateTime
    {
        $dateTime = new DateTime();
        $dateTime->setTimestamp($value);
        return $dateTime;
    }
}

您必须通过Settings.yaml中的配置选项之一再次使DateTimeResolver可用。

上下文

您的Resolver方法签名中的第三个参数是上下文。默认情况下,它设置为t3n\GraphQContext,该上下文公开当前请求。

设置每个端点的上下文很容易。这可能有助于在所有Resolver实现之间共享一些代码或对象。请确保扩展t3n\GraphQContext

假设我们有一个针对购物篮的graphql端点(简化版)

type Query {
  basket: Basket
}

type Mutation {
  addItem(item: BasketItemInput): Basket
}

type Basket {
  items: [BasketItem]
  amount: Float!
}

type BasketItem {
  id: ID!
  name: String!
  price: Float!
}

input BasketItemInput {
  name: String!
  price: Float!
}

首先,为您的购物端点配置上下文

t3n:
  GraphQL:
    endpoints:
      'shop':
        context: 'Your\Package\GraphQL\ShoppingBasketContext'
        schemas:
          basket:
            typeDefs: 'resource://Your.Package/Private/GraphQL/schema.graphql'
            resolverPathPattern: 'Your\Package\GraphQL\Resolver\Type\{Type}Resolver'
            resolvers:
              Query: 'Your\Package\GraphQL\Resolver\QueryResolver'
              Mutation: 'Your\Package\GraphQL\Resolver\MutationResolver'

对于这种场景,上下文将注入当前的购物篮(可能是流程会话范围);

<?php

declare(strict_types=1);

namespace Your\Package\GraphQL;

use Neos\Flow\Annotations as Flow;
use Your\Package\Shop\Basket;
use t3n\GraphQL\Context as BaseContext;

class ShoppingBasketContext extends BaseContext
{
    /**
     * @Flow\Inject
     *
     * @var Basket
     */
    protected $basket;

    public function getBasket()
    {
        return $this->basket;
    }
}

以及相应的解析器类

<?php

namespace Your\Package\GraphQL\Resolver;

use Neos\Flow\Annotations as Flow;
use t3n\GraphQL\ResolverInterface;

class QueryResolver implements ResolverInterface
{
    protected $someServiceToFetchProducts;

    // Note the resolver method signature. The context is available as third param
    public function basket($_, $variables, ShoppingBasketContext $context): array
    {
        return $context->getBasket();
    }
}
<?php

namespace Your\Package\GraphQL\Resolver;

use Neos\Flow\Annotations as Flow;
use t3n\GraphQL\ResolverInterface;

class MutationResolver implements ResolverInterface
{
    protected $someServiceToFetchProducts;

    public function addItem($_, $variables, ShoppingBasketContext $context): Basket
    {
        // construct your item with the input (simplified, don't forget validation etc.)
        $item = new BasketItem();
        $item->setName($variables['name']);
        $item->setPrice($variables['price']);

        $basket = $context->getBasket();
        $basket->addItem($item);

        return $basket;
    }
}

记录传入的请求

您可以为每个端点启用传入请求的记录

t3n:
  GraphQL:
    endpoints:
      'your-endpoint':
        logRequests: true

一旦激活,所有传入的请求都将记录到Data/Logs/GraphQLRequests.log。每个日志条目将包含端点、查询和变量。

保护您的端点

要保护API端点,您有多种选择。最简单的方法是为您的解析器配置一些权限。

privilegeTargets:
  'Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege':
    'Your.Package:Queries':
      matcher: 'method(public Your\Package\GraphQL\Resolver\QueryResolver->.*())'
    'Your.Package:Mutations':
      matcher: 'method(public Your\Package\GraphQL\Resolver\MutationResolver->.*())'

roles:
  'Your.Package:SomeRole':
    privileges:
      - privilegeTarget: 'Your.Package:Queries'
        permission: GRANT
      - privilegeTarget: 'Your.Package:Mutations'
        permission: GRANT

您还可以使用自定义上下文来访问当前登录用户。

架构指令

默认情况下,此包提供三个指令

  • AuthDirective
  • CachedDirective
  • CostDirective

要启用这些指令,请将此配置添加到您的端点

t3n:
  GraphQL:
    endpoints:
      'your-endpoint':
        schemas:
          root: # use any key you like here
            typeDefs: 'resource://t3n.GraphQL/Private/GraphQL/schema.root.graphql'
        schemaDirectives:
          auth: 't3n\GraphQL\Directive\AuthDirective'
          cached: 't3n\GraphQL\Directive\CachedDirective'
          cost: 't3n\GraphQL\Directive\CostDirective'

AuthDirective

AuthDirective将检查安全上下文以获取当前已验证的角色。这使您能够保护具有给定角色的用户的对象或字段。

使用它来允许编辑器更新产品,但仅允许管理员删除

type Mutation {
    updateProduct(): Product @auth(required: "Neos.Neos:Editor")
    removeProduct(): Boolean @auth(required: "Neos.Neos:Administrator")
}

CachedDirective

缓存始终是一个问题。一些查询可能很难解析,因此缓存结果是有价值的。因此,您应该使用CachedDirective

type Query {
  getProduct(id: ID!): Product @cached(maxAge: 100, tags: ["some-tag", "another-tag"])
}

缓存的指令将使用流缓存 t3n_GraphQL_Resolve 作为后端。该指令接受最大年龄参数以及标签。查看流缓存文档了解它们!缓存条目标识符将尊重所有参数(例如此示例中的id)以及查询路径。

CostDirective

Cost指令将为您的字段和对象添加一个复杂性函数,该函数由某些验证规则使用。每个类型及其子项都有一个默认的复杂性值1。它允许您像这样注解成本值和乘数

type Product @cost(complexity: 5) {
  name: String! @cost(complexity: 3)
  price: Float!
}

type Query {
  products(limit: Int!): [Product!]! @cost(multipliers: ["limit"])
}

如果您查询 produts(limit: 3) { name, price },查询将具有以下成本

每个产品9个(产品本身5个,获取名称3个,价格1个,默认复杂性)乘以3,因为我们定义了限制值作为乘数。因此,查询的总复杂性为27。

验证规则

您可以为每个端点启用几个验证规则。最常见的是查询深度以及查询复杂性规则。配置您的端点以启用这些规则

t3n:
  GraphQL:
    endpoints:
      'some-endpoint':
        validationRules:
          depth:
            className: 'GraphQL\Validator\Rules\QueryDepth'
            arguments:
              maxDepth: 11
          complexity:
            className: 'GraphQL\Validator\Rules\QueryComplexity'
            arguments:
              maxQueryComplexity: 1000

maxQueryComplexitiy 通过Cost指令计算。