Waaseyaa 的核心实体系统
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实体类型可以拥有article或page等 bundle,每个 bundle 拥有不同的字段定义。当未设置 bundle 键时,bundle 默认使用实体类型 ID 本身。toArray()是由 API 层的ResourceSerializer使用的序列化约定。它返回的结构决定了 JSON:API 响应的构建方式。处理 API 层的会话需要同时加载实体系统技能和 API 层技能,以理解这两个约定之间的交互。
Source: …
ContentEntityBase
ContentEntityBase 是抽象基类,为大多数 EntityInterface 提供默认实现。它继承自 EntityBase 并实现 ContentEntityInterface,后者将 EntityInterface 与 FieldableInterface 组合在一起。自定义实体类型应当继承此类,而 不是 直接实现 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 序列化。
框架为常见情况提供了以下字段项类型:
StringItemTextItemIntegerItemBooleanItemFloatItemEntityReferenceItem
每个类型都使用 #[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 明确限定了工作范围:
EntityInterfaceEntityBaseContentEntityBaseFieldableInterface- 六种核心字段项类型
- 测试工厂
没有超出该范围的内容。当会话偏离去添加验证规则或存储适配器——这些工作属于后期里程碑——时,Issue 的范围将它们拉回。
这种组合 —— Issue 范围 加上 已编码的上下文 —— 使得在数十个会话中管理复杂的框架开发成为可能。
Next: Building a temporal layer so your AI never lies about time
Baamaapii