controller 里塞了 200 行 SQL、service 沦为"传话筒":我把插件重构三次后才摸到的分层底线

插件开发 25 浏览 0 回复 返回上级

第一次拆插件的时候,我信心满满:搞个 controller/service/model/ 三个文件夹,名字一贴,专业感拉满。结果三个月后翻代码,controller 里躺着 180 行的复杂查询,service 只是 return $this->model->get_xxx(),model 倒是干净——因为所有脏活都被 controller 截胡了。

那次重构让我意识到:分层不是"文件物理分开",是"职责边界画清楚"。后来我又拆了两次,才慢慢稳住一套能跑的生产结构。这篇不聊框架,只聊 WordPress 插件里怎么让这三层不互相"越境"。

一、controller 的"天花板":只负责"接请求、调服务、吐响应"

我最开始的典型反例:

class Order_Controller {
    public function export_csv() {
        global $wpdb;
        $orders = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}orders WHERE status = 'paid' AND created_at > '2024-01-01'" );
        // 还要处理权限、格式转换、header 输出...
        header('Content-Type: text/csv');
        // 80 行后...
    }
}

controller 干了四件事:鉴权、查库、业务过滤、输出格式。后来我的硬规矩:

  • controller 可以 current_user_cancheck_ajax_refererwp_send_json
  • controller 不可以出现 $wpdb->、不可以出现业务计算、不可以出现"如果 A 状态且 B 条件则 C"的复合判断

改完后的骨架:

public function export_csv() {
    if ( ! current_user_can( 'manage_woocommerce' ) ) {
        wp_send_json_error( '权限不足', 403 );
    }
    
    $filters = $this->sanitize_filters( $_GET ); // 只干"洗干净输入"
    
    try {
        $csv = $this->order_service->generate_csv( $filters );
        wp_send_json_success( [ 'download_url' => $csv->get_url() ] );
    } catch ( \Exception $e ) {
        wp_send_json_error( $e->getMessage(), 500 );
    }
}

controller 变"薄"之后,单元测试好写了——mock service 就行,不用搭数据库。

二、service 的"护城河":业务规则的"唯一真相源"

我踩过的第二个坑:两个 controller 都要"计算积分",一个写在 User_Controller 里,一个写在 Task_Controller 里,公式改了要两头找。

现在 service 层是我强制要求的"业务规则集中营"。比如积分计算:

class Credit_Service {
    public function calculate_earn( $action, $context ) {
        $base = $this->get_action_base( $action );
        $multiplier = $this->get_context_multiplier( $context );
        $daily_cap = $this->get_user_daily_cap( $context['user_id'] );
        
        $raw = $base * $multiplier;
        return min( $raw, $daily_cap - $this->get_today_earned( $context['user_id'] ) );
    }
}

关键原则:任何"如果用户是 VIP 则双倍"这种会变的需求,必须落在 service,不能散落在 controller 或 model。model 只回答"数据是什么",service 回答"业务上这意味着什么"。

三、model 的"自闭"原则:只跟数据库说话,不跟业务逻辑说话

我现在的 model 层基本只做三件事:

  1. 把 service 传来的参数转成 SQL/ORM 操作
  2. 处理 WordPress 特有的表前缀、多站点切换、时区转换
  3. 返回原始数据或简单聚合,不做"业务含义解释"

举个对比。以前我的 model:

public function get_orders( $user_id ) {
    // 还要判断是不是管理员、要不要看子订单...
}

现在:

public function find_by_user( int $user_id, array $status_in = [], int $limit = 20 ) {
    // 参数进来是什么,就按什么查
    // 权限判断?那是 service 决定"能不能查"之后的事
}

model 的方法名我也改了风格:从 get_user_valid_orders_for_export 变成 find_by_user_with_status。前者带业务意图,后者只描述数据行为。业务意图往上浮,数据行为往下沉。

四、WordPress 特有毒点:钩子注册放哪层?

这我纠结最久。现在我的做法:

  • 钩子入口:plugin 主文件或 bootstrap 类,只做 add_action/add_filter 的"接线"
  • 钩子回调:通常是 controller 的 public 方法,或者专门的 Hook_Handler 做薄转发
  • 绝不:在 service 或 model 里直接 add_action

为什么?因为 WordPress 钩子是"全局副作用",service 如果被钩子在半路上触发,上下文不可控。把钩子收敛到入口层,才能一眼看清"这个插件到底在哪些时机介入了系统"。

五、一个能跑的小目录,供拍砖

my-plugin/
├── my-plugin.php          // 唯一入口:注册钩子、初始化容器
├── bootstrap.php          // 自动加载、常量、容器绑定
├── src/
│   ├── Controller/
│   │   ├── Admin/         // wp-admin 下的页面请求
│   │   ├── Ajax/          // admin-ajax.php 端点
│   │   └── Rest/          // REST API 端点
│   ├── Service/           // 业务规则、流程编排
│   ├── Model/             // 数据访问、WordPress 封装
│   ├── Domain/            // 纯实体、值对象(可选,复杂插件才上)
│   └── Infrastructure/    // 缓存、日志、第三方 SDK 包装
└── assets/

没有 includes/ 这种"什么都往里扔"的筐,也没有 functions/ 这种过程式遗产。每个文件进去,从命名就知道该找哪层。

六、还没想清楚的:service 之间的调用链

现在我的痛点是 service A 需要 service B 的能力,直接 new 还是走容器?我目前用构造函数注入,但偶尔遇到循环依赖(Credit_Service 要 Notification_Service,后者积分变动时又要回调查前者)。暂时用事件派发解耦,但 WordPress 没有内置事件总线,自己包了一层 do_action 的面向对象壳。

这块还没跑稳定,有实践过的可以聊聊——你们 service 层互相调用是怎么防"毛线球"的?

评论0
回复 · 0
还没有回复
微信客服 微信客服