Search by

veasin / ff-ddd

vea

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

Package info

github.com/veasin/ff-ddd

pkg:composer/veasin/ff-ddd

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.3.1 2026-11-11 03:31 UTC

This package is auto-updated.

Last update: 2026-11-11 03:32:24 UTC


README

基于 veasin/ff 函数式框架的 DDD 业务建模层。 概念配置驱动:抽象与数据操作分离 —— db 静态类调用前只构建概念配置,db 静态类负责数据操作。

安装

composer require veasin/ff-ddd

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

核心概念

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

铁律

  1. 抽象与数据操作分离(db 调用前只构建配置)
  2. 数据操作统一走 ff\ddd\db 静态类:save / get / list / count / delete / exists
  3. 一切显式,无自动推导(概念必须 define)

配置结构

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

子类型

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

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

// 操作符直接使用 SQL 符号 / 关键字,不做别名映射
type Op = '=' | '!=' | '<>' | '<' | '<=' | '>' | '>='
        | '+' | '-' | '*' | '/' | '%'
        | 'LIKE' | 'IN' | 'NOT IN' | 'IS NULL' | 'IS NOT NULL';

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

// 视图项:source.views 的段内项;有 table 即数据形态,否则按语义形态引用 relations
type ViewItem = SemanticView | DataView;

type SemanticView = {
  relation?: string;                      // relations 的键;缺省 = 外层 key(同时作为 SQL 别名)
  fields?: string[] | Record<string, string>; // 目标概念的语义字段
  type?: 'LEFT' | 'INNER' | 'RIGHT';      // 缺省 'LEFT'
};

type DataView = {
  table: string;                          // 目标表
  on: Record<string, string>;             // 目标列 => 当前列(原始列名)
  fields?: string[] | Record<string, string>; // 原始列
  type?: 'LEFT' | 'INNER' | 'RIGHT';      // 缺省 'LEFT'
};

source 配置

source 是概念 → 数据的适配层,可写在 colConfig 与 entConfig 中;读取时按项递归合并(ent 优先)。产物直接符合 ff\sql() 数组参数格式。

interface SourceConfig {
  table: string;                                    // 表名;可带别名 'posts p'
  primary?: string;                                 // 主键列;fields['#'] 优先
  db?: string;                                      // 连接名;默认 'default'
  time?: 's' | 'ms';                                // 时间戳单位;默认 's'
  fields?: Record<string, string>;                  // 语义字段 => 列;'#' 为主键保留键
  timestamps?: TimestampsConfig;                    // 完整写法
  '@'?: TimestampsShort;                            // 简写;按项覆盖 timestamps 的对应项
  views?: Record<string, Record<string, ViewItem>>; // 段名 => (别名/关系名 => 连接项)
}

type TimestampsConfig = {
  created?: boolean | string | null;                // true => created_at;写入 BIGINT 时间戳
  updated?: boolean | string | null;                // true => updated_at
  deleted?: boolean | string | null;                // true => deleted_at;启用即软删除,0 = 存活
};

type TimestampsShort = {
  '+'?: boolean | string | null;                    // created
  '~'?: boolean | string | null;                    // updated
  '-'?: boolean | string | null;                    // deleted
};
  • 配置层使用 table / fields;解析产物使用 from / MAP。
  • fields 方向固定为 语义字段 ⇒ 列名;主键键固定为 #,不写 id。
  • 未列出的字段按恒等映射处理;映射作用于 WHERE / ORDER / JOIN / INSERT / UPDATE。
  • @['-'] 启用时,读取、更新、删除、join 追加 deleted = 0;未启用时 delete 抛 DomainException。
  • 开放域(time / db / type / on / fields)原样透传,不做校验或转换;table 原样写入产物 from。
  • views 详见下文「views 查询级联」。

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

interface ColConfig {
  base?:    string | null;                                         // 继承父集合;合并时子优先、子 null 删父对应项
  source:   SourceConfig;                                          // 数据源;详见「source 配置」
  defs:     Record<string, Def>;                                   // map:method => Def(范围方法定义)
  mutators: Record<string, (v: unknown) => unknown>;              // map:field => 闭包(persist 前作用)
  uniques:  string[];                                              // 唯一键;默认 ['id'],仅 findUnique() 校验
  order:    null | string | Record<string, 'asc' | 'desc'>;        // 默认排序
  entity?:  string;                                                // id()/create()/findUnique() fork 目标 entity 类
  // —— 运行时状态(链式构建自动填充,不写死在定义中) ——
  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' => 'user_id',
        'db'      => 'default',
        'fields'  => ['#' => 'user_id', 'nick' => 'nick_name'],
        '@'       => ['+' => true, '~' => true, '-' => true],
        'time'    => 'ms',
        'views'   => ['list' => ['tokens' => ['fields' => ['token']]]],
    ],
    'defs'      => ['active' => ['status', 1], 'vip' => ['is_vip', 1]],
    '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 删父对应项
  source?:   SourceConfig;                                          // 与 col.source 按项递归合并,ent 优先
  relations: Record<string, RelationDef>;                           // map:method => RelationDef(关系定义)
  mutators:  Record<string, (v: unknown) => unknown>;              // 优先读自身,fallback 到 colConfig
  uniques:   string[];                                              // 优先读自身,fallback 到 colConfig
  order?:    null | string | Record<string, 'asc' | 'desc'>;        // 可选覆盖集合排序
  // —— 运行时状态(链式构建自动填充) ——
  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 优先
defs ✅ ❌ 集合专属
relations ❌ ✅ 单体专属
uniques ✅ ✅ 实体构造时 ent→col fallback;仅 findUnique() 校验
mutators ✅ ✅ 实体构造时 ent→col fallback
order ✅ ✅ 集合默认排序;ent 可覆盖,读取时 ent→col fallback
base ✅ ✅ 同类型继承(col→col, ent→ent)

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

视图构造 fallback

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

final.source   = mergeSource(colConfig.source, entConfig.source)   // 按项递归合并,ent 优先
final.uniques  = entConfig.uniques  ?? colConfig.uniques  ?? ['id']
final.mutators = entConfig.mutators ?? colConfig.mutators ?? []

共享字段仅 source、uniques、mutators 三项。order 不属于共享字段——实体没有默认排序。source 内的 views 随 source 一并按段名、段内别名递归合并。

base 继承

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

  • defs / relations 按项合并,子优先;子值为 null 删除父对应项
  • source 递归按项合并(fields / timestamps / @ / views 逐层合并),子优先
  • 其他非数组字段直接覆盖
  • base 继承关系在 normalize() 阶段解析展开,展开后 base 字段被移除

正常化(normalize)拆分

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

  • normalizeCol:defs→[],mutators→[],scopes→[],pending→[],refs→[];uniques→['id'];剥离 relations/create
  • normalizeEnt:relations→[],scopes→[],pending→[],refs→[];剥离 defs/entity

views 查询级联

views 用于"查询主概念时同时 JOIN 关联目标、带出主表之外的字段",配置在 source.views 中。段名由 db::get() / db::list() 的末参数选择,默认分别为 get / list。

ddd::define('blog/post', [
    'source' => [
        'table'   => 'posts',
        'primary' => 'id',
        'views'   => [
            'list' => [                         // list() 默认读取的段
                'author' => [                   // 别名 = join 表别名;key 同时是 relations 键
                    'fields' => ['name', 'avatar'],
                    'type'   => 'LEFT',
                ],
                'stats' => [                    // 有 table => 数据形态
                    'table'  => 'post_stats',
                    'on'     => ['post_id' => 'id'], // 目标列 => 当前列(原始列名)
                    'fields' => ['view_count' => 'views'],
                ],
            ],
            'get' => [...],                     // get() 默认读取的段
            '自定义段' => [...],
        ],
    ],
], [
    'relations' => ['author' => ['concept' => 'blog/user', 'via' => ['user_id' => 'id']]],
]);

形态判别:视图项含 table 即数据形态;否则为语义形态,按 relations 生成目标表与 ON。语义形态的 on 忽略,数据形态的 relation 忽略——结构域宽松归一,不抛异常。

返回形态:主表 fields = [](多表输出 表.*,单表退化为 *);join 字段放进 join[]['fields']。语义形态的 ON 使用符号操作符,目标列带别名、主表列经 MAP 映射为裸列名:

SELECT `posts`.*, `author`.`name` `name`, `author`.`avatar` `avatar`, `stats`.`views` `view_count`
FROM `posts`
LEFT JOIN `blog_users` `author` ON (`author`.`user_id` = `posts`.`id` AND `author`.`deleted_at` = 0)
LEFT JOIN `post_stats` `stats` ON (`stats`.`post_id` = `posts`.`id`)
WHERE `posts`.`status` = 1

db 静态类签名(末尾参数 = 视图段名):

  • db::get(entity $node, array $opt = [], ?string $view = null) —— 缺省 / null 读 get 段,其他字符串读自定义段
  • db::list(collection $node, array $opt = [], ?string $view = null) —— 缺省 / null 读 list 段,同上
  • db::save(collection|entity $node, array $opt = []): int|bool|null
  • db::count($node): int
  • db::delete(collection|entity $node): bool
  • db::exists(entity $node): bool

规则:

  • 视图生效时 options['fields'] 被忽略(带出字段由视图生成)
  • 分页 list() 的 count 始终单表,不 join
  • db::get / db::list 传 null 仍读取默认段($view ?? 'get' / 'list');要禁用 join 请传入不存在的段名
  • 语义形态引用的 relations 键不存在、目标概念未注册、via 为空 → DomainException
  • 目标概念启用 @['-'] 时,ON 追加 目标别名.<deleted> = 0;数据形态不感知软删除
db::list(ddd::collection('blog/post'), ['page' => 1]);                          // list 段
db::list(ddd::collection('blog/post'), ['page' => 1], 'custom');               // 自定义段
db::list(ddd::collection('blog/post'), ['page' => 1], 'none');                 // 段名不存在:不启用 join
db::get(ddd::collection('blog/post')->id(5));                                   // get 段

数据操作 API

数据操作统一走 ff\ddd\db 静态类,直接调用 ff\db() 执行 SQL,不经 ext 派发:

use ff\ddd\db;

// 创建 / 更新
db::save(ddd::collection('corp/users')->create(['email' => 'a@b.com', 'password' => 'x']));
db::save(ddd::collection('corp/users')->id(5)->update(['nick_name' => '新名']));

// 查询
db::get(ddd::collection('corp/users')->id(5));
db::list(ddd::collection('corp/users')->active(), ['page' => 1, 'max' => 20], 'list');

// 计数 / 存在性 / 删除
db::count(ddd::collection('corp/users')->active());
db::exists(ddd::collection('corp/users')->findUnique(['email' => 'a@b.com']));
db::delete(ddd::collection('corp/users')->id(5));

管道符兼容:$node |> db::save(...)。

自定义领域方法

业务方法(如 launch()、toggleSkip())属于"找到实体后的数据操作",可直接写在领域服务类中,内部调用 db::*:

use ff\ddd\db\db;

class DirsService {
    public function launch(int $id): array {
        $dir = db::get(ddd::collection('dirs')->id($id));
        // ... 执行业务逻辑(文件系统、exec 等)
        return $dir;
    }
}

若需自定义 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], 'vip' => ['is_vip', 1]],
], [
    '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)
db::get(ddd::collection('corp/users')->active());

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

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

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

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

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

// 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);
db::get(ddd::collection('corp/vip2/users')->vip());

// 9. views 查询级联(list 默认段;自定义段名见上文)
ddd::define('blog/post', [
    'source' => [
        'table' => 'posts',
        'primary' => 'id',
        'views' => ['list' => ['author' => ['fields' => ['name']]]],
    ],
], [
    'relations' => ['author' => ['concept' => 'blog/user', 'via' => ['user_id' => 'id']]],
]);
db::list(ddd::collection('blog/post'), ['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() 各自独立提示列表。