service 层越写越像"万能垃圾桶"?我用"输入校验→编排→防腐"三步法重新理清了 controller/service/model 的边界
之前分层拆完,controller 里还是塞满了 `if ($user->role === 'xxx')` 这种业务判断,service 层则沦为"数据库透传管道",model 除了 `get_results` 啥也不干。表面上三层文件都有了,实际一改动需求,三处一起改,分层了个寂寞。
后来我把 service 层的职责强行压成三条线,才慢慢把"伪分层"扭过来。
第一条:输入校验与转换,controller 只做"安检门"
以前我在 controller 里直接拿 `$_POST` 往 service 丢,结果 service 里到处 `isset`、`intval` 满天飞。现在 controller 用个轻量 DTO 对象把原始请求"洗"一遍,非法字段直接拦在外面,service 拿到的已经是"干净且带类型"的数据。
// controller 里只做这件事
$dto = new OrderCreateDTO($_POST);
if (!$dto->validate()) {
wp_send_json_error(['fields' => $dto->errors()]);
}
// service 拿到的是已经校验过的对象
$order = $orderService->create($dto);
controller 不碰业务规则,只负责"这包数据能不能进大门"。
第二条:service 做"编排",但禁止直接碰 SQL
我给自己定了个死规矩:service 层只能调 model 的"领域方法",不能用 `$wpdb->query`。比如创建订单不是 `insert_order_record`,而是 `OrderModel::createPending($dto->toArray())`。
这样 model 层开始真正长出血肉——它知道自己有哪些状态、哪些字段组合是合法的,而不是 service 在拼 SQL 字符串。
// OrderService.php
public function create(OrderCreateDTO $dto): OrderModel
{
// 编排:锁库存 -> 创建订单 -> 写日志
return $this->transaction(function() use ($dto) {
$inventory = $this->inventoryModel->lock($dto->sku, $dto->qty);
$order = OrderModel::createPending($dto->toArray());
$this->eventLog->record('order.created', $order->id);
return $order;
});
}
service 管"先做哪步后做哪步",model 管"这一步怎么落地"。
第三条:防腐层,隔离 WordPress 的"怪癖"
以前 `get_option`、`update_option` 散落在 controller 和 service 各处,缓存失效、序列化坑、多站点前缀问题到处埋雷。现在我把所有 WordPress 原生 API 的调用都收敛到 model 层的"网关方法"里,上层完全感知不到 `option_name` 前面有没有 `_site_transient_timeout_` 这种鬼东西。
// 上层只看到这个
$pluginConfig = ConfigModel::get('shipping_rules');
// 底层处理所有 WordPress 的脏活
class ConfigModel {
public static function get(string $key): mixed {
$raw = get_site_option(self::prefix($key));
return self::unserializeSafely($raw);
}
}
这层"防腐"让单元测试好写很多——mock 一个 ConfigModel 就行,不用去劫持 WordPress 的全局函数。
一个反直觉的发现
拆干净之后,controller 反而变厚了一点——不是业务代码变多,而是显式声明变多了:校验规则、权限断言、响应格式转换,这些以前隐身散落在各层的"暗逻辑",现在全部摊在 controller 的明面上。而 service 和 model 变薄了,变"呆"了,反而好测、好复用。
你们分层的时候,有没有遇到过"拆完还是一团,只是分成了三个文件"的情况?最后是怎么把真正的边界切出来的?

