veasin/ff-ddd

DDD modeling layer: concept-config driven, separation of abstraction and data operations, built on the veasin/ff functional framework

Maintainers

Package info

github.com/veasin/ff-ddd

pkg:composer/veasin/ff-ddd

Transparency log

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.2 2026-07-31 22:07 UTC

This package is auto-updated.

Last update: 2026-07-31 22:08:19 UTC


README

基于 veasin/ff 函数式框架的 DDD 业务建模层。 概念配置驱动:抽象与数据操作分离 —— then 之前只构建概念配置,then 触发才执行数据操作。

安装

composer require veasin/ff-ddd

veasin/ff 为依赖项,框架函数(如 ff\dbff\container)通过 composer 的 autoload.files 自动加载。

核心概念

术语 说明
概念 领域模型最小单元,由 ddd::define() 定义,概念名全小写 / 分隔,如 corp/user
集合 概念的一组实例,可被范围方法分割,ddd::collection(name) 开链
单体 集合经 id()/create() 缩小到的唯一元素,ddd::entity(name) 开链
范围 对集合的筛选条件,链上每一步都在叠加
关系 概念间的关联(从属导航),触发根转化 + 范围转换
配置 概念的全部定义,then 前链式操作的最终产物

铁律

  1. 抽象与数据操作分离(then 前只构建配置)
  2. 集合 → 范围方法 → 新集合;单体 → 关系方法 → 集合/单体
  3. 一切显式,无自动推导(概念必须 define

配置结构

ddd::define(name, $colCfg, $entCfg) 将集合与单体配置分离为第二、三参数,各自独立归一化、独立存储,不合并。

子类型

// 范围项:在某字段 field 上施加一次筛选
//   op 缺省 'eq';顺序固定 [field, value, op?]
type ScopeItem = [field: string, value: unknown, op?: string];

// 范围定义 = 范围方法返回值:
//   · ScopeItem —— 字面范围项
//   · 闭包      —— 运行时追加范围项
type Def = ScopeItem | ((c: Concept) => void);

type Op = 'eq' | 'ne' | 'gt' | 'ge' | 'lt' | 'le' | 'like' | 'in' | 'not_in' | 'is_null' | 'not_null';

type RelationDef = {
  concept: string;                        // 目标概念(类名或注册概念名)
  via: Record<string, string>;            // map:targetField => currentField
};

// with 关联项:查询时 JOIN 目标概念,带出主表之外的字段
type WithJoin = {
  concept: string;                        // 目标概念(类名或注册概念名)
  via: Record<string, string>;            // map:targetField => currentField(JOIN ON)
  fields?: string[] | Record<string, string>; // 带出字段;缺省 = 全部(输出 别名.*);列表直接输出 别名.字段,key=>value 重命名
  type?: 'LEFT' | 'INNER' | 'RIGHT';      // join 类型;缺省 'LEFT'
};

集合配置(colConfig,第二参数)

interface ColConfig {
  base?:    string | null;                                         // 继承父集合;合并时子优先、子 null 删父对应项
  source:   { table: string; primary?: string; db?: string };      // 数据源;table 可带别名('posts p'),primary 默认 'id',db 默认 'default'
  defs:     Record<string, Def>;                                   // map:method => Def(范围方法定义)
  mutators: Record<string, (v: unknown) => unknown>;              // map:field => 闭包(persist 前作用)
  uniques:  string[];                                              // 唯一键;默认 ['id']
  order:    null | string | Record<string, 'asc' | 'desc'>;        // 默认排序
  entity?:  string;                                                // id()/create()/findUnique() fork 目标 entity 类
  trigger?: string;                                                // 自定义集合 Trigger 类
  with:     Record<string, Record<string, WithJoin>>;              // map:段名 => (别名 => 关联项);list/get 为默认段
  // —— 运行时状态(链式构建自动填充,不写死在定义中) ——
  scopes:   ScopeItem[];                                           // 已应用范围项
  update:   Record<string, unknown> | null;                        // 当前概念瞬态更新
  pending:  Pending[];                                             // 仅上游 create 态(内联 data 快照)
  refs:     Record<string, Record<string, unknown>>;               // concept => (field => value) 上游瞬态包
}
ddd::define('corp/user', [
    'source'    => ['table' => 'corp_users', 'primary' => 'id', 'db' => 'default'],
    'defs'      => ['active' => ['status', 1], 'newest' => ['created_at', null, 'desc']],
    'uniques'   => ['id', 'email'],
    'mutators'  => ['password' => static fn($v) => password_hash($v, PASSWORD_DEFAULT)],
    'order'     => 'created_at',
    'entity'    => UserEnt::class,
    // 'base'   => 'base/user',
], null);

单体配置(entConfig,第三参数)

interface EntConfig {
  base?:     string | null;                                         // 继承父单体;合并时子优先、子 null 删父对应项
  relations: Record<string, RelationDef>;                           // map:method => RelationDef(关系定义)
  mutators:  Record<string, (v: unknown) => unknown>;              // 优先读自身,fallback 到 colConfig
  uniques:   string[];                                              // 唯一键;优先读自身,fallback 到 colConfig
  with?:     Record<string, Record<string, WithJoin>>;              // 同 colConfig;定义时整数组遮蔽 col,未定义读 col
  trigger?:  string;                                                // 自定义实体 Trigger 类
  // —— 运行时状态(链式构建自动填充) ——
  scopes:    ScopeItem[];                                           // 已应用范围项
  create:    Record<string, unknown> | null;                        // 当前概念构造数据;存在即 create 态
  update:    Record<string, unknown> | null;                        // 当前概念瞬态更新
  pending:   Pending[];                                             // 仅上游 create 态(内联 data 快照)
  refs:      Record<string, Record<string, unknown>>;               // concept => (field => value) 上游瞬态包
}
ddd::define('corp/user', null, [
    'source'    => ['table' => 'corp_users', 'primary' => 'id', 'db' => 'default'],
    'relations' => ['tokens' => ['concept' => 'corp/user/token', 'via' => ['user_id' => 'id']]],
    'uniques'   => ['id', 'email'],
    'mutators'  => ['password' => static fn($v) => password_hash($v, PASSWORD_DEFAULT)],
    // 'base'   => 'base/user',
]);

字段归属

字段 colConfig entConfig 说明
source 实体构造时 ent→col fallback
defs 集合专属
relations 单体专属
uniques 实体构造时 ent→col fallback
mutators 实体构造时 ent→col fallback
with 独立存储不合并;读取时整数组 ent→col fallback
order 仅集合配置持有
base 同类型继承(col→col, ent→ent)

配置分离强制规则:colConfig 不含 relations/create;entConfig 不含 defs/entitynormalize() 阶段强制执行(传入即剥离)。

视图构造 fallback

实体构造时,最终配置 = entConfig 为基底,以下共享字段若 entConfig 不存在则从 colConfig 回退读取:

final.source   = entConfig.source   ?? colConfig.source   ?? 默认
final.uniques  = entConfig.uniques  ?? colConfig.uniques  ?? ['id']
final.mutators = entConfig.mutators ?? colConfig.mutators ?? []

共享字段仅 sourceuniquesmutators 三项。order 不属于共享字段——实体没有默认排序。

with 为特例:独立存储、不做构造期合并,仅在读取时由 concept 整数组 fallback(ent.with ?? col.with)。entity 定义了 with 则整数组遮蔽 col(不按段回退);未定义则读 col 的 with

base 继承

base 仅同类型继承(colConfig→父 colConfig,entConfig→父 entConfig),不跨类型:

  • 父项某 key 为数组(defs/relations)则逐项合并,子优先;子值为 null 删除父对应项
  • 非数组字段直接覆盖(with 属此类——整键替换,不做段级合并)
  • base 继承关系在 normalize() 阶段解析展开,展开后 base 字段被移除

正常化(normalize)拆分

集合和单体各自独立归一化:

  • normalizeColdefs[]mutators[]with[]scopes[]pending[]refs[]uniques['id'];剥离 relations/create
  • normalizeEntrelations[]scopes[]pending[]refs[];剥离 defs/entity/orderwith 不设默认值——未定义则不存,从而保证 concept 读取时能 fallback 到 col

with 查询级联

with 用于"查询主概念时同时 JOIN 关联概念、带出额外字段"。配置在集合(默认),单体可额外定义(整数组遮蔽集合)。

ddd::define('blog/post', [
    'source' => ['table' => 'posts', 'primary' => 'id'],
    'with'   => [
        'list' => [                       // list() 默认读取的段
            'author' => [                 // 别名 = join 表别名
                'concept' => 'blog/user',
                'via'     => ['user_id' => 'id'],  // targetField => currentField(ON 条件)
                'fields'  => ['name', 'avatar'],   // 带出字段;缺省 = 全部(别名.*);列表直接输出,key=>value 重命名
            ],
        ],
        'get' => [...],                   // get() 默认读取的段
        '自定义段' => [...],
    ],
], null);

返回形态:主表不设别名(字段用限定符限定:表带别名用别名、否则用表名),join 表别名 = 配置 key。fields 为字段列表时直接输出 别名.字段(不设别名);fields 为 输出键 => 字段 时重命名输出。join 生效时,ON / WHERE / ORDER BY 的字段一律用主表限定符限定(防同名列歧义):

SELECT `posts`.*, `author`.`name`, `author`.`avatar`
FROM `posts` LEFT JOIN `blog_users` `author` ON (`author`.`user_id` = `posts`.`id`)
WHERE `posts`.`status` = 1 ORDER BY `posts`.`created_at` DESC

trigger 签名(末尾参数 = with 段名):

  • list(array $options = [], ?string $with = 'list') —— null 禁用、其他字符串读自定义段
  • get(array $options = [], ?string $with = 'get') —— 同上

规则:

  • with 生效时 options['select'] 被忽略(带出字段由 with 生成)
  • 分页 list() 的 count 始终单表单表,不 join
  • $with === null 回退普通单表查询
  • join 目标概念无数据源或 via 为空 → DomainException(铁律 #5 构建错误致命化)
ddd::collection('blog/post')->then->list(['page' => 1]);       // list 段
ddd::collection('blog/post')->then->list(['page' => 1], 'custom'); // 自定义段
ddd::collection('blog/post')->then->list(['page' => 1], null); // 禁用
ddd::collection('blog/post')->id(5)->then->get();              // get 段

自定义 Trigger 扩展

业务方法(如 launch()toggleSkip())属于"找到实体后的数据操作",应放在 Trigger 上。通过 lv0 配置注入 trigger 即可,不必绕三层继承:

use ff\ddd\ddd;
use ff\ddd\trigger\entity as entityTrigger;

// 1. 自定义 Trigger:继承单体触发基类,写业务方法
class DirsTrigger extends entityTrigger {
    public function launch(): array {
        $dir = $this->get();            // 复用原生 get()
        // ... 执行业务逻辑(文件系统、exec 等)
        return $dir;
    }
}

// 2. lv0 配置注入 trigger 类名
ddd::define('dirs', [
    'source'  => ['table' => 'dirs', 'primary' => 'id'],
    'trigger' => DirsTrigger::class,
]);

// 3. 使用:链式走到 then,自动返回自定义 Trigger 实例
$dir = ddd::collection('dirs')->id(5)->then->launch();

同样支持集合 Trigger 扩展(继承 trigger\collection)。若需自定义 Entity(如有额外导航方法),配置 entity 指定 fork 目标:

ddd::define('dirs', [
    'source' => ['table' => 'dirs', 'primary' => 'id'],
    'entity' => DirsEntity::class,   // id()/create()/findUnique() fork 到 DirsEntity
]);

快速开始

use function ff\{container, db};
use ff\ddd\ddd;

// 1. 注册概念(集合与单体配置分离)
ddd::define('corps', [
    'source'   => ['table' => 'corps', 'primary' => 'id'],
], [
    'relations'=> ['users' => ['concept' => 'corp/users', 'via' => ['id' => 'corp_id']]],
]);
ddd::define('corp/users', [
    'source'   => ['table' => 'corp_users', 'primary' => 'id'],
    'order'    => ['created_at' => 'desc'],
    'uniques'  => ['id', 'email'],
    'mutators' => ['password' => static fn($v) => password_hash($v, PASSWORD_DEFAULT)],
    'defs'     => ['active' => ['status', 1], 'newest' => ['created_at', null, 'desc']],
], [
    'relations'=> ['tokens' => ['concept' => 'corp/user/token', 'via' => ['user_id' => 'id']]],
]);
ddd::define('corp/user/token', [
    'source' => ['table' => 'corp_user_tokens', 'primary' => 'id'],
]);

// 2. 列表(集合,范围方法 active() 来自配置的 defs)
ddd::collection('corp/users')->active()->then->get();

// 3. 读取单体
ddd::collection('corp/users')->id(5)->then->get();

// 4. 新建(一级 rules:unique/mutators 自动生效)
ddd::collection('corp/users')->create(['email' => 'a@b.com', 'password' => 'secret'])->then->persist();

// 5. 关联创建(依赖链自动处理:先 persist user 得 id,再回填 token.user_id)
ddd::collection('corps')
    ->id(5)
    ->users()
    ->create(['email' => 'u@b.com', 'password' => 'x'])
    ->tokens()
    ->create(['token' => 'xxx', 'client' => 'web'])
    ->then->persist();   // 返回 token 的持久化结果

// 6. 更新 / 删除 / 计数 / 存在性
ddd::collection('corp/users')->id(5)->update(['nick_name' => '新名'])->then->persist();
ddd::collection('corp/users')->id(5)->then->delete();
ddd::collection('corp/users')->active()->then->count();
ddd::collection('corp/users')->findUnique(['email' => 'a@b.com'])->then->exists();

// 7. 配置继承(base)
ddd::define('corp/vip/users', [
    'base'   => 'corp/users',
    'defs'   => ['vip' => ['is_vip', 1]],
]);
ddd::collection('corp/vip/users')->vip()->then->get();

// 8. 显式定义(类形态)
class VipUsers extends \ff\ddd\collection{
    public const array CONCEPT = [
        'base'   => 'corp/users',
        'defs'   => ['vip' => ['is_vip', 1]],
    ];
}
ddd::define('corp/vip2/users', VipUsers::class);
ddd::collection('corp/vip2/users')->vip()->then->get();

// 9. with 查询级联(list 默认段;自定义段名见上文)
ddd::define('blog/post', [
    'source' => ['table' => 'posts', 'primary' => 'id'],
    'with'   => ['list' => ['author' => ['concept' => 'blog/user', 'via' => ['user_id' => 'id'], 'fields' => ['name']]]],
]);
ddd::collection('blog/post')->then->list(['page' => 1]);  // JOIN 带出 author.name

IDE 辅助

# 自动生成 .phpstorm.meta.php + _ddd.php(桩类命名空间 _ddd)
php vendor/bin/ddd.php

# 监控模式:文件变动自动重生成
php vendor/bin/ddd.php --watch

生成 PhpStorm 元数据(override / expectedArguments / registerArgumentsSet), 以及 IDE 桩类(_ddd 命名空间,@method 标注 defs / relations / 自定义方法)。 支持 lv1 class 反射 + base 继承链合并。collection()entity() 各自独立提示列表。