maiscraft / graphql
PHP 8 Attribute + Reflection driven GraphQL Schema auto-generation engine. Framework-agnostic, with strategy pattern type conversion and container DI.
Requires
- php: ^8.2
- psr/container: ^2.0
- webonyx/graphql-php: ^15.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
基于 PHP 8 Attribute + 反射的 GraphQL Schema 自动生成引擎,框架无关。
核心理念
- 约定优于配置:getter/setter/构造函数自动推导字段,无需手动注册
- 最小注解设计:
#[Type]、#[Field]、#[Query]、#[Mutation]、#[Arg]五个核心注解 - 反射驱动:PHP 反射自动生成 Schema,减少样板代码
- 策略模式 + 容器注入:TypeResolver(唯一决策点)→ TypeConverterFactory → 各 TypeConverter,核心服务通过容器单例
- ID 自动加解密:自定义
ID标量,输出时自动 encode、输入时自动 decode,业务层无感知 - ID 自动推断:参数名/字段名恰好为
id且 PHP 类型为int时,自动推断为ID类型,无需显式注解 - EntityObject 基类:继承即自动成为 GraphQL Type,无需
#[Type]注解 - 字段级方向控制:
#[Field(readonly: true)](仅 Output)、#[Field(writeonly: true)](仅 Input)、#[Field(view: ['list'])](视图限定),引擎自动派生多类型
架构
Driver(组装容器,注册核心服务)
└─ TypeResolver(唯一决策点 + 唯一注册入口)
├─ enum_exists / class_exists 判断收敛于此
├─ TypeKind 分类(SCALAR / ENUM / OBJECT / INPUT_OBJECT)
└─ TypeConverterFactory → EnumTypeConverter / ObjectTypeConverter / InputTypeConverter
└─ TypeRegistry(纯存储,零判断)
└─ resolveIDScalar() → 容器有 IdCodecInterface ? IDScalar : 内置 ID
└─ PhpTypeMapper(PHP → GraphQL 类型映射)
└─ RootFieldConverter(Query/Mutation 根字段 + 运行时值转换)
└─ SchemaDirector(构建 Schema,注入 scalarOverrides 让 webonyx 使用自定义 IDScalar)
职责边界:
- TypeResolver:唯一决策点,所有类型判断收敛到这里
- TypeRegistry:纯存储,只做 get/set/has/mapClassName
- Converter:纯转换,className → GraphQL Type,零注册逻辑
- 所有核心服务通过 ContainerInterface 注入,保证单例
安装
composer require maiscraft/graphql
composer require maiscraft/graphql-hyperf # Hyperf 框架适配
快速开始
1. 定义 Entity(Output Type)
方式一:继承 EntityObject(推荐)
namespace App\Domain\Entity; use Maiscraft\GraphQL\Entity\EntityObject; class Article extends EntityObject { public function __construct(array $data = []) { parent::__construct($data); } // id: int → 自动推断为 ID 类型,无需 #[Field(type: 'ID')] public function getId(): ?int { return $this->getData('id') ?: null; } public function getTitle(): string { return $this->getData('title') ?? ''; } // is* 方法 → is_published 布尔字段 public function isPublished(): bool { return $this->getData('status') === 'published'; } }
继承
EntityObject的类自动成为 GraphQL Type,无需#[Type]注解。引擎通过父链查找识别。
方式二:使用 #[Type] 注解
namespace App\Domain\Entity; use Maiscraft\GraphQL\Annotation\Type; #[Type(description: '文章')] class Article { public function __construct( private readonly ?int $id = null, private readonly string $title = '', ) {} public function getId(): ?int { return $this->id; } public function getTitle(): string { return $this->title; } }
字段暴露规则:
| 来源 | Output | Input |
|---|---|---|
| 构造函数参数 | ✅ 自动收集 | ✅ 自动收集 |
get* 方法 |
✅ 自动收集 | ❌ |
is* 方法 |
✅ → is_fieldname |
❌ |
set* 方法 |
❌ | ✅ 自动收集 |
| public 属性 | 仅 #[Field] 标注 |
仅 #[Field] 标注 |
#[Field(hidden: true)] |
❌ 不输出 | ❌ 不输出 |
2. 定义 Enum
namespace App\Domain\ValueObject; use Maiscraft\GraphQL\Annotation\Type; #[Type(description: '发布状态')] enum PublishStatus { case DRAFT; case PUBLISHED; case ARCHIVED; }
支持 backed enum 和非 backed enum,运行时值转换统一通过 ReflectionEnum::getCase() 反射获取,无硬编码方法依赖。
3. 定义 DTO(Input Type)
namespace App\DTO; class CreateArticleDTO { public function __construct( public readonly string $title, public readonly string $slug, public readonly ?string $description = null, ) {} }
构造函数参数自动成为 GraphQL Input 字段,运行时通过反射构造实例(snake_case → camelCase 自动映射)。
4. 定义 Resolver
namespace App\GraphQL; use App\Domain\Entity\Article; use App\DTO\CreateArticleDTO; use Maiscraft\GraphQL\Annotation\Query; use Maiscraft\GraphQL\Annotation\Mutation; use Maiscraft\GraphQL\Annotation\Arg; use Maiscraft\GraphQL\Contract\ResolverInterface; class ArticleResolver implements ResolverInterface { public function __construct( private ArticleRepositoryInterface $repository ) {} // id: int → 自动推断为 ID! 类型,无需 #[Arg(type: 'ID!')] #[Query(description: 'Get an article by ID')] public function article(int $id): ?Article { return $this->repository->findById($id); } // 返回类型以 Pagination 结尾 → 自动追加分页参数 page/perPage #[Query(description: 'Get paginated articles')] public function articles(): ArticlePagination { return $this->repository->paginate(); } #[Mutation(description: 'Create a new article')] public function createArticle(CreateArticleDTO $input): Article { return $this->repository->save(new Article( title: $input->title, slug: $input->slug, description: $input->description, )); } // id: int → 自动推断为 ID! 类型 #[Mutation(description: 'Delete an article')] public function deleteArticle(int $id): bool { return $this->repository->delete($id); } }
非 id 的 ID 参数需显式注解:
// productId 不是 "id",不会自动推断,需显式声明 #[Query(description: 'Get product variants')] public function productVariants(#[Arg(type: 'ID!')] int $productId): array { return $this->repository->getVariants($productId); }
5. 配置(Hyperf)
// config/autoload/graphql.php return [ 'debug' => true, // 目录路径扫描(从文件内容解析 namespace 获取类名) 'scan_paths' => [ BASE_PATH . '/app', ], 'security' => [ 'max_query_depth' => 15, 'max_query_complexity' => 1000, ], ];
6. 独立使用(无框架)
use Maiscraft\GraphQL\Default\SimpleContainer; use Maiscraft\GraphQL\Driver\Driver; $container = new SimpleContainer(); $driver = new Driver($container); $schema = $driver->buildSchema( resolvers: [ArticleResolver::class], types: [Article::class, PublishStatus::class], ); $result = \GraphQL\GraphQL::executeQuery( schema: $schema, source: '{ articles { items { id title } total } }', );
无框架使用时,容器中无 IdCodecInterface 绑定,引擎自动用 NoopIdCodec 兜底(明文 ID)。
7. 查询示例
query { article(id: "jR") { id title is_published } articles(page: 1, perPage: 10) { items { id title } total current_page last_page has_more_pages } }
注意:
id参数传入的是加密后的字符串(如"jR"),引擎自动 decode 为 int 传给 Resolver 方法。输出时id字段自动 encode 为加密字符串。
EntityObject 基类
EntityObject 是 GraphQL 包提供的规范实体基类。继承此类的子类自动成为 GraphQL ObjectType,无需 #[Type] 注解。
工作机制
EntityObject(abstract,声明 #[Type])
└─ Article extends EntityObject(无 #[Type])
└─ MetadataInspector::isGraphqlType() 沿父链查找
└─ 找到 EntityObject 的 #[Type] → 识别为 GraphQL Type
EntityObject声明#[Type]且为abstract,本身不会被扫描注册ClassScanner::isTypeClass()跳过抽象类MetadataInspector::isGraphqlType()沿父链查找#[Type]注解- 具体子类(如
Article)非抽象、无#[Type],但父链找到 → 被扫描注册
核心能力
| 能力 | 说明 |
|---|---|
$_data 数组存储 |
构造函数接收 Model::getAttributes() 数组,数据统一存入 $_data |
__call 魔术方法 |
getXxx() → $_data['xxx'](snake_case),setXxx/isXxx/hasXxx/unsXxx 同理 |
setData/getData |
显式读写 $_data,供 Model toEntity() 调用 |
setPropAttr() |
根据 public 属性声明类型自动转换(int/float/string/bool/enum) |
toArray() |
递归转为关联数组(处理嵌套 EntityObject) |
raw() |
返回原始 $_data(不做转换) |
\ArrayAccess |
支持 $entity['key'] 语法 |
| 外键加解密 | encryptForeignKey/decryptForeignKey 等辅助方法 |
使用示例
use Maiscraft\GraphQL\Entity\EntityObject; use Maiscraft\GraphQL\Annotation\Field; use Maiscraft\GraphQL\Annotation\Input; class Product extends EntityObject { public ProductType $type = ProductType::STANDARD; public string $title = ''; public ?float $price = null; // 只读字段,不出现在 Input 类型中 #[Input(exclude: true)] public array $available_locales = []; // 自定义 getter 覆盖 __call 兜底 public function getPriceInfo(): ?PriceInfo { return $this->getData('price_info'); } }
若子类需要隐藏(不暴露到 Schema),加
#[Type(hidden: true)]。
ID 标量类型
GraphQL 内置 ID 标量被替换为自定义 IDScalar,实现自动加解密。
工作机制
输出路径(serialize):
PHP int (123) → IDScalar::serialize() → IdCodec::encode(123) → "jR" (GraphQL 响应)
输入路径(parseValue / parseLiteral):
GraphQL "jR" → IDScalar::parseValue() → IdCodec::decode("jR") → 123 (PHP int)
核心组件
| 组件 | 位置 | 职责 |
|---|---|---|
IdCodecInterface |
Contract/IdCodecInterface.php |
加解密契约(encode/decode/decodeNullable) |
NoopIdCodec |
Default/NoopIdCodec.php |
默认兜底实现(明文透传,(string) $id) |
IDScalar |
Type/IDScalar.php |
自定义标量,继承 CustomScalarType,覆盖 serialize/parseValue/parseLiteral |
TypeRegistry |
TypeRegistry.php |
resolveIDScalar() 检查容器有无 IdCodecInterface,有则用 IDScalar,无则用内置 Type::id() |
SchemaDirector |
Director/SchemaDirector.php |
构建 Schema 时注入 scalarOverrides,让 webonyx 使用自定义 IDScalar 替代内置 ID |
注册自定义 IdCodec
方式一:通过容器绑定(推荐)
// Hyperf ConfigProvider use Maiscraft\GraphQL\Contract\IdCodecInterface; 'dependencies' => [ IdCodecInterface::class => fn($container) => $container->get(MyIdCodecAdapter::class), ],
方式二:实现 IdCodecInterface
use Maiscraft\GraphQL\Contract\IdCodecInterface; final class MyIdCodec implements IdCodecInterface { public function encode(int $id): string { return base_convert($id ^ 0x5DE3CE7, 10, 36); } public function decode(string $encoded): int { return base_convert($encoded, 36, 10) ^ 0x5DE3CE7; } public function decodeNullable(?string $encoded): ?int { if ($encoded === null || $encoded === '') { return null; } return $this->decode($encoded); } }
容器中无
IdCodecInterface绑定时,TypeRegistry::resolveIDScalar()返回内置Type::id(),ID 明文传输,无加解密。
webonyx scalarOverrides
webonyx 的 Schema 构造需要 scalarOverrides 配置才会用自定义标量替代内置 ID。SchemaDirector::build() 中:
$scalarOverrides = []; $idType = $typeRegistry->get('ID'); if ($idType instanceof CustomScalarType) { $scalarOverrides['ID'] = $idType; } return new Schema([ // ... 'scalarOverrides' => $scalarOverrides, ]);
没有 scalarOverrides,即使 TypeRegistry 注册了 IDScalar,序列化仍走内置 Type::id()。
ID 自动推断
参数名/字段名恰好为 id 且 PHP 类型为 int 时,自动推断为 ID 类型,无需显式 #[Arg(type: 'ID!')] 或 #[Field(type: 'ID')]。
推断规则
| 场景 | 条件 | 结果 | 实现位置 |
|---|---|---|---|
| Query/Mutation 参数 | 参数名 === 'id' + PHP int |
ID 或 ID!(按 nullable) |
ArgResolver::isIdParameter() |
| Output 字段 | 字段名 === 'id' + PHP int |
ID 或 ID!(按 nullable) |
ObjectTypeConverter::isIdFieldName() |
| Input 字段 | 字段名 === 'id' + PHP int |
ID 或 ID!(按 nullable) |
InputTypeConverter::isIdFieldName() |
不自动推断的情况:
- 外键字段:
product_id、categoryId、*_id、*Id→ 保持Int,需显式#[Arg(type: 'ID!')]或#[Field(type: 'ID')] - 非
int类型的id:如string $id→ 保持String - 有显式注解的:
#[Arg(type: 'String!')] int $id→ 用注解类型
示例对比
// 自动推断(无需注解) #[Query] public function article(int $id): ?Article { ... } // → ID! public function getId(): ?int { ... } // → ID // 需显式注解(非 id 名称) #[Query] public function productVariants(#[Arg(type: 'ID!')] int $productId): array { ... } #[Field(type: 'ID')] public function getCategoryId(): ?int { ... }
分页
引擎提供三种分页模式,声明返回类型即自动完成类型注册、参数追加、数据规范化。
三种分页类型
| 模式 | 泛型语法 | 直接写法 | 生成的 GraphQL 类型 | 特点 |
|---|---|---|---|---|
| 完整偏移分页 | Pagination<Page> |
PagePagination |
{Node}Pagination |
含 total/last_page,触发 COUNT(*) |
| 简单偏移分页 | SimplePagination<Page> |
PageSimplePagination |
{Node}SimplePagination |
无 total,多取 1 条判断 hasMore |
| Relay 游标分页 | CursorPagination<Page> |
PageConnection |
{Node}Connection |
edges+pageInfo+totalCount |
GraphQL 类型结构
完整偏移分页({Node}Pagination):
type PagePagination { items: [Page!]! total: Int! per_page: Int! current_page: Int! from: Int to: Int last_page: Int! has_more_pages: Boolean! }
简单偏移分页({Node}SimplePagination):
type PageSimplePagination { items: [Page!]! per_page: Int! current_page: Int! from: Int to: Int has_more_pages: Boolean! }
Relay 游标分页({Node}Connection):
type PageConnection { edges: [PageEdge!]! nodes: [Page!]! pageInfo: PageInfo! totalCount: Int! } type PageEdge { node: Page! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
自动追加参数
根据返回类型自动追加参数(仅当方法未显式声明时):
| 分页模式 | 自动追加参数 | 默认值 |
|---|---|---|
Pagination / SimplePagination |
page: Int, perPage: Int |
page=1, perPage=15 |
CursorPagination |
first: Int, after: String |
first=15, after=null |
Resolver 用法
// 完整偏移分页 — 自动追加 page/perPage 参数 #[Query(type: 'PagePagination', description: '页面列表')] public function pages(): \Hyperf\Contract\LengthAwarePaginatorInterface { return $this->service->paginate(); } // 带过滤参数 + 分页(分页参数自动追加) #[Query(type: 'ProductPagination', description: '商品列表')] public function products( #[Arg(name: 'filters', type: '[FilterInput!]')] array $filters = [], ): \Hyperf\Contract\LengthAwarePaginatorInterface { return $this->service->paginate($filters); } // 游标分页 — 自动追加 first/after 参数 #[Query(type: 'CursorPagination<Product>', description: '商品游标分页')] public function productsCursor(): array { return PaginationHelper::cursorPaginate($query, $first, $after); }
PaginationHelper 便捷工具
use Maiscraft\GraphQL\Pagination\PaginationHelper; // 完整偏移分页(触发 COUNT(*)) $result = PaginationHelper::paginate($query, page: 2, perPage: 15); // 简单分页(避免 COUNT(*),多取 1 条判断 hasMore) $result = PaginationHelper::simplePaginate($query, page: 2, perPage: 15); // Relay 游标分页 $result = PaginationHelper::cursorPaginate($query, first: 15, after: $cursor); // 从 Hyperf Paginator 构建 $result = PaginationHelper::fromPaginator($paginator); // 游标编解码 $cursor = PaginationHelper::encodeCursor(30); // base64("cursor:30") $offset = PaginationHelper::decodeCursor($cursor); // 30
GraphQL 查询示例
query { pages(page: 1, perPage: 10) { items { id title } total current_page last_page has_more_pages } }
query { productsCursor(first: 10, after: null) { edges { node { id title } cursor } pageInfo { hasNextPage endCursor } totalCount } }
Entity 字段中使用分页
class Category extends EntityObject { // 分页字段 — 只读,不出现在 Input 中 #[Field(type: 'ProductPagination', description: '分类下商品(分页)')] #[Input(exclude: true)] public array $products = []; }
注解参考
#[Type] — 类级别,标记 GraphQL 类型
#[Type(name: 'CustomName', description: '描述', hidden: false)] class Article { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string | 类短名 | GraphQL type 名称 |
description |
string | '' |
类型描述 |
as |
string | '' |
类型用途标记(如 'input'、'output'),扩展预留 |
hidden |
bool | false |
隐藏,不生成 Schema |
继承
EntityObject的子类无需#[Type],父链查找自动识别。若需隐藏子类,加#[Type(hidden: true)]。
#[Field] — 方法/属性/参数,标记字段
#[Field(type: 'ProductPagination', description: '商品列表', hidden: false)] public array $products = []; #[Field(readonly: true, view: ['detail'])] public ?string $description = null; #[Field(writeonly: true, type: '[ID!]')] public array $category_ids = [];
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string|null | null |
覆盖字段名 |
type |
string|null | 自动推导 | GraphQL 类型(支持 Article[]、ID!、JSON、ProductPagination) |
description |
string|null | null |
字段描述 |
hidden |
bool | false |
隐藏,不输出到 Schema |
readonly |
bool | false |
仅 Output(排除 Input),适用于系统生成字段 |
writeonly |
bool | false |
仅 Input(排除 Output),适用于写入字段 |
view |
array | [] |
视图限定(空=所有视图,['list'] =仅 list 视图) |
id字段(PHPint)自动推断为ID,无需#[Field(type: 'ID')]。
字段方向控制规则:
| 标记 | Output | Input | 说明 |
|---|---|---|---|
| 无标记(默认) | ✅ | ✅ | 读写都行 |
readonly: true |
✅ | ❌ | 系统生成字段(slug、created_at、variants) |
writeonly: true |
❌ | ✅ | 写入字段(password、category_ids 批量关联) |
view: ['list'] |
仅该视图 | ❌ | 视图特异性字段 |
view: ['detail'] |
仅该视图 | ❌ | 视图特异性字段 |
视图派生示例:
#[Type(name: 'Product')] class Product extends EntityObject { #[Field] public string $name; #[Field(readonly: true)] public string $slug; #[Field(readonly: true, view: ['detail'])] public ?string $description; #[Field(readonly: true, view: ['detail'])] public array $variants = []; #[Field(readonly: true, view: ['list'])] public ?float $min_price; #[Field(writeonly: true, type: '[ID!]')] public array $category_ids = []; }
引擎自动派生 3 个类型:
type ProductList { id: ID! name: String! slug: String! min_price: Float } type ProductDetail { id: ID! name: String! slug: String! description: String variants: [Variant!]! } input ProductInput { name: String! category_ids: [ID!] }
#[Query] / #[Mutation] — 方法,定义根字段
#[Query(name: 'articles', type: 'PagePagination', description: '文章列表')] public function articles(): mixed { ... } #[Mutation(name: 'createArticle', description: '创建文章')] public function createArticle(CreateArticleDTO $input): Article { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string | 方法名 | GraphQL 字段名 |
description |
string | '' |
字段描述 |
type |
string | 返回类型推导 | 返回类型名(支持 PagePagination、Pagination<Page> 等) |
#[Arg] — 参数,定义字段参数
#[Arg(name: 'filters', type: '[FilterInput!]', description: '过滤条件')] array $filters = []
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string | 参数名 | GraphQL 参数名 |
type |
string | 自动推导 | GraphQL 类型(支持 ID!、[FilterInput!] 等) |
description |
string | null |
参数描述 |
id参数(PHPint)自动推断为ID!,无需#[Arg(type: 'ID!')]。非id的 ID 参数需显式注解。
#[Input] — 属性/方法/参数,控制 Input 类型行为
// 从 Input 类型中排除(只读/计算字段) #[Input(exclude: true)] public array $available_locales = []; // 标记为可选(即使 PHP 类型非空) #[Input(required: false)] public ?PriceInfo $price = null;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
exclude |
bool | false |
从 Input 类型中排除该字段 |
required |
bool|null | null |
是否必填(null=自动推断,true=必填,false=可选) |
#[Rules] — 属性/参数,验证规则
class CreateProductDTO { #[Rules(['required', 'string', 'max:255'])] public string $title; #[Rules(['required', 'numeric', 'min:0'])] public float $price; // 管道格式简写 #[Rules('required|email')] public string $email; }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rules |
array|string|null | null |
验证规则(数组格式 ['required', 'max:255'] 或管道格式 `'required |
支持 Laravel 风格全部规则(
required|unique:table,col|exists:...等),通过ValidatorInterface执行。嵌套 InputType 递归验证。
#[Permission] — 类/方法,权限检查
// 类级别 — 该 Resolver 所有字段的默认权限 #[Permission(['content.view'])] class PageAdminResolver implements AuthResolverInterface { ... } // 方法级别 — 与类级权限合并 #[Permission(['content.create'])] public function createPage(CreatePageDTO $input): Page { ... } // 使用自定义策略类 #[Permission(policy: 'App\Policies\ProductPolicy')] public function deleteProduct(int $id): bool { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
args |
array|string | [] |
权限标识(如 ['content.view']) |
policy |
string|null | null |
自定义策略类名(null 时用默认 AuthorizeResourceInterface) |
授权流程:
AuthResolverInterface→FieldSecurityMiddleware→Permission::check()。#[Guard]已废弃,用#[Permission]替代。
#[Privacy] — 方法/属性/参数,字段级访问控制
// 闭包判断 — 拒绝时返回 null(不抛错) #[Privacy(fn($root, $args, $ctx) => $ctx?->is_admin)] public string $hiddenField; // 引用 Privacy 子类 #[Privacy(AdminPrivacy::class)] public function getSecretData(): string { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
policy |
callable|string | null |
闭包或 Privacy 子类类名 |
与
#[Permission]区别:#[Permission]拒绝时抛错,#[Privacy]拒绝时返回 null。
#[Cache] — 方法,字段级缓存
// 公开数据,所有用户共享同一缓存 #[Cache(ttl: 300)] public function products(): array { ... } // 按登录状态区分 #[Cache(ttl: 600, guard: true)] public function recommendations(): array { ... } // 按用户 ID 区分 #[Cache(ttl: 300, userId: true)] public function myOrders(): array { ... } // 按权限区分 #[Cache(ttl: 600, abilities: ['commerce:view', 'commerce:manage'])] public function dashboardStats(): array { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key |
string|null | null |
缓存键(null 时自动生成) |
prefix |
string | '' |
缓存键前缀 |
ttl |
int|null | null |
有效期(秒),null 用缓存驱动默认 |
guard |
bool | false |
按登录状态区分缓存 |
userId |
bool | false |
按用户 ID 区分缓存 |
abilities |
array | [] |
按权限标识区分缓存 |
#[QueryMiddleware] — 类,查询级中间件
#[QueryMiddleware(priority: 50)] class BodyHashMiddleware implements MiddlewareInterface { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
priority |
int | 100 |
执行优先级(越小越先),同优先级按类名排序 |
#[FieldMiddleware] — 类,字段级中间件
#[FieldMiddleware(priority: 50)] class MyFieldMiddleware implements FieldMiddlewareInterface { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
priority |
int | 100 |
执行优先级(越小越先),同优先级按类名排序 |
#[GraphqlInterface] — 接口/类,标记 GraphQL InterfaceType
#[GraphqlInterface(name: 'Node', description: '节点接口')] interface NodeInterface { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string|null | null |
GraphQL 接口名(null 用类短名) |
description |
string|null | null |
接口描述 |
#[Union] — 类,标记 GraphQL UnionType
#[Union(name: 'SearchResult', description: '搜索结果联合类型')] class SearchResultUnion { public function types(): array { return [Product::class, Page::class]; } }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
string|null | null |
GraphQL 联合类型名(null 用类短名) |
description |
string|null | null |
联合类型描述 |
#[Guard](已废弃)
// ❌ 废弃,改用 #[Permission] #[Guard('admin')] class AdminResolver { ... } // ✅ 替代方案 #[Permission(['admin.access'])] class AdminResolver { ... }
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
driver |
string | '' |
认证驱动名('admin'、'customer') |
AuthResolverInterface已保证认证,#[Permission]做细粒度授权。
项目结构
graphql/src/
├── Annotation/ # 注解定义
│ ├── Arg.php
│ ├── Cache.php # 字段级缓存
│ ├── Field.php
│ ├── FieldMiddleware.php # 字段级中间件标记
│ ├── GraphqlInterface.php # GraphQL Interface 标记
│ ├── Guard.php # (已废弃)
│ ├── Input.php # Input 类型行为控制
│ ├── Mutation.php
│ ├── Permission.php # 权限检查
│ ├── Privacy.php # 字段级访问控制
│ ├── Query.php
│ ├── QueryMiddleware.php # 查询级中间件标记
│ ├── Rules.php # 验证规则
│ ├── Type.php
│ └── Union.php # GraphQL Union 标记
├── Contract/ # 接口契约
│ ├── ContainerInterface.php
│ ├── IdCodecInterface.php # ID 加解密契约
│ ├── ResolverInterface.php
│ └── ...
├── Converter/ # 类型转换器(策略模式)
│ ├── TypeConverterInterface.php
│ ├── TypeConverterFactory.php
│ ├── EnumTypeConverter.php
│ ├── ObjectTypeConverter.php # isIdFieldName() → id 自动推断
│ ├── InputTypeConverter.php # isIdFieldName() → id 自动推断
│ └── RootFieldConverter.php
├── Default/ # 默认实现
│ ├── NoopIdCodec.php # ID 加解密兜底(明文透传)
│ ├── SimpleContainer.php
│ └── ...
├── Director/ # 构建指导者
│ └── SchemaDirector.php # 注入 scalarOverrides 让 webonyx 用自定义 IDScalar
├── Driver/ # 驱动层
│ ├── ClassScanner.php
│ └── Driver.php # 传容器给 TypeRegistry(用于 resolveIDScalar)
├── Engine/ # 引擎
│ ├── EngineConfig.php
│ └── GraphQLEngine.php
├── Entity/ # 实体基类
│ └── EntityObject.php # 继承即自动成为 GraphQL Type
├── Mapper/ # 类型映射
│ └── PhpTypeMapper.php # resolveBaseType('id') 查 TypeRegistry 的 IDScalar
├── Metadata/ # 元数据检查
│ └── MetadataInspector.php # isGraphqlType() 父链查找 #[Type]
├── Pagination/ # 分页
│ ├── PaginationConstants.php # 常量(SUFFIX='Pagination', DEFAULT_PAGE=1, DEFAULT_PER_PAGE=15)
│ ├── PaginationType.php # 完整偏移分页(含 total/last_page)
│ ├── SimplePaginationType.php # 简单偏移分页(无 total)
│ ├── ConnectionBuilder.php # Relay 游标分页(edges+pageInfo)
│ ├── PageInfo.php # PageInfo 类型
│ └── PaginationHelper.php # 便捷工具(paginate/simplePaginate/cursorPaginate)
├── Resolver/ # 参数/返回值解析
│ └── ArgResolver.php # isIdParameter() → id 参数自动推断 + 分页参数自动追加
├── Type/
│ ├── IDScalar.php # 自定义 ID 标量(serialize=encode, parseValue=decode)
│ └── JsonType.php # JSON 标量类型
├── ReflectionHelper.php # 共享工具 trait
├── TypeKind.php # 类型分类枚举
├── TypeRegistry.php # 纯存储 + resolveIDScalar()
└── TypeResolver.php # 唯一决策点 + 唯一注册入口
API 端点
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /graphql |
执行 GraphQL 查询 |
| GET | /graphql |
Introspection 查询 |
| GET | /playground |
GraphQL Playground 调试界面 |