マルチテナンシー——テナント別データ分離
マルチテナンシーとは
マルチテナンシーとは、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はテナントモデルと子モデルの関係を自動解決します。belongsTo・hasMany・morphToなどのリレーションが対応しています。
一部のリソースはテナントスコープを無効にしたい場合(例:マスタデータ):
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アプリケーションの管理画面が構築できます。次のエピソードでは自作プラグインの作り方を解説します。