PHP多租户系统需在请求入口早期(如public/index.php)通过$_SERVER['HTTP_HOST']识别域名,经TenantConfigFactory映射并缓存租户配置,严格隔离PDO实例及cache_prefix、log_channel、queue_connection、filesystem_disk、session_name等五类隐性上下文,杜绝数据混杂。

如何根据请求域名动态加载租户配置
PHP多租户系统里,不能靠硬编码写死 $tenant_id 或 database_name,必须在请求进入时就识别当前域名并加载对应租户上下文。关键不是“怎么切换”,而是“在哪儿切、切得够早、切得干净”。
常见错误是把域名解析逻辑塞进控制器或中间件末尾——这时数据库连接、日志前缀、缓存键等可能已按默认租户初始化完毕,再切就晚了。正确位置是应用启动早期,比如框架的引导文件(public/index.php)或自定义的 TenantBootstrapper 类中。
- 用
$_SERVER['HTTP_HOST']获取原始域名(注意处理端口、www 前缀、HTTPS 重定向等) - 查表或读取预加载的
tenants.php配置数组,映射到租户标识(如tenant_abc) - 把结果存入全局可访问的容器(如
$_SERVER['TENANT_ID']或静态属性),避免后续重复解析 - 拒绝未注册域名:直接返回 404 或 403,不进入路由层
用工厂模式封装租户配置加载逻辑
工厂不是为炫技,而是隔离“域名→租户数据”的耦合。你不会希望每个用到数据库的地方都重复写一遍 switch($host)。
定义一个 TenantConfigFactory 类,只暴露一个静态方法 getForHost(),它返回标准化的租户配置数组:
class TenantConfigFactory
{
private static array $configs = [];
<pre class="brush:php;toolbar:false;">public static function getForHost(string $host): array
{
if (isset(self::$configs[$host])) {
return self::$configs[$host];
}
$config = require __DIR__ . '/tenants/' . md5($host) . '.php';
// 或从 DB 查询:SELECT * FROM tenants WHERE domain = ?
self::$configs[$host] = $config;
return $config;
}}
这个工厂要能处理缓存失效(比如租户配置更新后需清空 self::$configs)、异常兜底(找不到时抛 TenantNotFoundException 而非静默返回空数组)。
- 配置文件命名建议用域名哈希(如
md5('app.example.com')),避免路径遍历风险 - 不要在工厂里做数据库连接——它只负责返回配置;连接由后续的
DatabaseConnectionFactory拿配置去建 - 如果租户数超百,考虑用 APCu 缓存
$configs数组,而非仅静态属性
数据库连接必须按租户隔离实例
很多人以为改了 DB_NAME 就算切换成功,但 PDO 实例是长连接复用的,没重建连接,旧连接仍连在上一个租户库上。
核心原则:每个租户请求必须使用独立的 PDO 实例,且该实例生命周期严格限定在本次请求内。
- 在工厂返回配置后,立刻调用
new PDO($dsn, $user, $pass, $options),绝不复用全局 $pdo - 用依赖注入容器(如 PHP-DI)绑定
PDO::class为 request-scoped,确保每次请求 new 一个 - ORM 如 Laravel Eloquent 可通过
DB::purge()+DB::reconnect()切换,但底层仍是重建连接,性能略低 - 注意连接池(如 Swoole)场景:worker 进程可能跨请求复用连接,必须在每次请求开头显式
close()旧连接
容易被忽略的“隐性租户状态”
除了数据库,还有至少五处会悄悄携带租户上下文,漏掉任何一处都会导致数据错乱:
-
cache_prefix:Redis 键必须加租户前缀,如tenant_abc:users:123,否则 A 租户删缓存会干掉 B 租户的 -
log_channel:Laravel 的Log::channel('tenant')要基于租户 ID 动态创建,日志文件名/ES 索引名都要区分 -
queue_connection:队列任务若异步执行,必须把tenant_id作为 payload 字段传入,不能依赖运行时环境 -
filesystem_disk:上传路径必须含租户标识,如s3://my-bucket/tenant_abc/avatar.jpg -
session_name:PHP 默认 session 名是固定的,多个租户共用会互相踢下线,需设为PHPSESSID_tenant_abc
这些不是“可配可不配”的选项,是租户隔离的刚性边界。一旦上线,租户数据混杂很难回溯修复。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











