chemisus/graphql

本包最新版(1.2)没有提供许可证信息。

1.2 2017-11-16 21:39 UTC

This package is not auto-updated.

Last update: 2024-09-15 05:49:44 UTC


README

Build Status Coverage Status

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

开发和测试

此包可以使用 dockerdocker-compose 在测试环境中设置 docker 容器。

Docker 环境

ant 将执行运行测试所需的所有操作,包括设置 docker 容器。

主机环境

ant -Dcontained=true 将执行相同的操作,但在主机上。

测试

有两个主要的测试运行器:SchemaTestErrorsTest

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