多租户
介绍
多租户(Multi-tenancy)是一个应用的单个实例为多个客户服务的概念。每个客户都有他们自己的数据和访问规则,以防止他们查看或修改彼此的数据。这是 SaaS 应用中的常见模式。用户通常属于用户组(通常称为团队或组织)。记录归用户组所有,用 户可以是多个组的成员。这适用于用户需要协作处理数据的应用。
多租户是一个非常敏感的话题。了解多租户的安全影响以及如何正确实现它非常重要。如果部分或错误地实现,属于一个租户的数据可能会暴露给另一个租户。Filament 提供了一组工具来帮助你在应用中实现多租户,但如何使用它们取决于你。
NOTE
Filament 不对你的应用的安全性提供任何保证。你有责任确保你的应用是安全的。有关详细信息,请参阅安全部分。
一对多租户
术语"多租户"是宽泛的,在不同语境下可能有不同意思。Filament 的多租户系统指的是用户属于多个租户(组织、团队、公司等),并且可能会在租户间切换。
如果你的用例较为简单,不需要多对多关联,那么你不需要在 Filament 中安装多租户。你可以使用观察者和全局查询范围设置。
比如,你有一个数据库字段 users.team_id,你可以让用户 使用全局查询范围,将所有记录查询范围设置为同一个 team_id:
use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
protected static function booted(): void
{
static::addGlobalScope('team', function (Builder $query) {
if (auth()->hasUser()) {
$query->where('team_id', auth()->user()->team_id);
// or with a `team` relationship defined:
$query->whereBelongsTo(auth()->user()->team);
}
});
}
}
要在记录创建时自动设置 team_id,你可以使用观察者:
class PostObserver
{
public function creating(Post $post): void
{
if (auth()->hasUser()) {
$post->team_id = auth()->user()->team_id;
// or with a `team` relationship defined:
$post->team()->associate(auth()->user()->team);
}
}
}
设置租户
要设置租户,你需要在配置中指定"租户"(比如,团队 team 或组织 organization)模型:
use App\Models\Team;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenant(Team::class);
}
你同时需要告诉 Filament,用户从属于哪个租户。你可以在 App\Models\User 模型上实现 HasTenants 接口:
<?php
namespace App\Models;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
class User extends Authenticatable implements FilamentUser, HasTenants
{
// ...
public function teams(): BelongsToMany
{
return $this->belongsToMany(Team::class);
}
public function getTenants(Panel $panel): Collection
{
return $this->teams;
}
public function canAccessTenant(Model $tenant): bool
{
return $this->teams()->whereKey($tenant)->exists();
}
}
上例中,用户属于多个团队,因此它有一个 teams() 关联。getTenants() 方法返回用户所属的团队。Filament 使用它来列出用户有权访问的租户。

为安全起见,你也需要实现 HasTenants 接口的 canAccessTenant(),以防止用户通过猜测租户 ID 并将其放入到 URL 来访问租户数据。
你可能希望用户可以注册新团队.
添加租户注册页面
注册页面将允许用户创建新租户。
登录访问你的应用时,如果用户还没有租户,他们将被重定向到此页面。
要设置注册页面,你需要创建一个继承 Filament\Pages\Tenancy\RegisterTenant 的新页面类。这是一个全页 Livewire 组件。你可以把它放在任何你想要的地方,比如 app/Filament/Pages/Tenancy/RegisterTeam.php:
namespace App\Filament\Pages\Tenancy;
use App\Models\Team;
use Filament\Forms\Components\TextInput;
use Filament\Pages\Tenancy\RegisterTenant;
use Filament\Schemas\Schema;
class RegisterTeam extends RegisterTenant
{
public static function getLabel(): string
{
return 'Register team';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name'),
// ...
]);
}
protected function handleRegistration(array $data): Team
{
$team = Team::create($data);
$team->members()->attach(auth()->user());
return $team;
}
}
你可以将任何表单组件添加到 form() 方法中,并在 handleRegistration() 方法内创建团队。
现在,我们需要告诉 Filament 启用这个页面。我们可以在配置中实现:
use App\Filament\Pages\Tenancy\RegisterTeam;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantRegistration(RegisterTeam::class);
}

自定义租户注册页面
你可以重写注册页基类上任何方法,使其按你的要求运行。甚至 $view 属性也可以被重写以使用你想要的自定义视图。
添加租户简介页面
简介也将允许用户编辑租户信息。
要设置简介页,你需要创建一个新的页面类,使之继承 Filament\Pages\Tenancy\EditTenantProfile。这是一个全页免 Livewire 组件。你可以将其放在任何希望的位置,比如 app/Filament/Pages/Tenancy/EditTeamProfile.php:
namespace App\Filament\Pages\Tenancy;
use Filament\Forms\Components\TextInput;
use Filament\Pages\Tenancy\EditTenantProfile;
use Filament\Schemas\Schema;
class EditTeamProfile extends EditTenantProfile
{
public static function getLabel(): string
{
return 'Team profile';
}
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name'),
// ...
]);
}
}
你可以添加任何表单组件到 form() 方法中。它们将直接保存到租户模型中。
现在,我们需要告诉 Filament 去使用该页面。我们可以在配置实现该功能:
use App\Filament\Pages\Tenancy\EditTeamProfile;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantProfile(EditTeamProfile::class);
}

自定义租户简介页
你可以重写覆盖简介页基类上的任何方法,使其按你的要求运行。甚至 $view 属性也可以被重写以使用你选择的自定义视图。
访问当前租户
在应用的任何地方,你都可以使用 Filament::getTenant() 方法访问当前请求的租户模型:
use Filament\Facades\Filament;
$tenant = Filament::getTenant();
账单
使用 Laravel Spark
Filament 提供 Laravel Spark 的账单集成。你的用户可开启订阅并管理它们的账单信息。
要安装该集成,首先要安装 Spark 并为你的租户模型配置它。
现在,你可以使用 Composer 安装 Filament 的 Spark billing provider:
composer require filament/spark-billing-provider
在配置中,将 tenantBillingProvider() 设置为 Spark:
use Filament\Billing\Providers\SparkBillingProvider;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantBillingProvider(new SparkBillingProvider());
}
现在,用户可以通过单击租户菜单中的链接来管理他们的账单。
要求订阅
要求订阅,才能使用应用的任何部分,你可以使用 requiresTenantSubscription() 的配置方法:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->requiresTenantSubscription();
}
现在,如果用户没有激活订阅,将会被重定向到账单页面。
要求对特定资源或页面 进行订阅
有时,你只希望应用中的特定资源和自定义页面需要订阅。你可以在资源或页面类的 isTenantSubscriptionRequired() 方法中返回 true 来实现该功能。
public static function isTenantSubscriptionRequired(Panel $panel): bool
{
return true;
}
你如果使用 requiresTenantSubscription() 配置方法,那么你可以从该方法中返回 false 以允许作为异常访问资源或者页面。
编写自定义账单集成
账单集成很容易编写。你只需实现 Filament\Billing\Providers\Contracts\Provider 接口。该接口有两个方法。
getRouteAction 用于获取用户访问账单页面时需要运行的路由动作。它可以是回调函数、或者控制器名、或者 Livewire 组件 - 任何当使用 Route::get() 时可以正常工作的行为。比如,你可以使用回调函数重定向到账单页面。
getSubscribedMiddleware() 返回检测租户是否激活订阅的中间件名。如果用户未激活订阅该中间件应该将用户重定向到账单页面。
下例是账单提供者,它为路由操作使用回调函数、为订阅中间件使用中间件:
use App\Http\Middleware\RedirectIfUserNotSubscribed;
use Filament\Billing\Providers\Contracts\BillingProvider;
use Illuminate\Http\RedirectResponse;
class ExampleBillingProvider implements BillingProvider
{
public function getRouteAction(): string
{
return function (): RedirectResponse {
return redirect('https://billing.example.com');
};
}
public function getSubscribedMiddleware(): string
{
return RedirectIfUserNotSubscribed::class;
}
}
自定义账单路由 slug
在配置中使用 tenantBillingRouteSlug() 方法,你可以自定义用于账单路由的 URL slug:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantBillingRouteSlug('billing');
}
自定义租户菜单
租户切换菜单是管理员(admin)布局的特色。它完全是可定制的。
每个菜单项是一个 Action,可用通过同样的方式自定义。要注册新菜单项到租户菜单中,你可以传递 Action 到配置 的 tenantMenuItems() 方法中:
use App\Filament\Pages\Settings;
use Filament\Actions\Action;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantMenuItems([
Action::make('settings')
->url(fn (): string => Settings::getUrl())
->icon('heroicon-m-cog-8-tooth'),
// ...
]);
}
对租户菜单项进行分组
默认情况下,所有租户菜单项都渲染在同一个列表中。如果你希 望将它们分为不同的组,可以向 tenantMenuItems() 方法传入一个包含多个数组的数组。每个数组都会渲染为一个独立的分组,并以分隔线隔开:
use App\Filament\Pages\Members;
use App\Filament\Pages\Settings;
use Filament\Actions\Action;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tenantMenuItems([
[
Action::make('settings')
->url(fn (): string => Settings::getUrl())
->icon('heroicon-m-cog-8-tooth'),
Action::make('members')
->url(fn (): string => Members::getUrl())
->icon('heroicon-m-user-group'),
],
[
Action::make('documentation')
->url('https://filamentphp.com/docs')
->icon('heroicon-m-book-open'),
],
]);
}

NOTE
register 项默认会被添加到最后一组中。若需自行指定位置,可使用 register 数组键将其显式注册到任意组内。由于默认的 sort() 行为会将其置于所在组的末尾,若希望将其放置在组内其他位置,则需调整排序设置:
->tenantMenuItems([
// ...
[
'register' => fn (Action $action): Action => $action->sort(2),
],
])



