Skip to content
全部文档

多租户

简介

多租户是指单个应用实例同时服务多个客户。每个客户拥有自己的数据和访问规则,防止彼此查看或修改对方的数据。这是 SaaS 应用中的常见模式。用户通常属于用户组(常称为团队或组织)。记录归该组所有,用户可以同时是多个组的成员。这适合用户需要协作处理数据的应用。

多租户是非常敏感的话题。理解多租户的安全影响以及如何正确实现至关重要。若实现不完整或不正确,属于一个租户的数据可能暴露给另一个租户。Filament 提供一套工具帮助你在应用中实现多租户,但如何使用它们取决于你自己。

WARNING

Filament 不对应用的安全性作任何保证。确保应用安全是你的责任。更多信息请参阅 安全 一节。

简单的一对多租户

「多租户」一词含义很广,在不同语境下可能指不同事物。Filament 的多租户系统意味着用户属于多个租户(组织、团队、公司等),并可在它们之间切换。

若你的场景更简单、不需要多对多关系,则不必在 Filament 中设置多租户。可以改用 观察者全局作用域

假设你有数据库列 users.team_id,可以使用 全局作用域 将所有记录限定为与用户相同的 team_id

php
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,可以创建一个 观察者

php
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);
        }
    }
}

设置多租户

要设置多租户,需要在 配置 中指定「租户」(如团队或组织)模型:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class);
}

你还需要告知 Filament 用户属于哪些租户。可以在 App\Models\User 模型上实现 HasTenants 接口:

php
<?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

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 使用此页面。可以在 配置 中完成:

php
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

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 使用此页面。可以在 配置 中完成:

php
use App\Filament\Pages\Tenancy\EditTeamProfile;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantProfile(EditTeamProfile::class);
}
租户个人资料页租户个人资料页

自定义租户个人资料页

你可以重写基础个人资料页类上的任意方法,使其按你的需求工作。甚至可以重写 $view 属性,使用你选择的自定义视图。

访问当前租户

在应用中的任意位置,都可以使用 Filament::getTenant() 访问当前请求的租户模型:

php
use Filament\Facades\Filament;

$tenant = Filament::getTenant();

账单

使用 Laravel Spark

Filament 提供与 Laravel Spark 的账单集成。用户可以开通订阅并管理账单信息。

要安装该集成,请先 安装 Spark 并为你的租户模型完成配置。

然后可以用 Composer 安装 Spark 的 Filament 账单提供方:

bash
composer require filament/spark-billing-provider

配置 中,将 Spark 设为 tenantBillingProvider()

php
use Filament\Billing\Providers\SparkBillingProvider;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantBillingProvider(new SparkBillingProvider());
}

现在就完成了!用户可以通过点击租户菜单中的链接来管理账单。

要求订阅

若要使用应用的任何部分都需要订阅,可以使用 requiresTenantSubscription() 配置方法:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->requiresTenantSubscription();
}

现在,若用户没有有效订阅,会被重定向到账单页。

为特定资源和页面要求订阅

有时你可能只希望对应用中的某些 资源自定义页面 要求订阅。可以在资源或页面类上的 isTenantSubscriptionRequired() 方法中返回 true

php
public static function isTenantSubscriptionRequired(Panel $panel): bool
{
    return true;
}

若使用了 requiresTenantSubscription() 配置方法,则可从此方法返回 false,作为例外允许访问该资源或页面。

编写自定义账单集成

账单集成编写起来相当简单。你只需要一个实现 Filament\Billing\Providers\Contracts\Provider 接口的类。该接口有两个方法。

getRouteAction() 用于获取用户访问账单页时应运行的路由 action。可以是回调函数、控制器名称或 Livewire 组件——任何在 Laravel 中对 Route::get() 有效的内容。例如,你可以用回调函数简单重定向到自己的账单页。

getSubscribedMiddleware() 返回用于检查租户是否有有效订阅的中间件名称。若没有有效订阅,该中间件应将用户重定向到账单页。

下面是一个账单提供方示例,路由 action 使用回调函数,订阅检查使用中间件:

php
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:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantBillingRouteSlug('billing');
}

自定义租户菜单

租户切换菜单位于管理布局中,可完全自定义。

每个菜单项由一个 action 表示,自定义方式相同。要注册新项,可将 actions 传给 配置tenantMenuItems() 方法:

php
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() 传入数组的数组。每个数组渲染为一组,组间以分隔线隔开:

php
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'),
            ],
        ]);
}
分成独立分组的租户菜单项分成独立分组的租户菜单项

INFO

默认情况下,register 项会加到最后一组。若要自行放置,请在任意分组中用 register 数组键显式注册。其默认 sort() 会把它放在所在组末尾;若要放在组内其他位置,请调整 sort:

php
->tenantMenuItems([
    // ...
    [
        'register' => fn (Action $action): Action => $action->sort(2),
    ],
])

允许搜索租户

你可以在 配置 中使用 searchableTenantMenu() 方法,允许搜索租户:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->searchableTenantMenu();
}

当用户列表中的租户超过 10 个时,会自动启用。你可以使用 searchableTenantMenu(false) 禁用它。

要自定义租户菜单中的 注册 链接,请用 register 数组键注册新项,并传入一个 自定义 action 对象的函数:

php
use Filament\Actions\Action;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'register' => fn (Action $action) => $action->label('Register new team'),
            // ...
        ]);
}

要自定义租户菜单开头的个人资料链接,请用 profile 数组键注册新项,并传入一个 自定义 action 对象的函数:

php
use Filament\Actions\Action;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'profile' => fn (Action $action) => $action->label('Edit team profile'),
            // ...
        ]);
}

要自定义租户菜单中的账单链接,请用 profile 数组键注册新项,并传入一个 自定义 action 对象的函数:

php
use Filament\Actions\Action
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenuItems([
            'billing' => fn (Action $action) => $action->label('Manage subscription'),
            // ...
        ]);
}

有条件地隐藏租户菜单项

也可用 visible()hidden() 方法有条件地隐藏租户菜单项,并传入要检查的条件。传入函数会将条件求值推迟到实际渲染菜单时:

php
use Filament\Actions\Action;

Action::make('settings')
    ->visible(fn (): bool => auth()->user()->can('manage-team'))
    // or
    ->hidden(fn (): bool => ! auth()->user()->can('manage-team'))

从租户菜单项发送 POST HTTP 请求

可从租户菜单项发送 POST HTTP 请求:把 URL 传给 url(),并使用 postToUrl()

php
use Filament\Actions\Action;

Action::make('lockSession')
    ->url(fn (): string => route('lock-session'))
    ->postToUrl()

禁用租户切换器

默认情况下,用户可以使用租户菜单在租户之间切换。若希望保持租户菜单可见,但阻止用户切换租户,可以在 配置 中使用 tenantSwitcher() 方法:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantSwitcher(false);
}

这样会保持租户菜单可见,显示当前租户名称和任何自定义菜单项,但隐藏其他租户列表。适用于希望展示租户信息但不允许切换,或应通过其他方式控制切换的场景。

隐藏租户菜单

你可以使用 tenantMenu(false) 隐藏租户菜单

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMenu(false);
}

不过,这通常说明 Filament 的多租户功能不适合你的项目。若每个用户只属于一个租户,应坚持使用 简单的一对多租户

设置头像

开箱即用时,Filament 使用 ui-avatars.com 根据用户姓名生成头像。不过,若用户模型有 avatar_url 属性,则会优先使用它。若要自定义 Filament 获取用户头像 URL 的方式,可以实现 HasAvatar 契约:

php
<?php

namespace App\Models;

use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Illuminate\Database\Eloquent\Model;

class Team extends Model implements HasAvatar
{
    // ...

    public function getFilamentAvatarUrl(): ?string
    {
        return $this->avatar_url;
    }
}

getFilamentAvatarUrl() 方法用于获取当前用户的头像。若该方法返回 null,Filament 会回退到 ui-avatars.com

你可以轻松将 ui-avatars.com 换成其他服务,只需创建一个新的头像提供方。可在此了解做法。

配置租户关系

创建和列出与租户关联的记录时,Filament 需要每个资源的两个 Eloquent 关系:定义在资源模型类上的「所有权(ownership)」关系,以及租户模型类上的关系。默认情况下,Filament 会根据标准 Laravel 约定尝试猜测这些关系的名称。例如,若租户模型是 App\Models\Team,它会在资源模型类上查找 team() 关系。若资源模型类是 App\Models\Post,它会在租户模型类上查找 posts() 关系。

自定义所有权关系名称

你可以在 tenant() 配置方法上使用 ownershipRelationship 参数,一次性自定义所有资源使用的所有权关系名称。本例中,资源模型类定义了 owner 关系:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, ownershipRelationship: 'owner');
}

或者,你可以在资源类上设置静态属性 $tenantOwnershipRelationshipName,仅自定义该资源使用的所有权关系名称。本例中,Post 模型类定义了 owner 关系:

php
use Filament\Resources\Resource;

class PostResource extends Resource
{
    protected static ?string $tenantOwnershipRelationshipName = 'owner';

    // ...
}

自定义资源关系名称

你可以在资源类上设置静态属性 $tenantRelationshipName,用于自定义获取该资源时使用的关系名称。本例中,租户模型类定义了 blogPosts 关系:

php
use Filament\Resources\Resource;

class PostResource extends Resource
{
    protected static ?string $tenantRelationshipName = 'blogPosts';

    // ...
}

配置 slug 属性

使用团队一类的租户时,你可能希望在 URL 中使用 slug 字段而不是团队 ID。可以在 tenant() 配置方法上使用 slugAttribute 参数:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, slugAttribute: 'slug');
}

配置名称属性

默认情况下,Filament 会使用租户的 name 属性在应用中显示其名称。若要更改,可以实现 HasName 契约:

php
<?php

namespace App\Models;

use Filament\Models\Contracts\HasName;
use Illuminate\Database\Eloquent\Model;

class Team extends Model implements HasName
{
    // ...

    public function getFilamentName(): string
    {
        return "{$this->name} {$this->subscription_plan}";
    }
}

getFilamentName() 方法用于获取当前用户的名称。

设置当前租户标签

在租户切换器中,你可能希望在当前团队名称上方添加类似「Active team」的小标签。可以在租户模型上实现 HasCurrentTenantLabel 方法:

php
<?php

namespace App\Models;

use Filament\Models\Contracts\HasCurrentTenantLabel;
use Illuminate\Database\Eloquent\Model;

class Team extends Model implements HasCurrentTenantLabel
{
    // ...

    public function getCurrentTenantLabel(): string
    {
        return 'Active team';
    }
}

设置默认租户

登录时,Filament 会将用户重定向到 getTenants() 方法返回的第一个租户。

有时你可能希望更改此行为。例如,你可以存储上次活跃的团队,并将用户重定向到该团队。

要自定义此行为,可以在用户上实现 HasDefaultTenant 契约:

php
<?php

namespace App\Models;

use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasDefaultTenant;
use Filament\Models\Contracts\HasTenants;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class User extends Model implements FilamentUser, HasDefaultTenant, HasTenants
{
    // ...

    public function getDefaultTenant(Panel $panel): ?Model
    {
        return $this->latestTeam;
    }

    public function latestTeam(): BelongsTo
    {
        return $this->belongsTo(Team::class, 'latest_team_id');
    }
}

为租户感知路由应用中间件

你可以通过在 面板配置文件 中将中间件类数组传给 tenantMiddleware() 方法,为所有租户感知路由应用额外中间件:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            // ...
        ]);
}

默认情况下,中间件会在页面首次加载时运行,但不会在后续 Livewire AJAX 请求中运行。若要在每次请求都运行中间件,可以将 true 作为第二个参数传给 tenantMiddleware() 方法,使其持久化:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            // ...
        ], isPersistent: true);
}

添加租户路由前缀

默认情况下,URL 结构会把租户 ID 或 slug 紧接在面板路径之后。若希望再加一段 URL 前缀,请使用 tenantRoutePrefix() 方法:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->path('admin')
        ->tenant(Team::class)
        ->tenantRoutePrefix('team');
}

之前,租户 1 的 URL 结构是 /admin/1。现在是 /admin/team/1

使用域名识别租户

使用租户时,你可能希望使用域名或子域名路由,例如 team1.example.com/posts,而不是像 /team1/posts 这样的路由前缀。可以在 tenant() 配置方法之外,配合使用 tenantDomain() 方法。tenant 参数对应租户模型的 slug 属性:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, slugAttribute: 'slug')
        ->tenantDomain('{tenant:slug}.example.com');
}

上例中,租户位于主应用域名的子域名上。你也可以设置为从租户解析整个域名:

php
use App\Models\Team;
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenant(Team::class, slugAttribute: 'domain')
        ->tenantDomain('{tenant:domain}');
}

本例中,domain 属性应包含有效的域名主机,例如 example.comsubdomain.example.com

INFO

当对整个域名使用参数(tenantDomain('{tenant:domain}'))时,Filament 会为应用中所有 tenant 参数注册 全局路由参数模式[a-z0-9.\-]+。这是因为 Laravel 默认不允许路由参数中出现 . 字符。这可能与其他使用多租户的面板,或应用中其他使用 tenant 路由参数的部分冲突。

为资源禁用多租户

默认情况下,启用多租户的面板中的所有资源都会限定到当前租户。若有在租户之间共享的资源,可以在资源类上将静态属性 $isScopedToTenant 设为 false 来禁用多租户:

php
protected static bool $isScopedToTenant = false;

为所有资源禁用多租户

若希望改为对每个资源选择加入多租户,而不是选择退出,可以在 service provider 的 boot() 方法或中间件中调用 Resource::scopeToTenant(false)

php
use Filament\Resources\Resource;

Resource::scopeToTenant(false);

现在,可以通过在资源类上将静态属性 $isScopedToTenant 设为 true,为每个资源选择加入多租户:

php
protected static bool $isScopedToTenant = true;

多租户安全

理解多租户的安全影响以及如何正确实现至关重要。若实现不完整或不正确,属于一个租户的数据可能暴露给另一个租户。Filament 提供一套工具帮助你在应用中实现多租户,但如何使用它们取决于你自己。Filament 不对应用的安全性作任何保证。确保应用安全是你的责任。

以下是 Filament 提供的、帮助你在应用中实现多租户的功能列表:

  • 已启用多租户的面板中,[租户感知](#disabling-tenancy-for-a-resource) 资源的 Eloquent 模型查询会自动应用全局作用域。用于获取资源记录的查询会自动限定到当前租户。该查询用于渲染资源列表表格,也用于在编辑或查看记录时从当前 URL 解析记录。这意味着若用户试图查看不属于当前租户的记录,将收到 404 错误。 - 必须在已启用多租户的面板中存在 [租户感知](#disabling-tenancy-for-a-resource) 资源,该资源的模型才会应用全局作用域。若要为没有对应资源的模型限定查询,必须 [使用中间件为该模型应用额外的全局作用域](#using-tenant-aware-middleware-to-apply-additional-global-scopes)。 - 全局作用域在从请求中识别出租户之后才会应用。这发生在面板请求的中间件栈期间。若在识别租户之前发起查询,例如在栈中较早的中间件或 service provider 中,该查询不会限定到当前租户。要保证中间件在识别当前租户之后运行,应将其注册为 [租户中间件](#applying-middleware-to-tenant-aware-routes)。 - 如上所述,在已启用多租户的面板之外发起的查询无法访问当前租户,因此不会被限定。若有疑问,请在部署应用前检查查询是否已正确限定。 - 若需要为特定查询禁用多租户全局作用域,可以在该查询上使用 `withoutGlobalScope(filament()->getTenancyScopeName())` 方法。 - 若任何查询禁用了所有全局作用域,多租户全局作用域也会被禁用。使用此方法时应谨慎,因为它可能导致数据泄漏。若需要禁用除多租户全局作用域之外的所有全局作用域,可以使用 `withoutGlobalScopes()` 方法并传入要禁用的全局作用域数组。
  • 新创建的 Eloquent 模型会自动与当前租户关联。为 [租户感知](#disabling-tenancy-for-a-resource) 资源创建新记录时,租户会自动关联到该记录。这意味着记录将属于当前租户,外键列会自动设为租户 ID。这是通过 Filament 在资源的 Eloquent 模型上为 `creating` 和 `created` 事件注册监听器完成的。 - 必须在已启用多租户的面板中存在 [租户感知](#disabling-tenancy-for-a-resource) 资源,该资源的模型才会发生自动关联。若要为没有对应资源的模型实现自动关联,必须为该模型 [注册 `creating` 事件的监听器](https://laravel.com/docs/eloquent#events),并将 `filament()->getTenant()` 与之关联。 - 这些事件在从请求中识别出租户之后才会运行。这发生在面板请求的中间件栈期间。若在识别租户之前创建模型,例如在栈中较早的中间件或 service provider 中,它不会与当前租户关联。要保证中间件在识别当前租户之后运行,应将其注册为 [租户中间件](#applying-middleware-to-tenant-aware-routes)。 - 如上所述,在已启用多租户的面板之外创建的模型无法访问当前租户,因此不会被关联。若有疑问,请在部署应用前检查模型是否已正确关联。 - 若需要为特定模型禁用自动关联,可以在创建时临时 [静音事件](https://laravel.com/docs/eloquent#muting-events)。若现有代码正在这样做或永久移除了事件监听器,应检查这是否影响多租户功能。

uniqueexists 校验

Laravel 的 uniqueexists 校验规则默认不使用 Eloquent 模型查询数据库,因此不会使用模型上定义的任何全局作用域,包括多租户相关的。因此,即使其他租户中存在具有相同值的软删除记录,校验也会失败。

若希望两个租户完全数据隔离,应改用 scopedUnique()scopedExists() 方法。它们用通过模型查询数据库的实现替换 Laravel 的 uniqueexists,从而应用模型上定义的任何全局作用域,包括多租户相关的:

php
use Filament\Forms\Components\TextInput;

TextInput::make('email')
    ->scopedUnique()
    // or
    ->scopedExists()

更多信息请参阅 校验文档 中的 unique()exists()

使用租户感知中间件应用额外的全局作用域

由于只有面板中存在对应资源的模型才会自动限定到当前租户,在面板中使用其他 Eloquent 模型时,为它们应用额外的租户限定可能会很有用。这样你就不必再操心把查询限定到当前租户,而是自动应用限定。为此,可以创建一个类似 ApplyTenantScopes 的新中间件类:

bash
php artisan make:middleware ApplyTenantScopes

handle() 方法中,你可以应用任何所需的全局作用域:

php
use App\Models\Author;
use Closure;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class ApplyTenantScopes
{
    public function handle(Request $request, Closure $next)
    {
        Author::addGlobalScope(
            'tenant',
            fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant()),
        );

        return $next($request);
    }
}

现在可以为所有租户感知路由 注册此中间件,并通过使其持久化,确保它在所有 Livewire AJAX 请求中都会使用:

php
use Filament\Panel;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->tenantMiddleware([
            ApplyTenantScopes::class,
        ], isPersistent: true);
}