gmostafa / php-graphql-oqm

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

v1.5 2023-04-29 00:36 UTC

README

Build Status Build Status Codacy Badge Total Downloads Latest Stable Version License

此包利用 GraphQL API 的内省功能生成一组映射到 API 模式结构的类。然后,这些生成的类可以非常简单直观地用于查询 API 服务器。

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

安装

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

composer require gmostafa/php-graphql-oqm

生成模式对象

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

php vendor/bin/generate_schema_objects

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

您也可以通过命令行选项指定所有选项

php vendor/bin/generate_schema_objects \
    -u "https://graphql-pokemon.vercel.app/" \
    -h "Authorization" \
    -v "Bearer 123" \
    -d "customClassesWritingDirectory" \
    -n "Vendor\Custom\Namespace"

或者如果您喜欢长参数

php vendor/bin/generate_schema_objects \
    --url "https://graphql-pokemon.vercel.app/" \
    --authorization-header-name "Authorization" \
    --authorization-header-value "Bearer 123" \
    --directory "customClassesWritingDirectory" \
    --namespace "Vendor\Custom\Namespace"

用法

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

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

和 Github 仓库: https://github.com/lucasbento/graphql-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

查询对象

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

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

输入对象

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

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

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:检索一组 Pokemon 对象。它有一个参数:first。
  2. pokemon:检索一个 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;
    }
}