controller 里塞了 800 行业务逻辑后,我终于决定把"分层"当成还债来搞

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 2 浏览 0 回复

之前写插件图快,controller 里直接怼 SQL、拼 HTML、算积分、发邮件,一个文件干到 800 行。某天需求改"积分规则支持按版块加权",我翻了两小时才找对位置,当场决定还债——把 controller / service / model 拆开。记录一下我的拆分思路,主要是 WordPress 插件场景下的落地姿势。

一、model 层:只干"存取",不碰业务语义

我现在的 model 全是静态方法,一个类对应一张表或一个 meta 命名空间。比如积分流水:

class CreditLogModel {
    public static function insert(array $data): int {
        global $wpdb;
        $wpdb->insert("{$wpdb->prefix}my_credit_log", [
            'user_id'    => absint($data['user_id']),
            'action'     => sanitize_key($data['action']),
            'amount'     => floatval($data['amount']),
            'post_id'    => absint($data['post_id'] ?? 0),
            'created_at' => current_time('mysql'),
        ]);
        return (int) $wpdb->insert_id;
    }

    public static function get_by_user(int $user_id, int $limit = 20): array {
        global $wpdb;
        // 纯查询,不带任何"积分是否有效"的判断
        return $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM ... WHERE user_id = %d ORDER BY id DESC LIMIT %d",
            $user_id, $limit
        ), ARRAY_A);
    }
}

关键纪律:model 返回原始数据,不封装"用户当前总积分"这种需要聚合计算的字段。曾经我把 get_total_credit 也塞进来,结果后来要加"冻结积分"概念时,model 里到处改,很脏。

二、service 层:业务规则的"垃圾场",但要有边界

service 是我以前最混乱的地方,现在给自己定了两条红线:

  • service 可以调多个 model,但不直接输出 HTML、不读 $_POST、不操作全局状态
  • 复杂业务拆"领域 service",别搞万能 PluginService

以发帖加积分为例,拆成 CreditRuleService(算分)和 CreditTransactionService(执行):

class CreditRuleService {
    public static function calc_post_reward(int $user_id, int $forum_id): float {
        $base = apply_filters('my_credit_post_base', 10.0, $forum_id);
        $multiplier = get_option("my_credit_forum_{$forum_id}_multiplier", 1.0);
        
        // 日发帖上限检查——这里只算"本次应得",不扣减
        $today_count = CreditLogModel::count_today_by_action($user_id, 'post');
        if ($today_count >= 5) {
            return 0.0;
        }
        
        return $base * $multiplier;
    }
}

class CreditTransactionService {
    public static function grant_for_post(int $user_id, int $post_id, int $forum_id): ?int {
        $amount = CreditRuleService::calc_post_reward($user_id, $forum_id);
        if ($amount  $user_id,
            'action'  => 'post',
            'amount'  => $amount,
            'post_id' => $post_id,
        ]);

        UserMetaModel::increment($user_id, 'my_credit_total', $amount);

        do_action('my_credit_granted', $user_id, $amount, $log_id);
        return $log_id;
    }
}

grant_for_post 里我故意没做"是否重复奖励"的检查——那是调用方(controller 或 hook 回调)的责任。service 保证"调用一次执行一次",幂等控制上移,避免层与层之间互相猜疑。

三、controller / hook 回调:只负责"接线"和"格式转换"

controller 越来越薄,核心工作就三个:验权、洗数据、调 service、转格式。

class PostHookController {
    public static function on_publish(int $post_id, \WP_Post $post): void {
        if ($post->post_type !== 'forum_topic') {
            return;
        }

        $user_id = (int) $post->post_author;
        $forum_id = (int) get_post_meta($post_id, 'forum_id', true);

        // 鉴权/状态检查在 controller 层做
        if (! user_can($user_id, 'publish_topics')) {
            return;
        }

        // 防重:用 post_meta 当简易幂等键
        if (get_post_meta($post_id, '_credit_granted', true)) {
            return;
        }

        $log_id = CreditTransactionService::grant_for_post($user_id, $post_id, $forum_id);
        if ($log_id) {
            update_post_meta($post_id, '_credit_granted', $log_id);
        }
    }
}

// 接线
add_action('publish_forum_topic', [PostHookController::class, 'on_publish'], 10, 2);

AJAX 或 REST 的 controller 同理,只是最后多一步 wp_send_json。我坚决不在 controller 里写 $wpdb->query,哪怕只是一行简单的更新。

四、拆完后的意外收获:测试和 hook 都变得清爽

以前想写单元测试,controller 里一堆 wp_die 和全局函数,根本跑不起来。现在 model 用 Brain\Monkey mock $wpdb,service 纯 PHP 逻辑直接跑,controller 测个"是否调了对的 service 方法"就行。

还有个副作用:do_action 的埋点位置变清晰了。以前业务散落在 controller 各处,hook 名起得随心所欲。现在 service 层固定抛 my_credit_{before|after}_{action},其他开发者接二次开发时不用翻我源码。

五、还没想好的地方

Query 层怎么拆?WordPress 的 WP_Queryget_posts 算 model 还是 service?我目前的妥协是:简单查询直接让 controller 调 WP_Query,复杂统计(比如"用户近30天积分趋势")包成 CreditReportService 里的方法,内部再调 model。感觉这个边界还会随着需求变,先记下来。

你们插件里的分层是怎么落地的?有没有被 WordPress 的全局函数和 hook 系统搞到被迫"破层"的时候?

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