laravel-multi-tenancy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLaravel Multi-Tenancy
Laravel 多租户架构
Multi-tenancy separates application logic into central (non-tenant) and tenanted (tenant-specific) contexts.
Related guides:
- tenancy-testing.md - Testing multi-tenant features
- Actions - Central vs Tenanted action organization
- Models - Model organization
- structure.md - Directory organization
多租户架构将应用逻辑划分为中心(非租户)和租户专属两种上下文。
相关指南:
- tenancy-testing.md - 多租户功能测试
- Actions - 中心与租户专属动作的组织方式
- Models - 模型组织方式
- structure.md - 目录结构
Philosophy
设计理念
Multi-tenancy provides:
- Clear separation between central and tenant contexts
- Database isolation with separate databases per tenant
- Automatic scoping of queries to current tenant
- Context awareness through helper classes
- Queue integration with tenant context preservation
多租户架构可实现:
- 清晰隔离中心与租户上下文
- 数据库隔离:为每个租户分配独立数据库
- 自动作用域:查询自动适配当前租户
- 上下文感知:通过辅助类实现
- 队列集成:保留租户上下文
When to Use
适用场景
Use multi-tenancy when:
- Building SaaS applications with complete data isolation
- Each customer needs their own database
- Compliance requires strict data separation
Don't use when:
- Simple user segmentation is sufficient (use user_id scoping)
- All customers share the same schema
- Application complexity doesn't justify the overhead
适合使用多租户架构的场景:
- 构建需完全数据隔离的SaaS应用
- 每个客户需要独立数据库
- 合规要求严格的数据分离
不适合使用的场景:
- 简单的用户分段即可满足需求(使用user_id作用域即可)
- 所有客户共享同一数据库 schema
- 应用复杂度不足以支撑多租户架构的开销
Directory Structure
目录结构
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.phpapp/
├── Actions/
│ ├── Central/ # 非租户动作
│ │ ├── Tenant/
│ │ │ ├── CreateTenantAction.php
│ │ │ └── DeleteTenantAction.php
│ │ └── User/
│ │ └── CreateCentralUserAction.php
│ └── Tenanted/ # 租户专属动作
│ ├── Order/
│ │ └── CreateOrderAction.php
│ └── Customer/
│ └── CreateCustomerAction.php
├── Data/
│ ├── Central/ # 中心DTO
│ └── Tenanted/ # 租户专属DTO
├── Http/
│ ├── Central/ # 中心路由(租户管理)
│ ├── Web/ # 租户应用路由
│ └── Api/ # 公开API(租户作用域)
├── Models/ # 所有模型存放于标准目录
│ ├── Tenant.php # 中心模型
│ ├── Order.php # 租户专属模型
│ └── Customer.php
└── Support/
└── TenantContext.phpCentral Actions
中心动作
Central actions manage tenants and cross-tenant operations.
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
<?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,
]);
}
}Tenanted Actions
租户专属动作
Tenanted actions operate within a specific tenant's context. All queries automatically scoped.
php
<?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
<?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 {
// 自动适配当前租户作用域
$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,
]);
}
}
}Tenant Context Helper
租户上下文辅助类
php
<?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);
}
}Usage:
php
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([...]);
});php
<?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);
}
}使用示例:
php
use App\Support\TenantContext;
$tenant = TenantContext::current();
$tenantId = TenantContext::id();
if (TenantContext::isActive()) {
// 租户专属逻辑
}
TenantContext::run($tenant, function () {
Order::create([...]);
});
TenantContext::runCentral(function () {
Tenant::create([...]);
});Tenant Identification Middleware
租户识别中间件
Domain-Based
基于域名
php
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
class IdentifyTenant extends InitializeTenancyByDomain
{
// Tenant identified by domain (e.g., tenant1.myapp.com)
}php
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
class IdentifyTenant extends InitializeTenancyByDomain
{
// 通过域名识别租户(例如:tenant1.myapp.com)
}Subdomain-Based
基于子域名
php
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
class IdentifyTenant extends InitializeTenancyBySubdomain
{
// Tenant identified by subdomain
}php
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
class IdentifyTenant extends InitializeTenancyBySubdomain
{
// 通过子域名识别租户
}Header-Based
基于请求头
php
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
class IdentifyTenant extends InitializeTenancyByRequestData
{
public static string $header = 'X-Tenant';
}php
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
class IdentifyTenant extends InitializeTenancyByRequestData
{
public static string $header = 'X-Tenant';
}Route Configuration
路由配置
Tenant Routes
租户路由
php
// routes/tenant.php
Route::middleware(['tenant'])->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
});php
// routes/tenant.php
Route::middleware(['tenant'])->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
});Central Routes
中心路由
php
// routes/central.php
Route::middleware(['central'])->prefix('central')->group(function () {
Route::get('/tenants', [TenantController::class, 'index']);
Route::post('/tenants', [TenantController::class, 'store']);
});php
// routes/central.php
Route::middleware(['central'])->prefix('central')->group(function () {
Route::get('/tenants', [TenantController::class, 'index']);
Route::post('/tenants', [TenantController::class, 'store']);
});Bootstrap Configuration
启动配置
php
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();php
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();Models
模型
All models live in . Central vs tenanted distinguished by traits/interfaces, not subdirectories.
app/Models/所有模型存放于目录。通过 trait/接口区分中心模型与租户专属模型,而非子目录。
app/Models/Central Model
中心模型
php
<?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
<?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);
}
}Tenanted Model
租户专属模型
php
<?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
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// 自动适配当前租户作用域
// 查询中无需指定tenant_id
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}Queue Jobs with Tenant Context
带租户上下文的队列任务
Jobs must preserve tenant context when queued.
php
<?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);
}
}Dispatching:
php
ProcessOrderJob::dispatch(TenantContext::current(), $orderData);队列任务在入队时必须保留租户上下文。
php
<?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
{
// 自动在租户上下文内运行
$action($this->orderData);
}
}调度方式:
php
ProcessOrderJob::dispatch(TenantContext::current(), $orderData);Common Patterns
常见模式
Running Code in Multiple Tenants
在多租户环境中运行代码
php
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
TenantContext::run($tenant, function () use ($tenant) {
Order::where('status', 'pending')->update(['processed' => true]);
});
}php
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
TenantContext::run($tenant, function () use ($tenant) {
Order::where('status', 'pending')->update(['processed' => true]);
});
}Accessing Central Data from Tenant Context
从租户上下文访问中心数据
php
TenantContext::runCentral(function () {
$allTenants = Tenant::all();
});php
TenantContext::runCentral(function () {
$allTenants = Tenant::all();
});Conditional Logic Based on Tenant
基于租户的条件逻辑
php
if (TenantContext::isActive()) {
$orders = Order::all(); // Scoped to tenant
} else {
$tenants = Tenant::all(); // Central
}php
if (TenantContext::isActive()) {
$orders = Order::all(); // 适配租户作用域
} else {
$tenants = Tenant::all(); // 中心数据
}Testing
测试
→ Complete testing guide: tenancy-testing.md
Includes:
- Testing central and tenanted actions
- ManagesTenants and RefreshDatabaseWithTenant traits
- TenantTestCase setup
- Pest configuration for multi-tenancy
- Test directory structure
→ 完整测试指南:tenancy-testing.md
包含:
- 中心与租户专属动作的测试
- ManagesTenants 和 RefreshDatabaseWithTenant trait
- TenantTestCase 配置
- Pest 多租户配置
- 测试目录结构
Summary
总结
Multi-tenancy provides:
- Clear separation - Central vs Tenanted namespaces
- Database isolation - Each tenant has dedicated database
- Automatic scoping - Queries automatically tenant-scoped
- Context helpers - Easy access to tenant context
- Queue integration - Jobs preserve tenant context
Best practices:
- Use directory structure to separate central and tenanted actions/DTOs
- Keep models in following Laravel convention
app/Models/ - Always use TenantContext helper for tenant access
- Test both central and tenant contexts separately
- Preserve tenant context in queued jobs
多租户架构可提供:
- 清晰隔离 - 中心与租户命名空间分离
- 数据库隔离 - 每个租户拥有独立数据库
- 自动作用域 - 查询自动适配租户
- 上下文辅助 - 便捷访问租户上下文
- 队列集成 - 任务保留租户上下文
最佳实践:
- 使用目录结构分离中心与租户专属动作/DTO
- 遵循Laravel约定,将模型存放于
app/Models/ - 始终使用TenantContext辅助类访问租户信息
- 分别测试中心与租户上下文
- 在队列任务中保留租户上下文