使用PHP尝试GraphQL


似乎最近招聘正在逐渐增加?目的是在使用PHP的同时学习GraphQL。

TL; DR

我研究了GraphQL,并编写了一个支持PHP中的GraphQL的服务器应用程序。
我使用graphql-php作为库,但是用PHP表达模式以及如何检查类型非常困难。
由于GraphQL是与语言无关的规范,因此PHP端的架构定义肯定会在将来自动生成。
GraphQL是一个强制客户端返回API数据的规范,因此,如果您尝试支持GraphQL,则服务器端要满足客户端的要求将变得更加复杂或不灵活。

什么是GraphQL?

GraphQL是API的查询语言。如果SQL是用于从数据库检索数据的查询语言,则GraphQL是用于从API检索JSON的查询语言。

看看GraphQL

在GraphQL中,如果定义架构(定义类型和类型的字段),则其将是API规范。

用于以下架构定义

1
2
3
4
5
6
7
8
type Query {
  me: User
}

type User {
  id: ID
  name: String
}

如果发出以下请求,则

1
2
3
4
5
{
  me {
    name
  }
}

您将收到这样的答复。

1
2
3
4
5
{
  "me": {
    "name": "Luke Skywalker"
  }
}

很容易理解,因为请求和响应格式非常相似。在这里,用户有两个ID和名称,但之所以只返回名称是因为在请求中仅指定了名称。

GraphQL

的好处

GraphQL具有许多优点。

  • 单个请求可以获取多个资源。
  • 可以从一个端点访问所有数据
  • 由于存在资源定义,因此可以使用一种类型
  • 您可以控制要在现场级别获取的资源
  • 直观类似于json的格式
  • 有关更多信息,我认为您应该阅读本文。 https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad
    之后,官方网站http://graphql.org/learn/

    让我们编写一个接受GraphQL的服务器

    想象一下一种允许用户发布文章的发布类型服务。
    所有源代码都是https://github.com/kazuhei/graphql-sample

    定义对象的架构

    定义用户,标签和发布的架构。

    图式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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的类型表达式,该表达式是字符串且唯一。
    另外,!表示不为空。

    定义查询的架构

    图式

    1
    2
    3
    4
    5
    type Query {
        posts: [Post]
        popularPosts: [Post]
        post(id: ID): Post
    }

    您可以通过指定称为

    查询的特殊类型来指定查询方法。

    实施

    这次,我们将使用一个名为https://github.com/webonyx/graphql-php的库来实现它。

    准备调试环境

    插入一个名为Chrome iQL的Chrome扩展程序,该扩展程序可以轻松重现GraphQL请求。

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

    实施架构

    我们将使用PHP类来表达模式。但是,大多数设置使用数组。

    键入/ Post.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    <?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);
        }
    }

    类型/ User.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <?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();
        }
    }

    类型/ Tag.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <?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();
        }
    }

    在$ config中使用php表示法定义graphql的架构。

    graphql-php根据配置类型的类型定义类是否为同一实例来确定对象类型的身份,因此我们提供了一个单例函数getInstance,使其始终返回相同的实例。

    $ config中的resolveField函数解析该字段的内容。我觉得这是GraphQL的重要组成部分,与准备常规API数据然后格式化并将其返回给API的情况不同,GraphQL希望准备数据,就像返回一样。

    准备数据类

    数据/ Post.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?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查询。

    图式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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
      }
    }

    响应如下所示:
    通常,它是从三个端点分别获取的内容,但是可以通过一个请求来获取。

    result.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    {
      "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": "..."
        }
      }
    }

    有趣的是

    1
    2
    3
    4
    5
      # 人気Postのidとtitleだけを一覧で取得
      popularPosts {
        id
        title
      }

    这是

    部分。
    在流行的帖子中,仅会获取ID和标题,而不会获取作者,因此作者获取过程不会在服务器端运行。与普通的API相比,我感到这很不愉快,而且我认为客户端主导着服务器端。

    考虑吗?

    关于GraphQL

    我感兴趣的是实现每种类型都有其自己的获取方式,以便服务器可以处理客户端的每个请求。
    结果,在服务器端,所有资源必须分别检索其数据。
    具体而言,过去,通过将数据库的posts表和users表结合而获得的部分从posts表中获取数据,并针对每个post从users表中获取数据。这是一个典型的N 1问题。

    为了解决这个问题,graphql-php建议使用两种类型的获取缓冲和异步通信,但是我发现两者都很复杂。可能不强制减少查询数量,而应将其放在内存缓存中,并尽量避免访问数据库。

    我认为GraphQL端的模式定义以及如何编写与json结构匹配的请求都非常好,但是我觉得服务器端的实现会非常复杂。

    关于graphql-php

    仍然有一个版本为v0.11.4的地方,但是如果您输入有误,则可能由于内部服务器错误而无法理解原因,因此非常痛苦。