chemisus / graphql
Requires
- php: ~7.1
- react/event-loop: ^0.4.3
- react/http-client: ^0.5.6
- webonyx/graphql-php: ^0.11.2
Requires (Dev)
- guzzlehttp/guzzle: ~6.0
- phpunit/phpunit: ^6.4
- squizlabs/php_codesniffer: ^3.1
README
composer require chemisus/graphql
GraphQL
如果你正在寻找一个响应式、两阶段、广度优先搜索(BFS)、GraphQL 库†,那么你找到了!
“两阶段”是什么意思?
查询的执行由两个阶段组成:获取和解析。
获取阶段
获取阶段的主要目标是高效地(在连接和数据传输方面)尝试获取完成查询所需的所有数据。任何获取到的项目都将存储在节点上,然后在子节点获取过程中或节点本身的解析过程中被引用。
获取阶段以 BFS 方式执行。
获取阶段使用 ReactPHP,允许节点返回承诺。如果返回了承诺,子节点将在承诺解决之前不会开始获取。
HTTP
包含了一个非常基础的 HTTP 辅助工具,它提供非阻塞操作,允许节点在其它节点获取数据时处理数据。每个方法都返回一个承诺。
SQL
不幸的是,PDO(以及依赖它的任何库)不支持非阻塞查询。似乎可以使用旧的 mysqli 库与非阻塞查询一起工作。问题 #10 已创建来研究这个问题。
解析阶段
一旦获取阶段完成,解析阶段将组装结果,允许每个节点为最终结果生成或选择其值。
解析阶段以 DFS 方式执行。
虽然解析阶段目前不支持返回承诺或回调,但 问题 #5 正在跟踪进度。
节点
Node
将包含在连接文档时有用的组合信息。
Node::getDocument()
获取文档本身Node::arg(string $key, $default=null)
获取指定的参数,否则获取 $default。Node::args()
获取所有指定的参数。Node::getSelection()
获取所有指定的字段Node::getParent()
获取父节点Node::getItems()
获取为节点获取的项目
文档连接
现在我们知道为什么需要获取和解析阶段,但我们还需要指定它们在应用中实际做什么。这就是连接的作用所在。
文档有四种连接操作,每个操作都可以分为以下两个类别之一:节点和边。
获取器
Document::fetch(Fetcher $fetcher)
向文档添加获取器。获取器是一个边操作,如前所述,它将允许批量获取数据。获取器的返回值应该是一个数组,即使节点类型本身不是列表。获取器返回的项目将可用于该节点的 Node::getItems()
。在执行过程中,对于查询中指定的每个边,将调用一次边的获取器。每次调用获取器时,提供的节点可能具有不同的项目、参数或子节点。
解析器
Document::resolve(Resolver $resolver)
向文档添加解析器。解析器是一个边操作,如前所述,它将确定边的最终结果。
强制转换器
Document::coerce(Coercer $coercer)
向文档添加一个转换器。转换器是一个节点操作,它将节点值转换为 JSON 值。由 Coercer::coerce(Node $node, $value)
返回的值应该是一个符合该节点模式定义的混合值。
如果节点是对象,那么将转换器视为指定多个解析器的好方法。转换器返回的值应该是一个对象,至少包含未定义解析器的字段。对象本身的值不需要转换,因为如果它们在查询中指定,它们将在之后进行转换。
类型器
Document::type(Typer $typer)
向文档添加一个类型器。类型器是一个节点操作,用于确定值的具体类型,如果节点的类型是接口或联合。由 Typer::type(Node $node, $value)
返回的值应该是一个 Type 的实例,可以通过 $node->getDocument()->getType($name)
获取。
连接示例
type Query {
book(id:String!): Book!
books(ids:[String!]!): [Book!]!
}
type Book {
id: String!
title: String!
authorId: String!
author: Person!
}
type Person {
id: String!
name: String!
}
节点:BookWirer
class BookWirer implements Wirer { function wire(Document $document) { $document->coerce('Book', new CallbackCoercer(function (Node $node, Book $value) { return (object)[ 'id' => $book->getId(), 'title' => $book->getTitle(), 'authorId' => $book->getAuthorId(), ]; }); } }
节点:PersonWirer
class PersonWirer implements Wirer { function wire(Document $document) { $document->coerce('Person', new CallbackCoercer(function (Node $node, Person $value) { return (object)[ 'id' => $person->getId(), 'name' => $person->getName(), ]; }); } }
边:QueryBookWirer
class QueryBookWirer implements Wirer { function wire(Document $document) { $document->fetcher('Query', 'book', new CallbackFetcher(function (Node $node) { $ids = $node->arg('id'); return [BookRepository::getBook($id)]; }); $document->resolve('Query', 'book', new CallbackResolver(function (Node $node, $parent, $value) { $books = $node->getItems(); return array_shift($books); }); } }
边:QueryBooksWirer
class QueryBooksWirer implements Wirer { function wire(Document $document) { $document->fetcher('Query', 'books', new CallbackFetcher(function (Node $node) { $ids = $node->arg('ids', []); return count($ids) ? BookRepository::getBooks($ids) : []; }); $document->resolve('Query', 'books', new CallbackResolver(function (Node $node, $parent, $value) { return $node->getItems(); }); } }
边:BookAuthorWirer
class BookAuthorWirer implements Wirer { function wire(Document $document) { $document->fetch('Book', 'author', new CallbackFetcher(function (Node $node) { $mapBookToAuthorId = function (Book $book) { return $book->getAuthorId(); }; $ids = array_map($mapBookToAuthorId, $node->getParent()->getItems()); return count($ids) ? PersonRepository::getPersons($ids) : []; }); $document->resolve('Book', 'author', new CallbackResolver(function (Node $node, Book $book, $value) { $filterAuthorById = function ($authorId) { return function (Person $person) use ($authorId) { return $person->id === $authorId; }; }; $authors = array_filter($node->getParent()->getItems(), $filterAuthorById($book->getAuthorId()) return $node->getItems(); }); } }
文档执行
class GraphQLRunner { /** * @param string $source * @param array $variables * @param Wirer[] $wirers */ public function run ( string $source, array $queryVariables, $wirers ) { // 1. load $documentBuilder = new DocumentBuilder(); $documentBuilder->loadSource($source); $documentBuilder->loadVariables($queryVariables) // 2. build $document = $documentBuilder->buildDocument(); // 3. wire $introspectionWirer = new IntrospectionWirer(); $introspectionWirer->wire($document); foreach($wirers as $wirer) { $wirer->wire($document); } // 4. execute $documentExecutor = new DocumentExecutor(); $data = $documentExecutor->execute($document); return $data; } }
$schemaSource = "type Query { ... }"; $querySource = "query Query { ... }"; $source = implode(PHP_EOL, [$schemaSource, $querySource]); $variables = ['param1' => 'value1']; $wirers = [ new BookWirer(), new PersonWirer(), new QueryBookWirer(), new QueryBooksWirer(), new BookAuthorWirer(), ]; $graphql = new GraphQLRunner(); $data = $graphql->run($source, $variables, $wirers)
扩展
这些扩展默认提供,但以后可能会移动到自己的插件包中。
extend type __Type {
# returns type name with modifications (e.g. "[ID!]!" -> "[ID!]!")
fullName: String!
# returns type name with no modifications (e.g. "[ID!]!" -> "ID")
baseName: String!
}
extend type __Field {
# returns full name of field type (e.g. "[ID!]!" -> "[ID!]!")
typeName: String!
}
要求
- php 7.1
开发和测试
此包可以使用 docker 和 docker-compose 在测试环境中设置 docker 容器。
Docker 环境
ant
将执行运行测试所需的所有操作,包括设置 docker 容器。
主机环境
ant -Dcontained=true
将执行相同的操作,但在主机上。
测试
有两个主要的测试运行器:SchemaTest
和 ErrorsTest
。
SchemaTest
SchemaTest
将加载模式,运行针对该模式的查询,然后将实际结果与预期结果进行比较。
要添加模式,在 ./resources/test/schema/<schemaName>.gql
创建文件。
要为模式添加查询,为查询创建 ./resources/test/schema/<schemaName>/<queryName>.gql
文件,并为结果创建 ./resources/test/schema/<schemaName>/<queryName>.json
文件。
要为模式添加 wirer,在 ./src/test/Wirers/<schemaName>DocumentWirer.php
创建 Wirer 类。
SchemaTest
将在文件创建后检测它们,并自动加载它们。
ErrorsTest
ErrorsTest
用于测试 GraphQL 规范中指定的各种错误。
†: in php, of course