youshido/graphql

纯PHP GraphQL

安装次数: 527 599

依赖: 16

推荐者: 0

安全性: 0

星级: 710

关注者: 36

分支: 106


README

不幸的是,我们不能再支持这个包了,正在寻找有人接管。目前只合并了修复bug且不破坏向后兼容性的PR。我们非常遗憾地承认这一点,但我们希望有人能带领社区将其进一步发展。

如果您有兴趣接管,请联系 @viniychuk。

GraphQL

Join the chat at https://gitter.im/Youshido/GraphQL Latest Stable Version Build Status Scrutinizer Code Quality Code Coverage SensioLabsInsight

这是基于位于 http://facebook.github.io/graphql/ 的官方GraphQL规范工作草案的纯PHP实现。

GraphQL是一种API查询语言。它为客户服务器通信领域带来了新的范式,并为任何请求提供了更加可预测的行为和最小的可能的数据包大小。GraphQL在许多方面都取得了进步,并具有根本的质量改进

  • 强类型通信协议使得客户端和服务器都更加可预测和稳定
  • 鼓励您构建不断发展的API,而不是在端点中使用版本
  • 批量请求数据和响应,以避免多次HTTP握手等待
  • 易于生成文档,以直观的方式探索创建的API
  • 客户端不太可能需要后端更改

当前包将努力保持与官方GraphQL规范最新版本的同步,该规范现在是2016年4月的版本。

可以通过以下链接获取Symfony包 – http://github.com/Youshido/GraphqlBundle

如果您有任何问题或建议,请在 GraphQL Gitter频道 上与我们交谈。

目录

入门

您最好从一些示例开始,"星球大战"成为GraphQL实现的一个"Hello world"。如果您只想要这个 – 您可以通过这个链接获得 – 星球大战示例。另一方面,我们为那些想要逐步掌握的人准备了逐步指南。

安装

使用Composer安装GraphQL包。如果您不熟悉它,您应该查看他们的 手册。运行 composer require youshido/graphql

或者您可以运行以下命令

mkdir graphql-test && cd graphql-test
composer init -n
composer require youshido/graphql

现在您已经准备好创建您的 GraphQL Schema 并检查是否一切正常。您的第一个GraphQL应用将能够接收 currentTime 请求并返回格式化的时间字符串。

您可以在示例目录中找到此示例 – 01_sandbox

创建一个包含以下内容的 index.php 文件

<?php
namespace Sandbox;

use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

require_once 'vendor/autoload.php';

$processor = new Processor(new Schema([
    'query' => new ObjectType([
        'name' => 'RootQueryType',
        'fields' => [
            'currentTime' => [
                'type' => new StringType(),
                'resolve' => function() {
                    return date('Y-m-d H:ia');
                }
            ]
        ]
    ])
]));

$processor->processPayload('{ currentTime }');
echo json_encode($processor->getResponseData()) . "\n";

现在您可以执行 php index.php 并获得包含您当前时间的响应

{
   data: { currentTime: "2016-05-01 19:27pm" }
}

就这样,你创建了一个带有 currentTime 字段,类型为 StringGraphQL Schema 以及对应的 resolver。如果你对这里的 fieldtyperesolver 不太了解,不要担心,随着学习的深入,你会逐渐明白。

如果你遇到任何问题 - 这里有一些故障排除点

  • 请检查你是否安装了最新版本的 composer(composer self-update
  • 确保你的 index.php 文件创建在与你的 vendor 文件夹相同的目录中(可能是在 graphql-test 文件夹中)
  • 最后但同样重要的是,检查你是否安装并运行了 php-cli,并且它的版本 >= 5.5(php -v

此外,你总是可以检查 示例文件夹 中的脚本是否可以正常工作。

教程 - 创建博客 Schema

为了我们的学习示例,我们将为博客构建一个 GraphQL Schema。你可能会使用我们的包以及你喜欢的框架(我们在这里有一个 Symfony 版本 版本),但为了本教程的目的,我们将所有的示例都保持为纯 PHP 代码。

(完整的博客 schema 示例可以通过以下链接获得 https://github.com/Youshido/GraphQL/tree/master/examples/02_blog

我们的博客将会有 Users,他们可以写 Posts 并留下 Comments。此外,还将有一个任何人都可以执行的 LikePost 操作。让我们从 Post 开始。看看返回最新 Post 的 titlesummary 的查询

GraphQL 查询是一种简单的文本查询,结构与 json 格式非常相似。

latestPost {
    title,
    summary
}

服务器应该回复一个相关的 json 响应

{
   data: {
       latestPost: {
           title: "This is a post title",
           summary: "This is a post summary"
       }
   }
}

看起来非常简单直接,所以让我们继续编写可以处理此请求的代码。

创建 Post schema

我们将快速浏览一下你可以用来定义你的 schema 的不同方法。每种方法都有其优缺点,内联方法可能看起来更简单、更快,而面向对象则随着项目的增长提供了更多的灵活性和自由。你绝对应该在每次可以复用你创建的类型时使用面向对象的方法。

我们将创建一个带有单个字段 latestPostRootQueryType。每个 GraphQL Field 都有一个 type(例如 String、Int、Boolean)并且它可以是不同的 kind(例如 Scalar、Enum、List)。你可以在 官方文档 中了解更多信息,但现在你可以把 type 的 field 视为 类的一个实例

内联方法

在你的项目文件夹中创建一个 inline-index.php 文件,并将以下代码粘贴进去

inline-index.php

<?php
namespace InlineSchema;

use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

// including autoloader
require_once __DIR__ . '/vendor/autoload.php';

// instantiating Processor and setting the schema
$processor = new Processor(new Schema([
    'query' => new ObjectType([
        // root query by convention has a name RootQueryType
        'name'   => 'RootQueryType',
        'fields' => [
            'latestPost' => [
                'type'    => new ObjectType([ // Post type is being created as ObjectType
                    'name'    => 'Post', // name of our type – "Post"
                    'fields'  => [
                        'title'   => new StringType(),  // defining "title" field, type - String
                        'summary' => new StringType(),  // defining "summary" field, type - String
                    ],
                ]),
                'resolve' => function () {          // resolver for latestPost field
                    return [                        // for now it returns a static array with data
                        "title"   => "New approach in API has been revealed",
                        "summary" => "In two words - GraphQL Rocks!",
                    ];
                }
            ]
        ]
    ])
]));

// creating payload and running it through processor
$payload = '{ latestPost { title, summary } }';
$processor->processPayload($payload);
// displaying result
echo json_encode($processor->getResponseData()) . "\n";

要检查一切是否正常工作 - 执行 inline-index.php: php inline-index.php 你应该会在 data 部分看到 json 编码的对象 latestPost 的响应

{
   data: {
       latestPost: {
           title: "New approach in API has been revealed",
           summary: "In two words - GraphQL Rocks!"
       }
   }
}

试着通过从请求中删除一个字段或更改 resolve 函数来玩一下代码。

面向对象方法

当你需要在不同的地方使用相同的自定义类型时,这是一个常见的情况,所以我们将为 PostType 创建一个单独的类,并在我们的 GraphQL Schema 中使用它。为了保持结构化,我们将把所有这些类以及未来的类放入 Schema 文件夹中。

创建一个文件 Schema/PostType.php 并将以下代码放入其中

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType   // extending abstract Object type
{

    public function build($config)  // implementing an abstract function where you build your type
    {
        $config
            ->addField('title', new StringType())       // defining "title" field of type String
            ->addField('summary', new StringType());    // defining "summary" field of type String
    }

    public function getName()
    {
        return "Post";  // if you don't do getName – className without "Type" will be used
    }

}

现在让我们创建本例的主要入口点 - index.php

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\PostType;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition

$rootQueryType = new ObjectType([
    'name' => 'RootQueryType',
    'fields' => [
        'latestPost' => [
            'type'    => new PostType(),
            'resolve' => function ($source, $args, $info)
            {
                return [
                    "title"   => "New approach in API has been revealed",
                    "summary" => "In two words - GraphQL Rocks!",
                ];
            }
        ]
    ]
]);

$processor = new Processor(new Schema([
    'query' => $rootQueryType
]));
$payload = '{ latestPost { title, summary } }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

通过运行 php index.php 确保一切正常工作。你应该会看到与内联方法相同的响应。

下一步将是创建一个名为最新帖子字段的独立类,通过扩展AbstractField类:Schema/LatestPostField.php

<?php

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Field\AbstractField;

class LatestPostField extends AbstractField
{
    public function getType()
    {
        return new PostType();
    }

    public function resolve($value, array $args, ResolveInfo $info)
    {
        return [
            "title"   => "New approach in API has been revealed",
            "summary" => "In two words - GraphQL Rocks!",
        ];
    }
}

现在我们可以更新我们的index.php

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\LatestPostField;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition
require_once __DIR__ . '/Schema/LatestPostField.php';

$rootQueryType = new ObjectType([
    'name' => 'RootQueryType',
    'fields' => [
        new LatestPostField()
    ]
]);

$processor = new Processor(new Schema([
    'query' => $rootQueryType
]));
$payload = '{ latestPost { title, summary } }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

为您的项目选择方法

出于几个原因(尤其是对于GraphQL来说最重要的原因),我们建议坚持面向对象的编程方法

  • 使你的Types可重用
  • 添加了使用IDE重构方案的能力
  • 自动完成以帮助你避免拼写错误
  • 当项目增长时,更易于在方案中导航

话虽如此,我们经常使用内联方法来探索和启动想法,或者开发只在一个地方使用的简单字段/解析器。使用内联方法,你可以快速敏捷地创建模拟数据服务器来测试你的前端或移动客户端。

使用有效的名称
我们强烈建议熟悉官方GraphQL规范。记住,GraphQL中有效的标识符应遵循模式/[_A-Za-z][_0-9A-Za-z]*/。这意味着任何标识符都应该由一个拉丁字母、下划线或数字组成,并且不能以数字开头。名称区分大小写

我们将继续致力于博客方案,以探索开发GraphQL服务器的所有基本细节。

查询文档

在GraphQL术语中——查询文档描述了由GraphQL服务接收到的完整请求。它包含操作列表和片段。两者都完全支持我们的PHP库。GraphQL中有两种类型的操作

  • 查询 —— 一个只读请求,不应该对服务器进行任何更改
  • 变异 ——一个请求,它会在服务器上更改(变异)数据,然后获取数据

你已经看到了latestPostcurrentTimeQuery示例,所以让我们定义一个简单的变异,提供喜欢帖子的API。以下是likePost变异的请求和响应示例

请求

mutation {
  likePost(id: 5)
}

响应

{
  data: { likePost: 2 }
}

任何操作都有一个响应类型,在这种情况下,likePost变异的类型是Int

注意,此变异的响应类型是一个标量Int。当然,在现实生活中,你更有可能得到一个类型为Post的响应,但对于上述简单示例,我们将实现代码,并将其保留在index.php

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\LatestPostField;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition
require_once __DIR__ . '/Schema/LatestPostField.php';

$rootQueryType = new ObjectType([
    'name'   => 'RootQueryType',
    'fields' => [
        new LatestPostField()
    ]
]);

$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        // defining likePost mutation field
        'likePost' => [
            // we specify the output type – simple Int, since it doesn't have a structure
            'type'    => new IntType(),
            // we need a post ID and we set it to be required Int
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            // simple resolve function that always returns 2
            'resolve' => function () {
                return 2;
            },
        ]
    ]
]);

$processor = new Processor(new Schema([
    'query'    => $rootQueryType,
    'mutation' => $rootMutationType
]));
$payload   = 'mutation { likePost(id: 5) }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

运行php index.php,你应该看到一个有效的响应

{"data":{"likePost":2}}

现在,让我们让我们的likePost变异返回整个Post作为结果。首先,我们将向PostType添加likesCount字段

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        // you can define fields in a single addFields call instead of chaining multiple addField()
        $config->addFields([
            'title'      => new StringType(),
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }

    // Since our class named by a convention, we can remove getName() method
}

其次,修改LatestPostField中的resolve函数

public function resolve($value, array $args, ResolveInfo $info)
{
    return [
        "title"      => "New approach in API has been revealed",
        "summary"    => "In two words - GraphQL Rocks!",
        "likesCount" => 2
    ];
}

最后,我们将把Mutation TypeIntType更改为PostType,并更新resolve函数以符合新的类型并更新请求

<?php
// ...
$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        'likePost' => [
            'type'    => new PostType(),
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            'resolve' => function () {
                return [
                    'title'     => 'New approach in API has been revealed',
                    'summary'   => 'In two words - GraphQL Rocks!',
                    'likesCount' => 2
                ];
            },
        ]
    ]
]);
// ...
$payload   = 'mutation { likePost(id: 5) { title, likesCount } }';
//...

执行php index.php,你应该在响应中看到titlelikesCount。现在我们可以尝试使用作为参数传递给我们的变异的id: 5

$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        'likePost' => [
            'type'    => new PostType(),
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            'resolve' => function ($source, $args, $resolveInfo) {
                return [
                    'title'      => 'Title for the post #' . $args['id'], // we can be sure that $args['id'] is always set
                    'summary'    => 'In two words - GraphQL Rocks!',
                    'likesCount' => 2
                ];
            },
        ]
    ]
]);

现在你对查询和变异的结构有了一个基本的理解,可以继续了解GraphQL类型系统的细节和GraphQL服务器架构的PHP特定功能。

类型系统

类型是GraphQL方案中定义的原子。每个字段、对象或参数都有一个类型。GraphQL是一种强类型语言。有系统类型和为特定应用程序定义的自定义类型,在我们的应用程序中,我们将有自定义类型PostUserComment等。您的自定义类型通常是在GraphQL系统类型之上构建的。

标量类型

GraphQL标量类型列表

  • Int
  • Float
  • String
  • Boolean
  • Id(按照规范序列化为String)

此外,我们还实现了一些可能有用且我们认为也应该是标量的类型

  • Timestamp
  • DateTimeTz(RFC 2822格式化日期和时间区)

日期和日期时间属性已被弃用并即将移除。我们将提供一个简单的解决方案,帮助您在项目中替换它们。

如果您需要定义新的标量类型,可以通过扩展AbstractScalarType类来实现。

标量类型的用法将与其他类型结合展示。

对象

您的业务逻辑中的每个实体可能都有一个代表其类型的类。这个类必须由AbstractObjectType扩展,或者作为ObjectType的实例创建。在我们的博客示例中,我们使用ObjectType创建了一个内联的PostType,并通过面向对象的方法扩展了AbstractObjectType来创建一个PostType类。

让我们更详细地看看PostType的结构,看看我们可以为每个字段配置哪些参数。

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\BooleanType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        // you can define fields in a single addFields call instead of chaining multiple addField()
        $config->addFields([
            'title'      => [
                'type' => new StringType(),
                'description'       => 'This field contains a post title',
                'isDeprecated'      => true,
                'deprecationReason' => 'field title is now deprecated',
                'args'              => [
                    'truncate' => new BooleanType()
                ],
                'resolve'           => function ($source, $args) {
                    return (!empty($args['truncate'])) ? explode(' ', $source['title'])[0] . '...' : $source['title'];
                }
            ],
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }
}

现在您可以将index.php更改为执行这些请求

$payload   = 'mutation { likePost(id: 5) { title(truncate: true), likesCount } }';

如您所见,我们现在有了用于变异的id参数,以及在PostTitle字段内部用于title字段的另一个参数truncate。我们可以在使用PostType的任何地方使用它。

接口

GraphQL支持接口。您可以定义接口并将其用作列表中项的类型,或者使用接口确保特定的对象确实具有您需要的字段。每个InterfaceType都必须至少有一个定义的字段和一个resolveType函数。该函数将被用于确定GraphQL解析器返回的确切Type。让我们创建一个ContentBlockInterface,它可以代表网页上的一段内容,该内容具有titlesummary字段(就像我们之前的帖子一样)。

<?php
/**
 * ContentBlockInterface.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Scalar\StringType;

class ContentBlockInterface extends AbstractInterfaceType
{
    public function build($config)
    {
        $config->addField('title', new NonNullType(new StringType()));
        $config->addField('summary', new StringType());
    }

    public function resolveType($object) {
        // since there's only one type right now this interface will always resolve PostType
        return new PostType();
    }
}

您通常会只使用build方法来定义需要实现字段。为了将此接口关联到PostType,我们必须重写其getInterfaces方法。

<?php
/**
* PostType.php
*/
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->addFields([
            'title'      => new StringType(),
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

如您所注意到的,接口和类型类中都没有getName方法——这是一个简化的方法,当您希望名称与类名完全相同(不带Type后缀)时可以使用。

如果您现在直接运行脚本(php index.php),应该会得到一个错误。

{"errors":[{"message":"Implementation of ContentBlockInterface is invalid for the field title"}]}

您得到这个错误是因为PostType中的title字段定义与ContentBlockInterface中描述的不同。为了修复它,我们必须声明与接口中存在相同的名称和类型的字段。我们已经有title了,但它是可空的字段,所以我们必须通过添加非空包装器来更改它——new NonNullType(new StringType())。您可以再次执行index.php脚本以检查结果,您应该得到正常的响应。

为了方便起见,我们还创建了$config->applyInterface()方法,它可以在build()中实现。

<?php
/**
 * PostType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->applyInterface(new ContentBlockInterface());
        $config->addFields([
            'likesCount' => new IntType()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

枚举

GraphQL枚举是标量类型的一种变体,它表示预定义值之一。枚举序列化为字符串:表示值的名称,但可以与一个数值(例如)相关联。

为了展示枚举的工作方式,我们将创建一个新的类——PostStatus

<?php
/**
 * PostStatus.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Enum\AbstractEnumType;

class PostStatus extends AbstractEnumType
{
    public function getValues()
    {
        return [
            [
                'value' => 0,
                'name'  => 'DRAFT',
            ],
            [
                'value' => 1,
                'name'  => 'PUBLISHED',
            ]
        ];
    }
}

现在,向PostType中添加一个状态字段

<?php
/**
 * PostType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->addFields([
            'title'      => new NonNullType(new StringType()),
            'summary'    => new StringType(),
            'likesCount' => new IntType(),
            'status'     => new PostStatus()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

并更新最新帖子字段内的解析函数

<?php

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Field\AbstractField;

class LatestPostField extends AbstractField
{
    public function getType()
    {
        return new PostType();
    }

    public function resolve($value, array $args, ResolveInfo $info)
    {
        return [
            "title"      => "New approach in API has been revealed",
            "summary"    => "In two words - GraphQL Rocks!",
            "status"     => 1,
            "likesCount" => 2
        ];
    }
}

在您的查询中请求status字段

$payload  = '{ latestPost { title, status, likesCount } }';

您应该得到类似以下的结果

{"data":{"latestPost":{"title":"New approach in API has been revealed","status":"PUBLISHED"}}}

联合

GraphQL联盟代表一个可以解析为指定GraphQL对象类型之一的对象类型。为了给您一个概念,我们将创建一个新的查询字段,它将返回联盟列表(并在之后到达ListType)。

您可以将联盟视为一个组合类型,通常在您需要有一个不同对象的列表时使用。

想象一下,您有一个页面,您需要获取该页面的所有内容块。内容块可以是PostBanner。创建一个BannerType

<?php
/**
 * BannerType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class BannerType extends AbstractObjectType
{
    public function build($config)
    {
        $config
            ->addField('title', new StringType())
            ->addField('imageLink', new StringType());
    }
}

现在让我们将 Banner 类型与 Post 类型结合起来,创建一个 ContentBlockUnion,它将扩展 AbstractUnionType。每个 UnionType 都需要通过实现 getTypes 方法来定义它联合的类型列表,并通过实现 resolveType 方法来解决将返回给 Union 的每个实例的对象。

<?php
/**
 * ContentBlockUnion.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Union\AbstractUnionType;

class ContentBlockUnion extends AbstractUnionType
{
    public function getTypes()
    {
        return [new PostType(), new BannerType()];
    }

    public function resolveType($object)
    {
        // we simple look if there's a "post" inside the object id that it's a PostType otherwise it's a BannerType
        return empty($object['id']) ? null : (strpos($object['id'], 'post') !== false ? new PostType() : new BannerType());
    }
}

我们还将创建一个简单的 DataProvider,它将为我们提供测试数据以进行操作。

<?php
/**
 * DataProvider.php
 */
namespace Examples\Blog\Schema;

class DataProvider
{
    public static function getPost($id)
    {
        return [
            "id"        => "post-" . $id,
            "title"     => "Post " . $id . " title",
            "summary"   => "This new GraphQL library for PHP works really well",
            "status"    => 1,
            "likesCount" => 2
        ];
    }

    public static function getBanner($id)
    {
        return [
            'id'        => "banner-" . $id,
            'title'     => "Banner " . $id,
            'imageLink' => "banner" . $id . ".jpg"
        ];
    }
}

现在,我们准备更新我们的模式并将 ContentBlockUnion 包含在内。随着我们的模式变得越来越大,我们还想将其提取到一个单独的文件中。

<?php
/**
 * BlogSchema.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Config\Schema\SchemaConfig;
use Youshido\GraphQL\Schema\AbstractSchema;
use Youshido\GraphQL\Type\ListType\ListType;

class BlogSchema extends AbstractSchema
{
    public function build(SchemaConfig $config)
    {
        $config->getQuery()->addFields([
            new LatestPostField(),
            'randomBanner'     => [
                'type'    => new BannerType(),
                'resolve' => function () {
                    return DataProvider::getBanner(rand(1, 10));
                }
            ],
            'pageContentUnion' => [
                'type'    => new ListType(new ContentBlockUnion()),
                'resolve' => function () {
                    return [DataProvider::getPost(1), DataProvider::getBanner(1)];
                }
            ]
        ]);
        $config->getMutation()->addFields([
            new LikePostField()
        ]);
    }

}

拥有这个单独的模式文件后,您应该更新您的 index.php,使其看起来像这样:

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\BlogSchema;
use Youshido\GraphQL\Execution\Processor;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';
require_once __DIR__ . '/Schema/LatestPostField.php';
require_once __DIR__ . '/Schema/ContentBlockInterface.php';
require_once __DIR__ . '/Schema/PostStatus.php';
require_once __DIR__ . '/Schema/LikePostField.php';
require_once __DIR__ . '/Schema/BlogSchema.php';
require_once __DIR__ . '/Schema/ContentBlockUnion.php';
require_once __DIR__ . '/Schema/BannerType.php';
require_once __DIR__ . '/Schema/DataProvider.php';

$processor = new Processor(new BlogSchema());
$payload  = '{ pageContentUnion { ... on Post { title } ... on Banner { title, imageLink } } }';


$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

由于 GraphQL 语法,您必须在联合请求中指定要获取的对象的每种类型的字段,如果您不熟悉它,请参阅官方文档。如果一切正常,您应该看到以下响应。

{"data":{"pageContentUnion":[
  {"title":"Post 1 title"},
  {"title":"Banner 1","imageLink":"banner1.jpg"}
]}}

此外,您可能还想查看如何使用GraphiQL 工具来更好地可视化您在这里所做的事情。

列表

正如您在上一个示例中看到的,ListType 用于创建任何属于或扩展 GraphQL 类型的项目的列表。列表类型还可以通过使用 InterfaceType 作为项目来创建,这为您在定义模式时提供了灵活性。让我们继续并添加 ListType 字段到我们的 BlogSchema。

<?php
/**
 * BlogSchema.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Config\Schema\SchemaConfig;
use Youshido\GraphQL\Schema\AbstractSchema;
use Youshido\GraphQL\Type\ListType\ListType;

class BlogSchema extends AbstractSchema
{
    public function build(SchemaConfig $config)
    {
        $config->getQuery()->addFields([
            new LatestPostField(),
            'randomBanner'     => [
                'type'    => new BannerType(),
                'resolve' => function () {
                    return DataProvider::getBanner(rand(1, 10));
                }
            ],
            'pageContentUnion' => [
                'type'    => new ListType(new ContentBlockUnion()),
                'resolve' => function () {
                    return [DataProvider::getPost(1), DataProvider::getBanner(1)];
                }
            ],
            'pageContentInterface' => [
                'type'    => new ListType(new ContentBlockInterface()),
                'resolve' => function () {
                    return [DataProvider::getPost(2), DataProvider::getBanner(3)];
                }
            ]
        ]);
        $config->getMutation()->addFields([
            new LikePostField()
        ]);
    }

}

我们添加了一个 pageContentInterface 字段,它有一个 ContentBlockInterfaceListType
解析函数返回一个包含一个 Post 和一个 Banner 的列表。为了测试它,我们将修改我们的有效负载如下:

<?php
$payload  = '{ pageContentInterface { title} }';

请注意,由于 BannerType 没有实现 ContentBlockInterface,您将得到一个错误。

{ "errors": [ "message": "Type Banner does not implement ContentBlockInterface" } ]}

为了修复这个问题,我们只需要通过实现 getInterfaces 方法并在我们的 BannerType 中添加适当的字段定义来添加 ContentBlockInterface

<?php
/**
 * BannerType.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Config\TypeConfigInterface;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class BannerType extends AbstractObjectType
{
    public function build($config)
    {
        $config
            ->addField('title', new NonNullType(new StringType()))
            ->addField('summary', new StringType())
            ->addField('imageLink', new StringType());
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

再次发送请求,您将得到一个包含 Post 和 Banner 标题的漂亮响应。

{
  "data": {
    "pageContentInterface":[
      {"title":"Post 2 title"},
      {"title":"Banner 3"}
    ]
  }
}

输入对象

到目前为止,我们主要在不需要您发送任何数据的请求上工作,除了简单的 Int,但在现实生活中,您会有很多需要发送到服务器的各种表单的请求(mutations)——登录、注册、创建帖子等等。为了正确处理和验证这些数据,GraphQL 类型系统提供了一个 InputObjectType 类。

默认情况下,所有 Scalar 类型都是输入,但如果您想要有更复杂的输入类型,您需要扩展 InputObjectType

让我们开发一个 PostInputType,它可以用在我们的系统中创建一个新的帖子。

<?php
/**
 * PostInputType.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Config\InputTypeConfigInterface;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostInputType extends AbstractInputObjectType
{

    public function build($config)
    {
        $config
            ->addField('title', new NonNullType(new StringType()))
            ->addField('summary', new StringType());
    }

}

InputType 可以用于创建新的突变(我们可以在 BlogSchema::build 中进行测试)。

<?php
// BlogSchema->build() method
$config->getMutation()->addFields([
    'likePost'   => new LikePost(),
    'createPost' => [
        'type'   => new PostType(),
        'args' => [
            'post'   => new PostInputType(),
            'author' => new StringType()
        ],
        'resolve' => function($value, array $args, ResolveInfo $info) {
            // code for creating a new post goes here
            // we simple use our DataProvider for now
            $post = DataProvider::getPost(10);
            if (!empty($args['post']['title'])) $post['title'] = $args['post']['title'];
            return $post;
        }
    ]
]);

尝试执行以下突变,以便您可以看到结果。

mutation {
  createPost(author: "Alex", post: {title: "Hey, this is my new post", summary: "my post" }) {
    title
  }
}

结果

{"data":{"createPost":{"title":"Hey, this is my new post"}}}

查看您查询/突变的结果以及检查模式的最有效方法是使用GraphiQL 工具

非空

NonNullType 非常简单易用——将其视为一个包装器,可以确保您的字段/参数是必需的,并且传递给解析函数。我们已经多次使用过 NonNullType,所以我们只向您展示可以调用 NonNullType 对象的有用方法。

  • getNullableType()
  • getNamedType()

这两个方法可以返回被 NonNullType 包装的类型,因此您可以获取其字段、参数或名称。

构建您的架构

尽早通知您任何可能的错误总是一个好主意,尤其是在开发阶段。为此,我们特别创建了许多抽象类,这些类将强制您实现正确的方法以减少错误数量,或者如果您足够幸运——根本没有任何错误。

抽象类型类

如果您想实现一个新类型,请考虑扩展以下类

  • AbstractType
  • AbstractScalarType
  • AbstractObjectType
  • AbstractMutationObjectType
  • AbstractInputObjectType
  • AbstractInterfaceType
  • AbstractEnumType
  • AbstractListType
  • AbstractUnionType
  • AbstractSchemaType

突变辅助类

您可以通过扩展 AbstractObjectType 或在您的 Schema::build 方法内部创建新的 ObjectType 字段来创建一个突变。对于类来说,拥有一个返回实际输出类型的 getType 方法至关重要,但它不能作为抽象方法实现,所以我们创建了一个名为 AbstractMutationObjectType 的包装类。这个抽象类可以帮助您记住 OutputType,因为它会强制您实现一个名为 getOutputType 的方法,这个方法最终将被内部 getType 方法使用。

有用信息

本节将定期更新,包含可能帮助您更快成为一名更好的 GraphQL 开发者的有用链接和参考资料。

GraphiQL 工具

为了进一步提高我们的测试体验,我们建议您开始使用 GraphiQL 客户端,它包含在我们的示例中。这是一个 JavaScript GraphQL 模式探索器。要使用它,请从 examples/02_blog/ 文件夹中运行 server.sh,并在浏览器中打开 examples/GraphiQL/index.html 文件。您将看到一个看起来很不错的编辑器,它具有自动完成功能,并在右侧的文档侧边栏中包含有关您当前模式的所有信息:[点击查看 GraphiQL 界面截图](https://raw.githubusercontent.com/Youshido/GraphQL/master/examples/GraphiQL/screenshot.png)。