gmostafa / php-graphql-oqm
GraphQL 对象到查询映射器 (QOM),可从 API 模式生成对象
Requires
- php: ^7.1 || ^8.0
- gmostafa/php-graphql-client: ^1.12
Requires (Dev)
- ext-json: *
- phpunit/phpunit: ^7.5|^9
This package is auto-updated.
Last update: 2024-08-29 16:48:19 UTC
README
此包利用 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());
有关如何使用客户端类的更多信息,请参阅
- https://github.com/mghoneimy/php-graphql-client#constructing-the-client
- https://github.com/mghoneimy/php-graphql-client#running-queries
注意
以下是一些关于模式对象的注意事项,以便您在使用生成类时生活更轻松
处理对象选择器
虽然标量字段设置器返回当前查询对象的一个实例,但对象字段选择器返回嵌套查询对象的对象。这意味着将 $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 是根,进化和进化需求是两个从它分支出来的子树。
提高查询对象的可读性
以下是一些如何使您的查询对象更具可读性的提示
- 将作为分支根使用的节点存储在具有意义的变量中,就像 charmander 的情况一样。
- 将每个选择器单独写在一行上。
- 每次使用对象选择器时,都要给下一个选择器添加额外的缩进。
- 将查询中间的新对象构造(如 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 个字段的根查询对象
- pokemons:检索一组 Pokemon 对象。它有一个参数:first。
- pokemon:检索一个 Pokemon 对象。它有两个参数:id 和 name。
将模式的小部分翻译成 3 个对象
- RootQueryObject:表示遍历 API 图的入口点
- RootPokemonsArgumentsObject:表示在 RootQueryObject 中的 "pokemons" 字段上的参数列表
- 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;
}
}