用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的好处有很多。

    1. 可以在一次请求中获取多个资源。

 

    1. 可以从一个端点访问所有数据。

 

    1. 由于存在资源的定义,可以使用类似类型的东西。

 

    1. 可以在字段级控制要获取的资源。

 

    以类似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请求。

スクリーンショット 2017-12-04 15.52.35.png

进行架构实现

我们将用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,但有时会出现不知道原因的内部服务器错误,这让人感到相当困惑。

广告
将在 10 秒后关闭
bannerAds