t3n / graphql
Neos Flow适配器,用于graphql
Requires
- php: >=7.2
- ext-json: *
- neos/flow: ~7.0 || ^8.0 || dev-master
- t3n/graphql-tools: ~1.0.2
Requires (Dev)
- t3n/coding-standard: ~1.1.0
This package is auto-updated.
Last update: 2024-09-14 11:41:44 UTC
README
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指令计算。