Languages

Version

Theme

用户

多租户

介绍

多租户(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();

Billing

使用 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 到[配置]](../panel-configuration) 的 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'),
            // ...
        ]);
}

自定义注册链接

要自定义租户菜单上的注册链接,请使用 register 作为数组键,并传入一个自定义其 Action对象的函数:

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 链接

要在租户按钮上自定义 Profile 链接,请使用 profile 作为数组键,并传入一个自定义 Action对象的函数:

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'),
            // ...
        ]);
}

自定义账单链接

要自定义租户菜单上的账单链接,请先使用 billing 作为数组键,并传入一个自定义 Action对象的函数:

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() 方法,并传入条件,你也可以条件性地隐藏租户菜单项。传入函数则会推迟条件求值,直到菜单被实际渲染:

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 请求

你可用通过将 URL 传入到 url() 方法,同时调用 postToUrl(),从租户菜单项中发送 POST HTTP 请求:

use Filament\Actions\Action;

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

隐藏租户菜单

使用 tenantMenu(false) 方法,你可以隐藏租户菜单:

use Filament\Panel;

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

不过可能这是 Filament 租户特性不适合于你的项目的一个信号。如果每个用户只属于一个租户,你应该使用一对多租户

设置头像

开箱即用,Filament 使用 ui-avatars.com 去生成基于用户名的头像。不过,如果你的用户模型中有一个 avatar_url 属性,则会使用该属性。要自定义 Filament 如何获取用户头像 URL,你可以实现 HasAvatar 契约:

<?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() 关联。

自定义 ownership 关联名

你可以在 tenant() 配置方法中使用 ownershipRelationship 参数,自定义跨所有资源使用的 ownership 的关联名。本例中,资源模型类定义了一个 owner 关联:

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

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

此外,你可以在资源类上设置 $tenantOwnershipRelationshipName 静态属性,然后可以使用该属性自定义仅用于该资源的所有权(ownership)关联。在这个例子中,Post 模型类定义了一个 owner 关联:

use Filament\Resources\Resource;

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

    // ...
}

自定义资源所有权关联名

你可以在资源类上设置 $tenantRelationshipName 静态属性,然后可以使用该属性自定义获取该资源的关联。本例中,租户模型定义了一个 blogPosts 关联:

use Filament\Resources\Resource;

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

    // ...
}

配置 slug 属性

使用像团队这样的租户时,你可能想添加 slug 字段而不是使用团队 ID 作为 URL。你可以在 tenant() 配置方法中的 slugAttribute 参数实现该目的:

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

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

配置 name 属性

默认情况下,Filament 会使用租户的 name 属性在应用中展示其名称。要对此进行修改,可以实现 HasName 契约:

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

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 会将用户重定向到 getTenant() 方法返回的第一个租户。

有时,你可能希望对此进行修改。比如,你可能会保持哪个团队是最后活跃的,并将用户重定向到那个团队。

要自定义该内容,你可以在用户上实现 HasDefaultTenant 契约:

<?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');
    }
}

将中间件应用到 tenant-aware 路由

你可以在面板配置文件中传入一个中间件类数组到 tenantMiddleware() 方法,将其他的中间件应用到所有租户感知(tenant-aware)的路由:

use Filament\Panel;

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

默认情况下,中间件会在页面首次加载时运行,但不会在随后的 Livewire AJAX 请求中运行。如果你想在每个请求时都运行中间件,你可以传入 true 作为 tenantMiddleware() 方法的第二个参数,使之持久化:

use Filament\Panel;

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

添加租户路由前缀

默认情况下,URL 结构会将 tenant ID 或 slug 放在面板路径之后。如果你想使用其他 URL 分段加上前缀,请使用 tenantRoutePrefix() 方法:

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 这样的路由前缀。你可以使用 tenantDomain() 方法以及 tenant() 配置方法来实现该功能。tenant 参数对应于该租户模型的 slug 属性:

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

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

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

NOTE

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

禁用某个资源的租户

默认情况下,面板中的所有资源将租约范围设置为当前租户。如果你的资源要在不同租户之间共享,你可以通过在资源类上将 $isScopedToTenant 静态属性设置成 false 禁用租约:

protected static bool $isScopedToTenant = false;

禁用所有资源的租赁

如果你想让每个资源的租赁使用 opt-in 而非 opt-out,你可以在服务提供者的 boot() 方法或中间件中调用 Resource::scopeToTenant(false)

use Filament\Resources\Resource;

Resource::scopeToTenant(false);

现在,通过将资源类上的 $isScopedToTenant 静态属性设置为 true 时,你可以选择性加入(opt-in)到租户:

protected static bool $isScopedToTenant = true;

租户安全

理解多租户的安全含义以及如何合理地实现是很重要的。如果部分或错误地实现,属于一个租户的数据可能会暴露给另一个租户。Filament 提供了一组工具来帮助你在应用中实现多租户,但如何使用这些工具取决于你。Filament 不对你的应用的安全性提供任何保证。你有责任确保你的应用是安全的。

以下是 Filament 提供的帮你在应用中实现多租户的一系列特性:

  • 自动全局作用域的 Eloquent 模型查询,适用于[租户感知] 资源,这些资源属于已启用租户的面板。用于获取资源记录的查询会自动限定在当前租户内。此查询用于渲染资源的列表表格,也用于在编辑或查看记录时从当前 URL 解析记录。这意味着如果用户尝试查看不属于当前租户的记录,他们将收到 404 错误。

    • A tenant-aware resource has to exist in the panel with tenancy enabled for the resource’s model to have the global scope applied. If you want to scope the queries for a model that does not have a corresponding resource, you must use middleware to apply additional global scopes for that model.
    • The global scopes are applied after the tenant has been identified from the request. This happens during the middleware stack of panel requests. If you make a query before the tenant has been identified, such as from early middleware in the stack or in a service provider, the query will not be scoped to the current tenant. To guarantee that middleware runs after the current tenant is identified, you should register it as tenant middleware.
    • As per the point above, queries made outside the panel with tenancy enabled do not have access to the current tenant, so are not scoped. If in doubt, please check if your queries are properly scoped or not before deploying your application.
    • If you need to disable the tenancy global scope for a specific query, you can use the withoutGlobalScope(filament()->getTenancyScopeName()) method on the query.
    • If any of your queries disable all global scopes, the tenancy global scope will be disabled as well. You should be careful when using this method, as it can lead to data leakage. If you need to disable all global scopes except the tenancy global scope, you can use the withoutGlobalScopes() method passing an array of the global scopes you want to disable.
  • Automatic association of newly created Eloquent models with the current tenant. When a new record is created for a tenant-aware resource, the tenant is automatically associated with the record. This means that the record will belong to the current tenant, as the foreign key column is automatically set to the tenant’s ID. This is done by Filament registering an event listener for the creating and created events on the resource’s Eloquent model. If you need to disable this feature for a specific model, you can use the withoutTenancy() method on the model.

    • A tenant-aware resource has to exist in the panel with tenancy enabled for the resource’s model to have the automatic association to happen. If you want automatic association for a model that does not have a corresponding resource, you must register a listener for the creating event for that model, and associate the filament()->getTenant() with it.
    • The events run after the tenant has been identified from the request. This happens during the middleware stack of panel requests. If you create a model before the tenant has been identified, such as from early middleware in the stack or in a service provider, it will not be associated with the current tenant. To guarantee that middleware runs after the current tenant is identified, you should register it as tenant middleware.
    • As per the point above, models created outside the panel with tenancy enabled do not have access to the current tenant, so are not associated. If in doubt, please check if your models are properly associated or not before deploying your application.
    • If you need to disable the automatic association for a particular model, you can mute the events temporarily while you create it. If any of your code currently does this or removes event listeners permanently, you should check this is not affecting the tenancy feature.

uniqueexists 验证

Laravel 的 uniqueexists 验证规则默认不使用 Eloquent 模型去查询数据库,因此,它不会使用定义在模型上的全局查询范围限定(global scope),包括多租户。因此,即使在不同租户中有一个使用同一个值的软删除记录,验证也会失败。

如果你想让两个租户有完全的数据分离,你应该使用 scopedUnique()scopedExists() 方法,它们将 Laravel 的 uniqueexists 实现替换为使用模型查询数据库的实现,应用模型上定义的任何全局查询范围限定,包括多租户:

use Filament\Forms\Components\TextInput;

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

更多信息,请查阅验证文档中的 unique()exists()

使用 tenant-aware 中间件设置额外的全局查询范围

Since only models with resources that exist in the panel are automatically scoped to the current tenant, it might be useful to apply additional tenant scoping to other Eloquent models while they are being used in your panel. This would allow you to forget about scoping your queries to the current tenant, and instead have the scoping applied automatically. To do this, you can create a new middleware class like ApplyTenantScopes:

php artisan make:middleware ApplyTenantScopes

handle() 方法中,你可以应用你想要使用的全局查询范围限定:

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

You can now register this middleware for all tenant-aware routes, and ensure that it is used across all Livewire AJAX requests by making it persistent:

use Filament\Panel;

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

Still need help? Join our Discord community or open a GitHub discussion

Previous
多因素认证