mxneyio/php-graphql-oqm

GraphQL 对象到查询映射器(QOM),可从 API 模式生成对象

v1.0.0 2023-03-24 01:49 UTC

This package is not auto-updated.

Last update: 2024-09-18 19:49:15 UTC


README

Build Status

此包利用 GraphQL API 的反射功能生成一组映射到 API 模式结构的类。然后,可以使用简单直观的方式使用生成的类查询 API 服务器。

使用 PHP 与 GraphQL API 交互从未如此简单!

安装

运行以下命令使用 composer 安装包

composer require gmostafa/php-graphql-oqm

生成模式对象

安装包后,第一步是生成模式对象。这可以通过执行以下命令轻松实现

php vendor/bin/generate_schema_objects

此脚本将使用 GraphQL 的反射功能检索 API 模式类型,然后从类型生成模式对象,并保存到包根目录下的 schema_object 目录中。您可以通过在运行命令时提供“自定义类写入目录”的值来覆盖默认写入目录。

用法

在下面的所有示例中,我将使用超级酷的公开 Pokemon GraphQL API 作为说明。

查看 API: https://graphql-pokemon.now.sh/

以及 Github 仓库: https://github.com/lucasbento/graphql-pokemon

在生成公开 Pokemon API 的模式对象之后,我们可以通过使用 RootQueryObject 轻松地查询 API。以下是一个示例

$rootObject = new RootQueryObject();
$rootObject
    ->selectPokemons((new RootPokemonsArgumentsObject())->setFirst(5))
        ->selectName()
        ->selectId()
        ->selectFleeRate()
        ->selectAttacks()
            ->selectFast()
                ->selectName();

此查询执行的操作是选择前 5 个宝可梦,返回它们的名称、id、逃跑率和带有名称的快速攻击。容易吗!?

剩下的只是实际运行查询以获取结果

$results = $client->runQuery($rootObject->getQuery());

有关如何使用客户端类的更多信息,请参阅

注意

以下是一些关于模式对象的注意事项,以便在使用生成类时使您的生活更加轻松

处理对象选择器

虽然标量字段设置器返回当前查询对象的实例,但对象字段选择器返回嵌套查询对象的对象。这意味着将 $rootObject 引用设置为对象选择器返回的结果意味着根查询对象的引用已丢失。

不要这样做

$rootObject = (new RootQueryObject())->selectAttacks()->selectSpecial()->selectName();

这样,您将得到 PokemonAttackQueryObject 的引用,而 RootQueryObject 的引用已丢失。

这样做

$rootObjet = new RootQueryObject();
$rootObject->selectAttacks()->selectSpecial()->selectName();

这样,您可以跟踪 RootQueryObject 引用,并安全地开发查询。

处理多个对象选择器

假设我们想获取宝可梦“Charmander”,检索它的进化、进化要求和它的进化的进化要求,我们应该如何做到这一点?

我们不能这样做

$rootObject = new RootQueryObject();
$rootObject->selectPokemon(
    (new RootPokemonArgumentsObject())->setName('charmander')
)
    ->selectEvolutions()
        ->selectName()
        ->selectNumber()
        ->selectEvolutionRequirements()
            ->selectName()
            ->selectAmount()
    ->selectEvolutionRequirements()
        ->selectName()
        ->selectAmount();

这是因为引用现在指向 Charmander 的进化的进化要求,而不是 Charmander 本身。

最好的方法是这样构建查询

$rootObject = new RootQueryObject();
$charmander = $rootObject->selectPokemon(
    (new RootPokemonArgumentsObject())->setName('charmander')
);
$charmander->selectEvolutions()
    ->selectName()
    ->selectNumber()
    ->selectEvolutionRequirements()
        ->selectName()
        ->selectAmount();
$charmander->selectEvolutionRequirements()
    ->selectName()
    ->selectAmount();

这样,我们保留了 Charmander 的引用,并以直观的方式构建了查询。

通常,每当有一个分支(就像获取同一对象的进化及其进化要求的情况)时,最好的方法是像树一样构建查询,其中树的根是分支掉的对象的引用。在这种情况下,Charmander 是根,进化及其进化要求是两个从它分支出来的子树。

提高查询对象的可读性

关于如何使查询对象更易于阅读的一些建议

  1. 将作为根节点使用的节点存储在具有意义的变量中,就像charmander的情况一样。
  2. 将每个选择器单独写在单独的一行上。
  3. 每次使用对象选择器时,为下一个选择器添加额外的缩进。
  4. 将查询(如ArgumentsObject构造)中的新对象构造移动到新的一行。

模式对象生成

运行生成脚本后,SchemaInspector将在GraphQL服务器上运行查询以检索API模式。之后,SchemaClassGenerator将递归地遍历从根查询类型开始的模式,为模式规范中的每个对象创建一个类。

SchemaClassGenerator将根据以下从GraphQL类型到SchemaObject类型的映射生成不同的模式对象

  • OBJECT: QueryObject
  • INPUT_OBJECT: InputObject
  • ENUM: EnumObject

此外,将为每个对象中每个字段的参数生成一个ArgumentsObject。参数对象命名约定是

{CURRENT_OBJECT}{FIELD_NAME}ArgumentsObject

QueryObject

对象生成器将从根queryType开始遍历模式,根据以下规则为遇到的每个查询对象创建一个类

  • RootQueryObject将为模式声明中的queryType对应类型生成,此对象是所有GraphQL查询的起点。
  • 对于名称为{OBJECT_NAME}的查询对象,将创建一个名为{OBJECT_NAME}QueryObject的类。
  • 对于查询对象选择集中每个选择字段,将创建一个相应的选择器方法,根据以下规则
    • 标量字段将为其创建一个简单的选择器,将字段名称添加到选择集中。简单的选择器将返回正在创建的查询对象(this)的引用。
    • 对象字段将为其创建一个对象选择器,该选择器将内部创建一个新的查询对象并将其嵌套在当前查询中。对象选择器将返回新创建的查询对象的实例。
  • 对于与对象字段相关的每个参数列表,将根据以下规则创建一个ArgumentsObject和一个对应于每个参数值的setter
    • 标量参数:将为它们创建一个简单的setter以设置标量参数值。
    • 列表参数:将为它们创建一个列表setter以使用array设置参数值。
    • 输入对象参数:将为它们创建一个输入对象setter以使用类型为InputObject的对象设置参数值。

The InputObject

对于在遍历模式时遇到的每个输入对象,将根据以下规则创建相应的类

  • 对于名称为{OBJECT_NAME}的输入对象,将创建一个名为{OBJECT_NAME}InputObject的类
  • 对于输入对象声明中的每个字段,将创建一个setter,根据以下规则
    • 标量字段:将为它们创建一个简单的setter以设置标量值。
    • 列表字段:将为它们创建一个列表setter以使用array设置值。
    • 输入对象参数:将为它们创建一个输入对象setter以使用类型为InputObject的对象设置值。

The EnumObject

对于在遍历模式时遇到的每个枚举,将根据以下规则创建相应的ENUM类

  • 对于名称为{OBJECT_NAME}的枚举对象,将创建一个名为{OBJECT_NAME}EnumObject的类
  • 对于ENUM声明中的每个EnumValue,将在类中创建一个const以存储其值

实时API示例

从根查询类型来看,这是Pokemon GraphQL API的模式结构

"queryType": {
  "name": "Query",
  "kind": "OBJECT",
  "description": "Query any Pokémon by number or name",
  "fields": [
    {
      "name": "query",
      "type": {
        "name": "Query",
        "kind": "OBJECT",
        "ofType": null
      },
      "args": []
    },
    {
      "name": "pokemons",
      "type": {
        "name": null,
        "kind": "LIST",
        "ofType": {
          "name": "Pokemon",
          "kind": "OBJECT"
        }
      },
      "args": [
        {
          "name": "first",
          "description": null
        }
      ]
    },
    {
      "name": "pokemon",
      "type": {
        "name": "Pokemon",
        "kind": "OBJECT",
        "ofType": null
      },
      "args": [
        {
          "name": "id",
          "description": null
        },
        {
          "name": "name",
          "description": null
        }
      ]
    }
  ]
}

我们基本有一个包含2个字段的根查询对象

  1. pokemons: 获取一系列的宝可梦对象。它有一个参数:first。
  2. pokemon: 获取一个宝可梦对象。它有两个参数:id和name。

将此模式小部分转换为3个对象

  1. RootQueryObject: 表示指向遍历API图的入口点
  2. RootPokemonsArgumentsObject: 表示在RootQueryObject中的"pokemons"字段上的参数列表
  3. RootPokemonArgumentsObject: 表示在RootQueryObject中的"pokemon"字段上的参数列表

以下是生成的3个类

<?php

namespace GraphQL\SchemaObject;

class RootQueryObject extends QueryObject
{
    const OBJECT_NAME = "query";

    public function selectPokemons(RootPokemonsArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("pokemons");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectPokemon(RootPokemonArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("pokemon");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }
}

RootQueryObject 包含2个选择方法,每个字段一个,以及一个可选的参数,包含所需的ArgumentsObjects。

<?php

namespace GraphQL\SchemaObject;

class RootPokemonsArgumentsObject extends ArgumentsObject
{
    protected $first;

    public function setFirst($first)
    {
        $this->first = $first;
    
        return $this;
    }
}

RootPokemonsArgumentsObject 包含列表中"pokemons"字段的唯一参数作为一个属性,并提供一个设置器来更改其值。

<?php

namespace GraphQL\SchemaObject;

class RootPokemonArgumentsObject extends ArgumentsObject
{
    protected $id;
    protected $name;

    public function setId($id)
    {
        $this->id = $id;
    
        return $this;
    }

    public function setName($name)
    {
        $this->name = $name;
    
        return $this;
    }
}

RootPokemonArgumentsObject 包含列表中"pokemon"字段的2个参数作为属性,并提供设置器来更改它们的值。

额外

此外,在递归遍历模式时将创建PokemonQueryObject。它对于完成此演示不是必需的,但为了使事情更清晰,我将它添加到下面,以便有人想看到更多生成动作的话可以查看。

<?php

namespace GraphQL\SchemaObject;

class PokemonQueryObject extends QueryObject
{
    const OBJECT_NAME = "Pokemon";

    public function selectId()
    {
        $this->selectField("id");
    
        return $this;
    }

    public function selectNumber()
    {
        $this->selectField("number");
    
        return $this;
    }

    public function selectName()
    {
        $this->selectField("name");
    
        return $this;
    }

    public function selectWeight(PokemonWeightArgumentsObject $argsObject = null)
    {
        $object = new PokemonDimensionQueryObject("weight");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectHeight(PokemonHeightArgumentsObject $argsObject = null)
    {
        $object = new PokemonDimensionQueryObject("height");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectClassification()
    {
        $this->selectField("classification");
    
        return $this;
    }

    public function selectTypes()
    {
        $this->selectField("types");
    
        return $this;
    }

    public function selectResistant()
    {
        $this->selectField("resistant");
    
        return $this;
    }

    public function selectAttacks(PokemonAttacksArgumentsObject $argsObject = null)
    {
        $object = new PokemonAttackQueryObject("attacks");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectWeaknesses()
    {
        $this->selectField("weaknesses");
    
        return $this;
    }

    public function selectFleeRate()
    {
        $this->selectField("fleeRate");
    
        return $this;
    }

    public function selectMaxCP()
    {
        $this->selectField("maxCP");
    
        return $this;
    }

    public function selectEvolutions(PokemonEvolutionsArgumentsObject $argsObject = null)
    {
        $object = new PokemonQueryObject("evolutions");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectEvolutionRequirements(PokemonEvolutionRequirementsArgumentsObject $argsObject = null)
    {
        $object = new PokemonEvolutionRequirementQueryObject("evolutionRequirements");
        if ($argsObject !== null) {
            $object->appendArguments($argsObject->toArray());
        }
        $this->selectField($object);
    
        return $object;
    }

    public function selectMaxHP()
    {
        $this->selectField("maxHP");
    
        return $this;
    }

    public function selectImage()
    {
        $this->selectField("image");
    
        return $this;
    }
}