关于 laravel-multi-tenancy
laravel-multi-tenancy 是面向 Laravel 多租户应用开发者的实现指南,介绍中央与租户上下文的组织方式。内容涵盖数据库隔离、查询自动限定、租户识别及队列上下文保留。
主要功能
- 01区分中央与租户操作
- 02每个租户使用独立数据库
- 03查询自动限定到当前租户
- 04通过域名、子域名或请求头识别租户
- 05队列任务保留租户上下文
适用场景
- 01构建需要严格数据隔离的 SaaS 应用
- 02为租户组织路由、模型和操作
- 03处理需要跨租户执行的任务
- 04测试中央与租户上下文
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ leeovery/ agentic-skills --skill laravel-multi-tenancy安装后在 Claude Code 中输入/laravel-multi-tenancy确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ManagesTenants.phpRefreshDatabaseWithTenant.phptenancy-testing.mdTenancyPest.phpTenancyTestCase.phpTenantTestCase.php详细介绍
Laravel 多租户
多租户将应用程序逻辑划分为中央(非租户)和租户(特定于租户)的上下文。
相关指南:
- tenancy-testing.md - 测试多租户功能
- 操作 - 中央与租户操作的组织方式
- 模型 - 模型组织方式
- 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
└── Services/
└── Tenancy/
├── Landlord.php
└── Facades/
└── Landlord.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,
]);
}
}
}
Landlord 辅助类
将 Stancl Tenancy 封装在 Landlord 服务类中,以获得更简洁的 API:
<?php
declare(strict_types=1);
namespace App\Services\Tenancy;
use App\Models\Tenant;
class Landlord
{
public static function tenant(): ?Tenant
{
return tenant();
}
public static function initialize(Tenant|int|string $tenant): void
{
tenancy()->initialize($tenant);
}
public static function end(): void
{
tenancy()->end();
}
public static function runAsCentral(callable $callback): mixed
{
return tenancy()->central($callback);
}
public function tenantId(): ?string
{
return tenant()?->getTenantKey();
}
public function eachTenant(callable $callback): void
{
Tenant::each(function (Tenant $tenant) use ($callback): void {
$this->runAs($tenant, $callback);
});
}
public function runAs(Tenant|int|string $tenant, callable $callback): mixed
{
if (! $tenant instanceof Tenant) {
$tenant = tenancy()->find($tenant);
}
return tenancy()->run($tenant, $callback);
}
}
用法:
use App\Services\Tenancy\Landlord;
$tenant = Landlord::tenant();
$tenantId = Landlord::tenantId();
if (Landlord::tenant() !== null) {
// Tenant-specific logic
}
Landlord::runAs($tenant, function () {
Order::create([...]);
});
Landlord::runAsCentral(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(Landlord::tenant(), $orderData);
常见模式
在多个租户中运行代码
// Using Landlord's eachTenant helper
resolve(Landlord::class)->eachTenant(function () {
Order::where('status', 'pending')->update(['processed' => true]);
});
// Or manually for specific tenants
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
resolve(Landlord::class)->runAs($tenant, function () {
Order::where('status', 'pending')->update(['processed' => true]);
});
}
在租户上下文中访问中央数据
Landlord::runAsCentral(function () {
$allTenants = Tenant::all();
});
根据租户执行条件逻辑
if (Landlord::tenant() !== null) {
$orders = Order::all(); // Scoped to tenant
} else {
$tenants = Tenant::all(); // Central
}
测试
包括:
- 测试中央和租户操作
- ManagesTenants 和 RefreshDatabaseWithTenant traits
- TenantTestCase 设置
- 多租户的 Pest 配置
- 测试目录结构
最佳实践
- 使用目录结构区分中央和租户操作/DTO
- 按照 Laravel 约定,将模型放在
app/Models/ - 始终使用 Landlord 辅助类访问租户
- 分别测试中央和租户上下文
- 在队列任务中保留租户上下文
评论
支持 Markdown