项目初期我们习惯把API、后台、前端控制器全塞进同一个app目录里。业务量一起来,控制器和模型文件混在一起,路由定义挤在同一个文件里,想给API加个全局中间件还得小心翼翼避开后台的路由。更头疼的是团队分工——负责后台的同事和负责API的同事修改同一个app目录,代码合并时频繁冲突。
ThinkPHP 8.0的多应用模式就是为了解决这种复杂度而生的。它允许在同一个项目下划分出多个独立的应用,每个应用有自己的控制器、模型、视图、路由和配置。这篇借着我们团队最近拆旧项目的经验,把多应用的搭建过程、路由策略、跨应用交互和部署细节完整写出来。
一、启用多应用模式
ThinkPHP 8.0默认是单应用模式,入口都在app目录下。要改成多应用,首先需要在项目根目录的composer.json中确认安装了多应用扩展:
composer require topthink/think-multi-app
安装完成后,框架会自动识别app目录下的子目录作为独立应用。一个典型的多应用目录结构:
project/
├── app/
│ ├── api/ # API应用
│ │ ├── controller/
│ │ ├── model/
│ │ ├── middleware/
│ │ ├── route/
│ │ │ └── app.php
│ │ └── common.php
│ ├── admin/ # 后台管理应用
│ │ ├── controller/
│ │ ├── model/
│ │ ├── view/
│ │ ├── middleware/
│ │ ├── route/
│ │ │ └── app.php
│ │ └── common.php
│ └── BaseController.php # 公共基类
├── config/
├── public/
│ ├── index.php
│ └── admin.php # 后台入口(可选)
├── route/
│ └── app.php # 全局路由
└── extend/
每个应用有自己的目录结构,路由文件在各自route/app.php里定义,互不干扰。公共的基类、traits或者第三方库放在app根目录或extend中,供所有应用使用。
二、创建API应用
先建API应用。在app/api/下创建controller目录,写一个简单的用户列表接口:
<?php
namespace appapicontroller;
use appapicontrollerBase;
use thinkfacadeDb;
class User extends Base
{
public function list()
{
$users = Db::table('users')->field('id, nickname, avatar, phone')->paginate(15);
return json([
'code' => 0,
'data' => $users,
]);
}
}
API应用需要一个基类来统一处理JSON响应和权限校验。在app/api/controller/Base.php中:
<?php
namespace appapicontroller;
use thinkfacadeApp;
use thinkexceptionHttpResponseException;
use thinkResponse;
class Base
{
protected function jsonSuccess($data = [], string $msg = 'ok', int $code = 0)
{
$response = json([
'code' => $code,
'msg' => $msg,
'data' => $data,
]);
throw new HttpResponseException($response);
}
protected function jsonError(string $msg = 'error', int $code = 1, int $httpCode = 200)
{
$response = json([
'code' => $code,
'msg' => $msg,
], $httpCode);
throw new HttpResponseException($response);
}
}
接着定义路由。在app/api/route/app.php中:
<?php
use thinkfacadeRoute;
Route::group('v1', function () {
Route::get('users', 'User/list');
});
外部访问的URL就是/api/v1/users。这里的路径自动匹配应用名,因为入口文件默认是public/index.php,框架根据URL中的第一个路径段api来定位到app/api应用,后面的v1/users交给该应用的路由处理。
三、创建后台管理应用
后台应用通常需要视图渲染、表单提交和权限管理。在app/admin/下创建controller/Index.php:
<?php
namespace appadmincontroller;
use thinkfacadeView;
class Index
{
public function dashboard()
{
// 渲染后台首页视图
return View::fetch();
}
}
后台的路由在app/admin/route/app.php:
<?php
use thinkfacadeRoute;
Route::get('dashboard', 'Index/dashboard');
后台的入口URL就是/admin/dashboard。如果你想为后台设置独立域名或二级域名,可以在入口文件public/index.php里做域名绑定,或者用Nginx转发。但多应用模式本身就支持按应用名访问,不搞独立域名也能很好工作。
四、中间件的隔离
不同应用通常需要不同的中间件。比如API需要JWT认证或限流,后台需要登录态检测和权限验证。中间件可以定义在各自应用的middleware目录下,然后在应用的路由文件或控制器中单独使用。
以API的简单Token校验为例,创建app/api/middleware/Auth.php:
<?php
namespace appapimiddleware;
use thinkResponse;
class Auth
{
public function handle($request, Closure $next)
{
$token = $request->header('Authorization');
if (!$token || !$this->validateToken($token)) {
return Response::create([
'code' => 401,
'msg' => '未授权',
], 'json', 401);
}
return $next($request);
}
private function validateToken(string $token): bool
{
// 具体校验逻辑
return true;
}
}
在API的路由文件app/api/route/app.php中应用这个中间件:
Route::group('v1', function () {
Route::get('users', 'User/list');
})->middleware(appapimiddlewareAuth::class);
后台应用也有自己的中间件,两者完全隔离,不会互相干扰。
五、模型层和公共代码共享
API和后台都操作同一套数据库表,如果两个应用各自写一份模型,字段定义和业务逻辑就会重复。共享模型有两种方式:一是把模型放在app/model公共目录(单应用模式的位置),二是在app下建一个common目录放置公共模型。ThinkPHP在多应用模式下可以自动加载app/model下的类,所以最简单的方法是把模型放在那里。
例如app/model/User.php:
<?php
namespace appmodel;
use thinkModel;
class User extends Model
{
protected $table = 'users';
protected $pk = 'id';
}
在API和后台控制器里都可以直接use appmodelUser;使用,保证了数据层的一致性和复用。
如果需要跨应用调用服务层(如API需要用到后台的某个统计Service),则建议把服务类放在app/common/service中,或者通过依赖注入解耦。
六、配置的继承与覆盖
每个应用可以有自己独立的配置文件,放在应用目录下的config文件夹里。比如API需要不同的数据库连接或者不同的缓存前缀,就可以在app/api/config/database.php中覆盖全局配置。
应用配置的优先级:应用自己的配置 > 全局config目录下的配置。这种分层设计让全局设置和个性化分离得很好,不需要为了一个应用的特殊需求改动全局配置。
七、跨应用调用与RPC
尽管各应用独立,但有时候API应用需要调用后台应用的一个功能,例如推送统计通知。在多应用模式下,最佳实践是避免直接引用对方的控制器或类——那样会产生耦合。替代方案包括:
- 把公共业务逻辑抽到
app/common目录中,两个应用都调用它。 - 使用内部RPC(在同一个进程内直接调用公共Service层)。
- 如果部署在不同的服务器或容器,采用HTTP接口互相调用,并加上签名验证。
对于同机部署的情况,抽公共Service是最快的办法:
// app/common/service/StatisticsService.php
namespace appcommonservice;
use appmodelUser;
class StatisticsService
{
public function getUserCount(): int
{
return User::count();
}
}
然后在任何应用中调用:
$count = app('appcommonserviceStatisticsService')->getUserCount();
八、入口文件与域名绑定
生产环境可能需要将api.example.com指向API应用,admin.example.com指向后台。ThinkPHP的多应用模式可以通过在入口文件设置app_name来实现。例如为API设置独立入口文件public/api.php:
// public/api.php
namespace think;
// 设置应用名称
define('APP_NAME', 'api');
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/topthink/framework/src/helper.php';
(new App())->http->run();
然后在Nginx中把api.example.com转发到public/api.php。这样无论是路径还是域名,都能灵活地指向不同应用。
九、迁移旧单应用项目的一点经验
我们把旧项目从单应用迁移到多应用时,采取了分步走的方式:
- 先启用
topthink/think-multi-app,框架会自动识别app下的子目录。此时老控制器仍可以留在原地,不会报错。 - 新建
app/api和app/admin目录,逐步移动对应模块的控制器和路由。 - 将共用的模型和服务抽到
app/model和app/common。 - 配置各自的中间件和路由分组,最后上线切换域名或路径。
整个迁移过程花了两天,测试完之后,代码结构清晰了很多,团队协作的冲突也明显减少了。
十、总结
ThinkPHP 8.0的多应用模式不是新瓶装旧酒,它是项目规模增长后的自然选择。一个项目拆成两个甚至更多的应用,不仅让目录树一目了然,也让中间件、路由和配置的管理变得有条理。复用公共的模型和服务,既避免了重复劳动,又保持了数据层的统一。
如果你现在维护的ThinkPHP项目开始觉得app目录里东西越来越多、越来越乱,不妨考虑用多应用模式拆分一下。可以先从最独立的模块开始(比如API),建一个新应用,移一波控制器,跑通路由,再逐步迁移其他模块。这个投入产出的改善,大概率会让你觉得值得。

