关于 laravel-multi-tenancy
面向 Laravel 开发者的多租户项目组织与实现指南,区分中央和租户上下文。涵盖独立数据库、查询自动作用域、租户上下文辅助类及队列处理。
主要功能
- 01区分中央与租户操作
- 02每个租户使用独立数据库
- 03查询自动按租户限定
- 04通过辅助类访问租户上下文
- 05队列任务保留租户上下文
适用场景
- 01为 SaaS 应用隔离客户数据
- 02按域名、子域名或请求头识别租户
- 03在多个租户上下文中运行代码
- 04测试中央与租户功能
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ leeovery/ claude-laravel --skill laravel-multi-tenancy安装后在 Claude Code 中输入/laravel-multi-tenancy确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ManagesTenants.phpRefreshDatabaseWithTenant.phpTenancyPest.phpTenancyTestCase.phpTenantTestCase.phptenancy-testing.md详细介绍
Laravel 多租户
多租户将应用逻辑分为中央(非租户)和租户(特定租户)上下文。
相关指南:
- tenancy-testing.md - 测试多租户功能
- Actions - 中央与租户操作的组织方式
- Models - 模型的组织方式
- structure.md - 目录组织方式
理念
多租户提供:
- 清晰分离中央与租户上下文
- 数据库隔离,为每个租户使用独立数据库
- 自动作用域,将查询限定在当前租户
- 通过辅助类实现上下文感知
- 与队列集成,保留租户上下文
适用场景
在以下情况下使用多租户:
- 构建需要完全隔离数据的 SaaS 应用
- 每位客户都需要自己的数据库
- 合规要求严格的数据分离
在以下情况下不要使用:
- 简单的用户分组已足够(使用 user_id 作用域)
- 所有客户共用相同的架构
- 应用复杂度不足以证明额外开销合理
目录结构
app/
├── Actions/
│ ├── Central/ # Non-tenant actions
│ │ ├── Tenant/
│ │ │ ├── CreateTenantAction.php
│ │ │ └── DeleteTenantAction.php
│ │ └── User/
│ │ └── CreateCentralUserAction.php
│ └── Tenanted/ # Tenant-specific actions
│ ├── Order/
│ │ └── CreateOrderAction.php
│ └── Customer/
│ └── CreateCustomerAction.php
├── Data/
│ ├── Central/ # Central DTOs
│ └── Tenanted/ # Tenant DTOs
├── Http/
│ ├── Central/ # Central routes (tenant management)
│ ├── Web/ # Tenant application routes
│ └── Api/ # Public API (tenant-scoped)
├── Models/ # All models in standard location
│ ├── Tenant.php # Central model
│ ├── Order.php # Tenanted model
│ └── Customer.php
└── Support/
└── TenantContext.php
中央操作
中央操作用于管理租户和执行跨租户操作。
<?php
declare(strict_types=1);
namespace App\Actions\Central\Tenant;
use App\Data\Central\CreateTenantData;
use App\Models\Tenant;
use Illuminate\Support\Facades\DB;
class CreateTenantAction
{
public function __construct(
private readonly CreateTenantDatabaseAction $createDatabase,
) {}
public function __invoke(CreateTenantData $data): Tenant
{
return DB::transaction(function () use ($data): Tenant {
$this->guard($data);
$tenant = $this->createTenant($data);
($this->createDatabase)($tenant);
return $tenant;
});
}
private function guard(CreateTenantData $data): void
{
throw_if(
Tenant::where('domain', $data->domain)->exists(),
TenantDomainAlreadyExistsException::forDomain($data->domain)
);
}
private function createTenant(CreateTenantData $data): Tenant
{
return Tenant::create([
'id' => $data->tenantId,
'name' => $data->name,
'domain' => $data->domain,
]);
}
}
租户操作
租户操作在特定租户的上下文中运行。所有查询都会自动限定作用域。
<?php
declare(strict_types=1);
namespace App\Actions\Tenanted\Order;
use App\Data\Tenanted\CreateOrderData;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CreateOrderAction
{
public function __invoke(User $user, CreateOrderData $data): Order
{
return DB::transaction(function () use ($user, $data): Order {
// Automatically scoped to current tenant
$order = $user->orders()->create([
'status' => $data->status,
'total' => $data->total,
]);
$this->createOrderItems($order, $data->items);
return $order;
});
}
private function createOrderItems(Order $order, array $items): void
{
foreach ($items as $item) {
$order->items()->create([
'product_id' => $item->productId,
'quantity' => $item->quantity,
'price' => $item->price,
]);
}
}
}
租户上下文辅助工具
<?php
declare(strict_types=1);
namespace App\Support;
use App\Models\Tenant;
use Stancl\Tenancy\Facades\Tenancy;
class TenantContext
{
public static function current(): ?Tenant
{
return Tenancy::tenant();
}
public static function id(): ?string
{
return Tenancy::tenant()?->getTenantKey();
}
public static function isActive(): bool
{
return Tenancy::tenant() !== null;
}
public static function run(Tenant $tenant, callable $callback): mixed
{
return tenancy()->runForMultiple([$tenant], $callback);
}
public static function runCentral(callable $callback): mixed
{
return tenancy()->runForMultiple([], $callback);
}
}
用法:
use App\Support\TenantContext;
$tenant = TenantContext::current();
$tenantId = TenantContext::id();
if (TenantContext::isActive()) {
// Tenant-specific logic
}
TenantContext::run($tenant, function () {
Order::create([...]);
});
TenantContext::runCentral(function () {
Tenant::create([...]);
});
租户识别中间件
基于域名
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
class IdentifyTenant extends InitializeTenancyByDomain
{
// Tenant identified by domain (e.g., tenant1.myapp.com)
}
基于子域名
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
class IdentifyTenant extends InitializeTenancyBySubdomain
{
// Tenant identified by subdomain
}
基于标头
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
class IdentifyTenant extends InitializeTenancyByRequestData
{
public static string $header = 'X-Tenant';
}
路由配置
租户路由
// routes/tenant.php
Route::middleware(['tenant'])->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
});
中央路由
// routes/central.php
Route::middleware(['central'])->prefix('central')->group(function () {
Route::get('/tenants', [TenantController::class, 'index']);
Route::post('/tenants', [TenantController::class, 'store']);
});
引导配置
return Application::configure(basePath: dirname(__DIR__))
->withRouting(function () {
Route::middleware('web')
->prefix('central')
->name('central.')
->group(base_path('routes/central.php'));
Route::middleware(['web', 'tenant'])
->group(base_path('routes/tenant.php'));
})
->create();
模型
所有模型都放在 app/Models/ 中。中央模型与租户模型通过 trait 或接口区分,而不是通过子目录区分。
中央模型
<?php
declare(strict_types=1);
namespace App\Models;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
class Tenant extends BaseTenant
{
public function users(): HasMany
{
return $this->hasMany(User::class);
}
}
租户模型
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// Automatically scoped to current tenant
// No tenant_id needed in queries
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
带租户上下文的队列任务
排入队列时,任务必须保留租户上下文。
<?php
declare(strict_types=1);
namespace App\Jobs\Tenanted;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Jobs\TenantAwareJob;
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, TenantAwareJob;
public function __construct(
public TenantWithDatabase $tenant,
public OrderData $orderData,
) {
$this->onQueue('orders');
}
public function handle(ProcessOrderAction $action): void
{
// Runs in tenant context automatically
$action($this->orderData);
}
}
分派:
ProcessOrderJob::dispatch(TenantContext::current(), $orderData);
常见模式
在多个租户中运行代码
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
TenantContext::run($tenant, function () use ($tenant) {
Order::where('status', 'pending')->update(['processed' => true]);
});
}
在租户上下文中访问中央数据
TenantContext::runCentral(function () {
$allTenants = Tenant::all();
});
根据租户执行条件逻辑
if (TenantContext::isActive()) {
$orders = Order::all(); // Scoped to tenant
} else {
$tenants = Tenant::all(); // Central
}
测试
包括:
- 测试中央操作和租户操作
- ManagesTenants 和 RefreshDatabaseWithTenant traits
- TenantTestCase 设置
- 多租户的 Pest 配置
- 测试目录结构
总结
多租户提供:
- 清晰分离 - 中央与租户命名空间
- 数据库隔离 - 每个租户都有专用数据库
- 自动作用域 - 查询会自动限定到租户
- 上下文辅助工具 - 轻松访问租户上下文
- 队列集成 - 任务会保留租户上下文
最佳实践:
- 使用目录结构分离中央和租户操作/DTO
- 遵循 Laravel 惯例,将模型放在
app/Models/中 - 始终使用 TenantContext 辅助工具访问租户
- 分别测试中央和租户上下文
- 在排入队列的任务中保留租户上下文
评论
支持 Markdown