用PHP尝试GraphQL
最近似乎逐渐增加了招聘数量?这是要通过使用PHP来学习GraphQL的意图。
太长不看。
我调查了一下GraphQL,并且用PHP编写了一个支持GraphQL的服务器应用程序。我使用了graphql-php这个库,但在用PHP表示模式以及进行类型检查方面遇到了相当困难。
由于GraphQL不依赖于特定的语言规范,所以将来可能会自动生成PHP端的模式定义。
可以说,GraphQL是一种规范,要求客户端强制服务器返回哪些数据。因此,如果要支持GraphQL,则服务器端可能会变得更复杂,或者失去灵活性。
GraphQL是一种查询语言。
GraphQL是用于API的查询语言。如果将SQL用作从数据库检索数据的查询语言,那么GraphQL就是用于从API检索JSON数据的查询语言。
我要试试GraphQL。
在GraphQL中,当定义模式(定义类型和类型的字段)时,它将直接成为API的规范。
对于以下的模式定义
type Query {
me: User
}
type User {
id: ID
name: String
}
如果提出以下类似的请求
{
me {
name
}
}
会收到这样的回复。
{
"me": {
"name": "Luke Skywalker"
}
}
由于请求和响应的格式非常相似,所以很容易理解。在这里,用户的属性有id和name两个,但只返回name是因为请求中只指定了name。
GraphQL的优点
GraphQL的好处有很多。
-
- 可以在一次请求中获取多个资源。
-
- 可以从一个端点访问所有数据。
-
- 由于存在资源的定义,可以使用类似类型的东西。
-
- 可以在字段级控制要获取的资源。
- 以类似JSON的形式直观易懂。
我想你可以阅读这篇文章来了解更详细的内容:https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad
此外,你还可以直接参考官方网站:http://graphql.org/learn/
让我们尝试编写一个接收GraphQL的服务器。
假设有一种用户可以发布文章的发布型服务。所有的源代码可以在 https://github.com/kazuhei/graphql-sample 找到。
定义对象的模式
定义用户、标签、帖子的架构。
type User {
id: ID!
name: String!
age: Int
}
type Tag {
name: String!
}
type Post {
id: ID!
title: String!
contents: String!
author: User!
tags: [Tag]!
}
GraphQL中有一种类型表示称为ID,它是一个字符串且唯一。
另外,叹号表示NotNull的意思。
定义查询模式
type Query {
posts: [Post]
popularPosts: [Post]
post(id: ID): Post
}
可以通过指定一个特殊的类型Query来指定查询的方法。
实施
这次我们将使用名为 https://github.com/webonyx/graphql-php 的库来进行实现。
准备调试环境
我会安装一个名为ChromeiQL的Chrome扩展,可以轻松复现GraphQL请求。
进行架构实现
我们将用PHP类来表示模式。虽然说起来大多数是使用数组进行配置。
<?php
namespace Type;
use DataSource\TagDataSource;
use DataSource\UserDataSource;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class Post extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Post',
'fields' => [
'id' => [
'type' =>Type::id(),
],
'title' => [
'type' => Type::string(),
],
'contents' => [
'type' => Type::string()
],
'author' => [
'type' => User::getInstance(),
],
'tags' => Type::listOf(Tag::getInstance())
],
'resolveField' => function ($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($value, $args, $context, $info);
} else {
return $value->{$info->fieldName};
}
}
];
parent::__construct($config);
}
private static $singleton;
public static function getInstance(): self
{
return self::$singleton ? self::$singleton : self::$singleton = new self();
}
public function resolveAuthor($value)
{
return UserDataSource::getById($value->authorId);
}
public function resolveTags($value)
{
return TagDataSource::getByPostId($value->id);
}
}
<?php
namespace Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class User extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'User',
'fields' => [
'id' => Type::int(),
'name' => Type::string(),
'age' => Type::int(),
],
];
parent::__construct($config);
}
private static $singleton;
public static function getInstance(): self
{
return self::$singleton ? self::$singleton : self::$singleton = new self();
}
}
<?php
namespace Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class Tag extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Tag',
'fields' => [
'name' => [
'type' => Type::string(),
],
],
];
parent::__construct($config);
}
private static $singleton;
public static function getInstance(): self
{
return self::$singleton ? self::$singleton : self::$singleton = new self();
}
}
我们将用PHP的语法将GraphQL的模式定义在$config中。
由于graphql-php通过检查配置类型定义类的类型以确定对象类型的一致性,所以它提供了一个名为getInstance的单例函数,始终返回同一个实例。
在$config中的resolveField函数中解决了field的内容。我觉得这是GraphQL的重要之处,和通常的API不同的是,GraphQL不是先准备好一般API的数据然后按照API格式返回,而是根据GraphQL的要求准备好数据然后返回。
准备数据类
<?php
namespace Data;
class Post
{
// DBから取得可能
public $id;
public $title;
public $contents;
public $authorId;
// graphql-phpに上書きされる
public $author;
public $tags;
public function __construct(string $id, string $title, string $contents, string $authorId)
{
$this->id = $id;
$this->title = $title;
$this->contents = $contents;
$this->authorId = $authorId;
}
}
graphql-php根据$config的resolveField,直接覆盖字段以适应GraphQL响应,因此设计考虑到了这一点。这很困难。由于努力使其符合GraphQL架构,导致PHP端的数据类类型逐渐瓦解…。
如果你也对其他数据类和服务器端的代码感兴趣,请在https://github.com/kazuhei/graphql-sample上查看。
让它动一动试试
发送GraphQL查询。
query {
# Postの一覧
posts {
id
title
contents
author {
name
age
}
tags {
name
}
}
# 人気Postのidとtitleだけを一覧で取得
popularPosts {
id
title
}
# PostをIDで取得
post(id: "2") {
id
title
contents
}
}
响应如下所示。
通常情况下,这个内容需要从3个不同的终端点分别获取,但是现在可以通过一个请求获取。
{
"data": {
"posts": [
{
"id": "1",
"title": "first season",
"contents": "...",
"author": {
"name": "Sophie Hojo",
"age": 15
},
"tags": [
{
"name": "Prism Stone"
},
{
"name": "SoLaMi Dressing"
}
]
},
{
"id": "2",
"title": "second season",
"contents": "...",
"author": {
"name": "Mirei Minami",
"age": 14
},
"tags": [
{
"name": "armageddon"
}
]
},
{
"id": "3",
"title": "third season",
"contents": "...",
"author": {
"name": "Laala Manaka",
"age": 12
},
"tags": [
{
"name": "nonsugar"
}
]
},
{
"id": "4",
"title": "4th season",
"contents": "...",
"author": {
"name": "Laala Manaka",
"age": 12
},
"tags": [
{
"name": "DanPri"
},
{
"name": "FantasyTime"
}
]
}
],
"popularPosts": [
{
"id": "4",
"title": "4th season"
},
{
"id": "3",
"title": "third season"
},
{
"id": "1",
"title": "first season"
}
],
"post": {
"id": "2",
"title": "second season",
"contents": "..."
}
}
}
这里有趣的是
# 人気Postのidとtitleだけを一覧で取得
popularPosts {
id
title
}
这部分是关于popularPosts的。只获取了id和title,而没有获取author,所以服务器端不会执行获取author的处理。对比一般的API来说,我个人感觉这样的操作非常不舒服,感觉客户端在支配服务器端。
评估和印象
关于GraphQL
实施中引起我关注的是,为了让服务器能够响应客户端的各种请求,每个类型都要保留自己的数据获取方法。
结果就是,服务器端必须分别获取所有资源的数据。
具体来说,以前是从数据库中联接posts和users表来获取数据的部分,现在变成了从posts表获取数据,然后再分别从users表获取每个post的数据。这是典型的N+1问题。
为了解决这个问题,graphql-php推荐了两种方法:获取缓存和异步通信,但我觉得两者都很复杂。也许可以不强行减少查询数量,而是将查询结果大量放入memcache中,尽量避免频繁访问数据库,这也是一种选择。
我觉得GraphQL的模式定义以及与JSON结构匹配的请求编写方法非常好,但是服务器端的实现变得相当复杂。
关于graphql-php
尽管版本仍为v0.11.4,但有时会出现不知道原因的内部服务器错误,这让人感到相当困惑。