Waaseyaa 的核心实体系统

发布: (2026年3月23日 GMT+8 09:40)
8 分钟阅读
原文: Dev.to

Source: Dev.to

(未提供需要翻译的文本。如需翻译,请提供要翻译的内容。)

概览

系列背景: 这是Waaseyaa 系列的第 3 部分。请阅读系列介绍以了解概览,以及协同开发治理页面了解多仓库工作流的治理方式。

Drupal 对 PHP 内容管理系统的最大贡献并非其 UI 或模块生态系统——而是实体/字段模型。内容类型是已配置的类型化字段的组合,任何内容类型都可以拥有任意字段,且字段拥有自己的存储和验证逻辑,这使得 Drupal 足够灵活,能够对几乎任何内容域进行建模。

Waaseyaa 继承了这一模型,并针对 PHP 8.4+ 进行了重写,使用了现代类型声明和 Symfony 的依赖注入。本篇文章将介绍实体系统的工作原理,以及结构化 AI 上下文如何使其在多个会话中可构建且不失去架构一致性。

Source:

EntityInterface

框架中的每个实体都实现 EntityInterface。该接口的约定非常简洁:

interface EntityInterface
{
    public function id(): int|string|null;
    public function uuid(): string;
    public function label(): string;
    public function getEntityTypeId(): string;
    public function bundle(): string;
    public function isNew(): bool;
    public function toArray(): array;
    public function language(): string;
}

关于此约定有几件事会影响框架其余部分的工作方式:

  • id() 返回 int|string|null —— 之所以可为 null,是因为尚未保存的新实体没有 ID。isNew() 正是依据这一点:当实体的 ID 为 null(或通过 enforceIsNew() 明确强制)时,实体被视为新建。
  • bundle() 用于实现实体子类型。比如 node 实体类型可以拥有 articlepage 等 bundle,每个 bundle 拥有不同的字段定义。当未设置 bundle 键时,bundle 默认使用实体类型 ID 本身。
  • toArray() 是由 API 层的 ResourceSerializer 使用的序列化约定。它返回的结构决定了 JSON:API 响应的构建方式。处理 API 层的会话需要同时加载实体系统技能和 API 层技能,以理解这两个约定之间的交互。

Source:

ContentEntityBase

ContentEntityBase 是抽象基类,为大多数 EntityInterface 提供默认实现。它继承自 EntityBase 并实现 ContentEntityInterface,后者将 EntityInterfaceFieldableInterface 组合在一起。自定义实体类型应当继承此类,而 不是 直接实现 EntityInterface

abstract class ContentEntityBase extends EntityBase implements ContentEntityInterface
{
    protected array $fieldDefinitions = [];

    public function __construct(
        array $values = [],
        string $entityTypeId = '',
        array $entityKeys = [],
        array $fieldDefinitions = [],
    ) {
        parent::__construct($values, $entityTypeId, $entityKeys);
        $this->fieldDefinitions = $fieldDefinitions;
    }

    public function hasField(string $name): bool
    {
        return array_key_exists($name, $this->values)
            || array_key_exists($name, $this->fieldDefinitions);
    }

    public function get(string $name): mixed
    {
        return $this->values[$name] ?? null;
    }

    public function set(string $name, mixed $value): static
    {
        $this->values[$name] = $value;
        return $this;
    }
}
  • 实体值 存储在 $values 数组中,字段访问通过 get()set() 完成,而不是使用专门的字段对象。
  • hasField() 同时检查值数组和字段定义——即使字段尚未有值,也可以被定义。这使实体保持轻量:在 v0.1.0 中,字段值是数组中的原始值,完整的 FieldItemList 集成计划在 waaseyaa/field 包中实现。

Source:

字段系统

字段系统位于 waaseyaa/field 包中。每种字段类型实现 FieldItemInterface

interface FieldItemInterface extends ComplexDataInterface
{
    public function isEmpty(): bool;
    public function getFieldDefinition(): FieldDefinitionInterface;
    /** @return string[] */
    public static function propertyDefinitions(): array;
    public static function mainPropertyName(): string;
}
  • 字段项是带有自身属性结构的类型化数据对象。
  • propertyDefinitions() 声明字段类型所携带的属性。
  • mainPropertyName() 标识主要属性(通常为 'value')。

每种字段类型还提供 schema()jsonSchema() 方法,用于存储和 API 序列化。

框架为常见情况提供了以下字段项类型:

  • StringItem
  • TextItem
  • IntegerItem
  • BooleanItem
  • FloatItem
  • EntityReferenceItem

每个类型都使用 #[FieldType] 属性进行注解,声明其 ID、标签、描述以及默认基数。

示例:Skill 实体(Claudriel)

Claudriel 中的 Skill 实体展示了实体类型的实际工作方式:

final class Skill extends ContentEntityBase
{
    protected string $entityTypeId = 'skill';

    protected array $entityKeys = [
        'id'    => 'sid',
        'uuid'  => 'uuid',
        'label' => 'name',
    ];

    public function __construct(array $values = [])
    {
        parent::__construct($values, 'skill', $this->entityKeys);
    }
}

这就是 薄应用模式 的实际体现。Claudriel 通过实体键定义实体类型——映射告诉框架哪些值数组键对应实体的 ID、UUID 和标签。框架提供基类、UUID 处理以及字段访问助手,而应用仅需提供使实体可用的最少元数据。

自动生成、序列化和 API 端点

应用层面的更改(添加新实体类型或调整其键)仍然保持在应用层。

实体工厂

对于测试,waaseyaa/testing 包提供了 EntityFactory —— 一个生成实体值数组的测试数据生成器:

$factory = new EntityFactory();
$factory->define('skill', [
    'name'   => 'Default skill',
    'status' => 1,
]);

$values = $factory->create('skill', ['name' => 'Custom']);
// => ['name' => 'Custom', 'status' => 1]

工厂的 create(string $entityTypeId, array $overrides = []): array 方法将已注册的默认值与每个测试的覆盖值合并。它还支持:

  • sequence() 回调用于在多个实体之间生成唯一值。
  • createMany() 用于批量创建。

注意: 这只是一个测试工具,不是生产服务 —— 应用代码中的实体类型直接通过 new Skill($values) 构造。

专业技能的作用

实体系统技能承载上述知识 —— 接口契约、值数组模式、实体键映射以及常见错误。

在该技能出现之前,处理实体系统的会话偶尔会生成以下代码:

  • 使用了错误的方法名。
  • 创建了不存在的类。
  • 误解了通过 get()set() 访问字段的工作方式。

有了该技能后,这些错误就停止了。会话在任何涉及 packages/entity/ 的会话开始时加载该技能,拥有接口契约和行为规则,并生成符合系统的代码。

推动最初实体系统构建的 GitHub Issue 明确限定了工作范围:

  • EntityInterface
  • EntityBase
  • ContentEntityBase
  • FieldableInterface
  • 六种核心字段项类型
  • 测试工厂

没有超出该范围的内容。当会话偏离去添加验证规则或存储适配器——这些工作属于后期里程碑——时,Issue 的范围将它们拉回。

这种组合 —— Issue 范围 加上 已编码的上下文 —— 使得在数十个会话中管理复杂的框架开发成为可能。

Next: Building a temporal layer so your AI never lies about time

Baamaapii

0 浏览
Back to Blog

相关文章

阅读更多 »