Search by

maiscraft / graphql

mslbit

PHP 8 Attribute + Reflection driven GraphQL Schema auto-generation engine. Framework-agnostic, with strategy pattern type conversion and container DI.

Package info

github.com/mslbit/graphql

pkg:composer/maiscraft/graphql

Statistics

Installs: 0

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-06-21 13:47 UTC

This package is auto-updated.

Last update: 2026-08-27 11:28:29 UTC


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 配置才会用自定义标量替代内置 IDSchemaDirector::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 IDID!(按 nullable) ArgResolver::isIdParameter()
Output 字段 字段名 === 'id' + PHP int IDID!(按 nullable) ObjectTypeConverter::isIdFieldName()
Input 字段 字段名 === 'id' + PHP int IDID!(按 nullable) InputTypeConverter::isIdFieldName()

不自动推断的情况:

  • 外键字段:product_idcategoryId*_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!JSONProductPagination
description string|null null 字段描述
hidden bool false 隐藏,不输出到 Schema
readonly bool false 仅 Output(排除 Input),适用于系统生成字段
writeonly bool false 仅 Input(排除 Output),适用于写入字段
view array [] 视图限定(空=所有视图,['list'] =仅 list 视图)

id 字段(PHP int)自动推断为 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 返回类型推导 返回类型名(支持 PagePaginationPagination<Page> 等)

#[Arg] — 参数,定义字段参数

#[Arg(name: 'filters', type: '[FilterInput!]', description: '过滤条件')]
array $filters = []
属性 类型 默认值 说明
name string 参数名 GraphQL 参数名
type string 自动推导 GraphQL 类型(支持 ID![FilterInput!] 等)
description string null 参数描述

id 参数(PHP int)自动推断为 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

授权流程:AuthResolverInterfaceFieldSecurityMiddlewarePermission::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 调试界面