99designs/graphql

纯PHP GraphQL

v1.0.1 2023-11-01 06:27 UTC

This package is auto-updated.

Last update: 2024-08-30 01:55:24 UTC


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的查询语言。它为客户端-服务器通信带来了新的范式,并为任何请求提供了更可预测的行为和最小的可能的数据传输。

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

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

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

如果您有任何问题或建议,请在我们 GraphQL Gitter频道 上进行讨论。

目录

入门

您应该从一些示例开始,"Star Wars" 成为了GraphQL实现的一个"Hello world"。如果您只是想找到这个 – 您可以通过这个链接获得 – Star Wars示例。另一方面,我们为那些希望逐步提高速度的人准备了一份逐步指南。

安装

使用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" }
}

就这样,你已经创建了一个包含类型为 StringcurrentTime 字段和相应 resolverGraphQL Schema。如果你不知道这里的 fieldtyperesolver 是什么,请不要担心,你会在学习过程中了解它们。

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

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

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

教程 - 创建博客模式

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

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

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

GraphQL 查询是一个简单的文本查询,其结构非常类似于 JSON 格式。

latestPost {
    title,
    summary
}

假设服务器应该回复一个相关的 JSON 响应

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

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

创建 Post 模式

我们将快速查看你可以用来定义你的模式的不同方法。每种方法都有其优点和缺点,内联方法可能看起来更容易更快,而面向对象的方法随着项目的增长提供了更多的灵活性和自由度。你应该在每次可以重用你创建的类型时使用面向对象的方法。

我们将创建一个具有一个字段 latestPostRootQueryType。每个 GraphQL Field 都有一个 type(例如,String、Int、Boolean)并且它可以有不同类型的 kind(例如,Scalar、Enum、List)。你可以在 官方文档 中了解更多信息,但就现在而言,你可以将 type 的字段 类似于 类的实例

内联方法

在你的项目文件夹中创建一个 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]*/。这意味着任何标识符都应包含拉丁字母、下划线或数字,并且不能以数字开头。名称是区分大小写的

我们将继续对Blog Schema进行工作,以探索开发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格式带时区的日期)

Date和DateTime已被弃用并将被移除。我们将提供一个简单的方法来替换它们在你的项目中

如果你需要定义一个新的标量类型,可以通过扩展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,以及用于PostTitletitle字段的另一个参数truncate。我们可以在使用PostType的任何地方使用它。

接口

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

<?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"}]}

你得到了这个错误,因为PostTypetitle字段的定义与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)。

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

想象一下,您有一个页面,需要获取该页面的所有内容块。内容块可以是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"
        ];
    }
}

现在,我们准备好更新我们的Schema,并将ContentBlockUnion包含在内。随着我们的Schema越来越大,我们希望将其提取到单独的文件中。

<?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()
        ]);
    }

}

拥有这个单独的Schema文件后,您应该更新您的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作为项目来创建,这为您定义Schema提供了灵活性。让我们继续给我们的BlogSchema添加ListType字段。

<?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"}}}

查看您的查询/突变的结果以及检查Schema的最佳方式是使用GraphiQL工具

非空

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

  • getNullableType()
  • getNamedType()

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

构建您的模式

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

抽象类型类

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

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

突变帮助类

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

有用信息

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

GraphiQL 工具

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