本文目录导读:

- 拆分的核心原则(先想清楚再动手)
- 目录结构设计(两种主流方案)
- 模块间通信机制(核心难点)
- 实际拆分步骤(五步走)
- 常见问题和避坑指南
- 推荐工具链
- 拆分后的收益(判断成功标准)
- Laravel 专项实践(如果用的是 Laravel)
下面系统地梳理 PHP 项目模块化拆分的完整经验,包含从思路到落地的可执行方案。
拆分的核心原则(先想清楚再动手)
基于业务边界,而非技术分层
拆分的第一驱动因素是业务域,不是 Controller/Service/Model 这种技术分层。
❌ 错误示例: modules/ ├── Controllers/ ├── Services/ ├── Models/ ✅ 正确示例: modules/ ├── User/ ← 用户域 ├── Order/ ← 订单域 ├── Payment/ ← 支付域 ├── Product/ ← 商品域
关键认知:技术分层是横向的,业务域是纵向的,当一个需求变更要同时改动 User/Order/Payment 三个模块时,说明边界划分失败了。
遵循“高内聚、低耦合”
- 高内聚:一个模块的所有代码(Controller/Service/Model/配置文件/路由)都放在一起
- 低耦合:模块之间只能通过公开接口通信,禁止直接访问对方的内部类
依赖方向要明确
依赖关系应该是单向的,不允许循环依赖:
User 模块 ──→ Order 模块 ──→ Payment 模块 ↑ ↑ └── 单向依赖,禁止反向
目录结构设计(两种主流方案)
方案 A:按业务域拆分(推荐)
app/ ├── Modules/ │ ├── User/ │ │ ├── Controllers/ │ │ ├── Services/ │ │ ├── Models/ │ │ ├── Repositories/ │ │ ├── Exceptions/ │ │ ├── Config/ # 模块自己的配置 │ │ ├── Routes/ # 模块自己的路由 │ │ ├── Migrations/ # 模块自己的数据库迁移 │ │ ├── Tests/ # 模块自己的单元测试 │ │ └── ModuleServiceProvider.php # 模块服务提供者 │ ├── Order/ │ │ └── (同上结构) │ └── Payment/ │ └── (同上结构) ├── Shared/ # 公共代码(非业务) │ ├── Helpers/ │ ├── Middleware/ │ ├── Traits/ │ └── Contracts/ └── Core/ # 框架核心
方案 B:分包拆分(适合中小型项目)
src/ ├── User/ │ ├── UserController.php │ ├── UserService.php │ └── User.php # 模型 ├── Order/ │ ├── OrderController.php │ ├── OrderService.php │ ├── Order.php │ └── OrderObserver.php ├── Shared/ │ └── ... └── ...
模块间通信机制(核心难点)
服务注入(依赖注入)
// 模块 A 公开自己的服务
class UserService
{
public function getUserProfile(int $userId): array { ... }
}
// 模块 B 注入使用
class OrderService
{
public function __construct(
private UserService $userService // 通过 DI 注入
) {}
public function createOrder(int $userId, array $items)
{
$user = $this->userService->getUserProfile($userId);
// ...
}
}
事件驱动(解耦最佳实践)
// 模块 A(用户模块)发布事件
class UserRegistered
{
public function __construct(public User $user) {}
}
// 事件发布
Event::dispatch(new UserRegistered($user));
// 模块 B(积分模块)监听并响应
class RegisterPointsListener
{
public function handle(UserRegistered $event): void
{
// 给新用户奖励积分,不直接依赖 User 模块内部
Points::reward($event->user->id, 100);
}
}
契约接口(Contract Interface)
// 在 Shared/Contracts 中定义契约
interface PaymentGateway
{
public function charge(float $amount, string $currency): bool;
}
// 模块实现契约
class StripePayment implements PaymentGateway
{
public function charge(float $amount, string $currency): bool { ... }
}
// 其他模块只依赖契约,不依赖实现
class OrderService
{
public function __construct(
private PaymentGateway $payment // 注入时自动绑定实现
) {}
}
实际拆分步骤(五步走)
Step 1:识别业务边界
通过 DDD(领域驱动设计)思想,找出你的业务中的“上下文边界”:
示例场景:电商系统 用户模块(user) → 注册、登录、资料、地址 商品模块(product) → 商品 CRUD、库存、分类 订单模块(order) → 创建订单、查询、取消 支付模块(payment) → 支付回调、退款 物流模块(logistics)→ 发货、轨迹
Step 2:梳理模块依赖关系
User ←── Order ←── Payment ↑ ↑ ↑ └──── Product ──────┘
标出哪些模块需要调用哪些模块的什么功能。
Step 3:定义公开接口(BaseService 类)
每个模块的 Service 层作为它的“门面”,为其他模块提供可调用的方法,命名要动词化、业务语义化:
// 模块公开 API
class UserApi
{
public function getUserById(int $id): ?array;
public function getUserAddresses(int $userId): array;
public function updateUserPhone(int $userId, string $phone): bool;
}
Step 4:迁移代码
按 每半天迁移一个模块 的节奏,边迁移边测试:
# 示例:迁移 User 模块 1. 创建 modules/User 目录 2. 移动 UserController.php → modules/User/Controllers/ 3. 移动 UserService.php → modules/User/Services/ 4. 移动 User.php → modules/User/Models/ 5. 调整命名空间 6. 更新路由文件 7. 跑通测试
Step 5:持续集成与重构
使用 PHPStan 静态分析 检查循环依赖,用 Deptrac 工具 自动检测模块间的耦合:
# deptrac.yaml dependencies: - modules/User - modules/Order rules: - User → Order # 允许 - Order → User # 不允许
常见问题和避坑指南
不要为了拆分而拆分
- 如果项目只有 5000 行代码,拆分成 10 个模块只会增加管理成本
- 建议阈值:当代码量 > 2万行、或团队 > 5 人时开始考虑模块化
命名空间冲突
注意类名冲突问题,给模块加统一前缀:
// 模块 User 的控制器 namespace Modules\User\Http\Controllers; // 模块 Order 的控制器 namespace Modules\Order\Http\Controllers;
共享的“配置/工具”放哪里?
❌ 不要:每个模块都复制一份工具的代码 ✅ 应该:公共代码提取到 Shared 目录,通过 Composer 自动加载
数据库迁移的表关联
不同模块的表之间避免直接外键约束,改用逻辑外键:
// User 表: users(id)
// Order 表: orders(user_id) —— 不用外键,用索引即可
Schema::create('orders', function (Blueprint $table) {
$table->unsignedBigInteger('user_id');
$table->index('user_id'); // 仅索引,不建外键
});
测试隔离
每个模块有自己的 phpunit.xml 和测试数据库,保证测试互不干扰。
推荐工具链
| 工具 | 用途 |
|---|---|
| PHPStan / Psalm | 静态分析,检查循环依赖、未定义方法 |
| Deptrac | 架构依赖检测,防止模块间乱引用 |
| Pest / PHPUnit | 单元测试,每个模块独立测 |
| Laravel Modules | Laravel 项目的模块化插件包 |
| Composer | 把模块作为独立的包加载 |
拆分后的收益(判断成功标准)
✅ 当你完成模块化后,应达到以下效果:
- 新功能开发时,只改一个模块或最多两个模块
- 修改模块 A 的代码,不影响模块 B 的运行
- 模块可以被独立部署(比如支付模块单独部署到另一台服务器)
- 团队成员可以并行开发不同模块,不会冲突
- 模块可以独立测试,不需要搭整个项目的环境
Laravel 专项实践(如果用的是 Laravel)
推荐使用官方模块化方式,或引入第三方包:
# 安装模块化扩展包
composer require nwidart/laravel-modules
# 创建新模块
php artisan module:make User
# 生成的路由结构
# routes/web.php 中引入
Route::prefix('user')->group(module_path('User', 'Routes/web.php'));
如果项目较小,也可以用 Laravel 的 app/Http 重构:把 Http 目录按业务拆分。
最后的核心观点:模块化拆分不是为了代码好看,而是为了降低维护成本和支持团队协作,拆分的粒度由业务复杂度决定,宁可拆多也不要拆错——因为合并比拆分难得多。