#07 FilamentPHP応用

マルチテナンシー——テナント別データ分離

マルチテナンシーとは

マルチテナンシーとは、1つのアプリケーションを複数の「テナント」(組織・チーム・企業など)が共有しながら、それぞれのデータを完全に分離する設計パターンです。SaaSアプリケーションでよく使われます。

FilamentはV3以降マルチテナンシーをネイティブサポートしており、v5でも引き続きテナント切り替えUI・URLルーティング・データスコープが自動で処理されます。

セットアップ:->tenant(Team::class) でテナント設定

まずテナントモデル(例:Team)を作成します。

php artisan make:model Team -m

teamsテーブルのマイグレーション:

Schema::create('teams', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

// team_user ピボットテーブル
Schema::create('team_user', function (Blueprint $table) {
    $table->foreignId('team_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('role')->default('member');
    $table->timestamps();
});

PanelProviderでテナントを設定します。

// app/Providers/Filament/AdminPanelProvider.php

public function panel(Panel $panel): Panel
{
    return $panel
        ->default()
        ->id('admin')
        ->path('admin')
        ->tenant(Team::class)
        ->tenantRoutePrefix('team')  // URL: /admin/team/{team}/posts
        ->tenantSlugAttribute('slug') // URLに使う属性

HasTenants インターフェースの実装

UserモデルにHasTenantsインターフェースを実装します。

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)
            ->withPivot('role')
            ->withTimestamps();
    }

    public function getTenants(Panel $panel): Collection
    {
        return $this->teams;
    }

    public function canAccessTenant(Model $tenant): bool
    {
        return $this->teams()->whereKey($tenant)->exists();
    }

    public function canAccessPanel(Panel $panel): bool
    {
        return true;
    }
}

getTenants() の実装

getTenants()はユーザーがアクセスできるテナントの一覧を返します。ここでロールや有効期限によるフィルタリングを追加できます。

public function getTenants(Panel $panel): Collection
{
    // アクティブなチームのみ返す
    return $this->teams()
        ->wherePivot('status', 'active')
        ->get();
}

canAccessTenant() のアクセス制御

特定テナントへのアクセス権限チェックです。よりきめ細かい制御の例:

public function canAccessTenant(Model $tenant): bool
{
    return $this->teams()
        ->whereKey($tenant)
        ->wherePivot('status', 'active')
        ->exists();
}

管理者は全テナントにアクセスできるようにする場合:

public function canAccessTenant(Model $tenant): bool
{
    if ($this->is_super_admin) {
        return true;
    }

    return $this->teams()->whereKey($tenant)->exists();
}

テナントの切り替えUI

テナントが設定されると、パネルのサイドバー上部に自動的にテナント切り替えセレクターが表示されます。現在のテナント名と切り替えドロップダウンが一体になったUIです。

テナント情報の表示をカスタマイズするにはTeamモデルに以下を実装します。

// app/Models/Team.php

use Filament\Models\Contracts\HasCurrentTenantLabel;

class Team extends Model implements HasCurrentTenantLabel
{
    public function getCurrentTenantLabel(): string
    {
        return $this->name;
    }

    // アバター画像を表示する場合
    public function getFilamentAvatarUrl(): ?string
    {
        return $this->logo_url;
    }
}

->tenantRoutePrefix() でURL設定

テナントを識別するURLのプレフィックスを設定します。

->tenant(Team::class)
->tenantRoutePrefix('workspace')
// URL例: /admin/workspace/my-team/posts

スラッグの代わりにIDをURLに使いたい場合:

->tenant(Team::class, slugAttribute: 'id')
// URL例: /admin/team/5/posts

テナント登録フロー ->registerTenant()

新規ユーザーがチームを作成できる登録フローを設定できます。

->tenant(Team::class)
->tenantRegistration(RegisterTeam::class)
// app/Filament/Pages/Tenancy/RegisterTeam.php

namespace App\Filament\Pages\Tenancy;

use Filament\Pages\Tenancy\RegisterTenant;
use Filament\Forms\Form;
use App\Models\Team;

class RegisterTeam extends RegisterTenant
{
    public static function getLabel(): string
    {
        return 'チームを作成する';
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name')
                    ->label('チーム名')
                    ->required()
                    ->maxLength(255),

                TextInput::make('slug')
                    ->label('URL識別子')
                    ->required()
                    ->unique(Team::class, 'slug')
                    ->alphaNum()
                    ->maxLength(50),
            ]);
    }

    protected function handleRegistration(array $data): Team
    {
        $team = Team::create($data);
        $team->members()->attach(auth()->user(), ['role' => 'owner']);
        return $team;
    }
}

テナントプロファイルページ

テナントの設定編集ページも同様に作成できます。

->tenantProfile(EditTeamProfile::class)
class EditTeamProfile extends EditTenantProfile
{
    public static function getLabel(): string
    {
        return 'チーム設定';
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name')->label('チーム名')->required(),
                FileUpload::make('logo')->label('ロゴ')->image(),
            ]);
    }
}

グローバルスコープとテナントスコープの使い分け

テナントを有効にすると、Filamentは自動的に全クエリにWHERE team_id = ?相当のスコープを付与します。ただしモデル側にリレーションが必要です。

// posts テーブルに team_id カラムを追加
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('team_id')->constrained()->cascadeOnDelete();
});

// Post モデル
class Post extends Model
{
    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class);
    }
}

Filamentはテナントモデルと子モデルの関係を自動解決します。belongsTohasManymorphToなどのリレーションが対応しています。

一部のリソースはテナントスコープを無効にしたい場合(例:マスタデータ):

class CategoryResource extends Resource
{
    // このリソースはテナントフィルタを適用しない
    public static function isScopedToTenant(): bool
    {
        return false;
    }
}

テナント情報を取得する

コードの中で現在のテナントを参照するには:

use Filament\Facades\Filament;

$tenant = Filament::getTenant();  // 現在のTeamインスタンス

// アクションの中で
Action::make('createPost')
    ->action(function (): void {
        Post::create([
            'team_id' => Filament::getTenant()->id,
            'title' => $this->data['title'],
        ]);
    })

まとめ

FilamentのマルチテナンシーはPanelProviderに->tenant(Team::class)を追加するだけで基本的な仕組みが動作します。HasTenantsインターフェースの実装・データスコープ・テナント登録フローを組み合わせることで、本格的なSaaSアプリケーションの管理画面が構築できます。次のエピソードでは自作プラグインの作り方を解説します。