#18 FilamentPHP応用

アクセス制御の深掘り——canCreate/canEdit/canDeleteのカスタマイズ

FilamentのアクセスコントロールはPolicyと連動する

FilamentのResourceは、デフォルトでLaravelの認可ポリシー(Policy)を自動的に参照します。PostPolicyが存在すれば、Filamentの一覧・作成・編集・削除の各操作は自動的にポリシーでチェックされます。

まずポリシーの基本構造を確認しましょう。

php artisan make:policy PostPolicy --model=Post
namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    // 一覧の表示権限
    public function viewAny(User $user): bool
    {
        return $user->hasPermissionTo('posts.viewAny');
    }

    // 詳細の表示権限
    public function view(User $user, Post $post): bool
    {
        return $user->hasPermissionTo('posts.view');
    }

    // 作成権限
    public function create(User $user): bool
    {
        return $user->hasPermissionTo('posts.create');
    }

    // 更新権限
    public function update(User $user, Post $post): bool
    {
        // 管理者か、記事の著者のみ編集可能
        return $user->hasRole('admin')
            || ($user->hasPermissionTo('posts.update') && $post->user_id === $user->id);
    }

    // 削除権限
    public function delete(User $user, Post $post): bool
    {
        return $user->hasRole('admin');
    }

    // 一括削除権限(Filament専用)
    public function deleteAny(User $user): bool
    {
        return $user->hasRole('admin');
    }

    // 論理削除済みレコードの復元権限
    public function restore(User $user, Post $post): bool
    {
        return $user->hasRole('admin');
    }

    // 完全削除権限
    public function forceDelete(User $user, Post $post): bool
    {
        return $user->hasRole('super_admin');
    }
}

Resourceでのオーバーライド

ポリシーより詳細な制御が必要な場合は、Resourceクラスでcan系メソッドをオーバーライドします。

class PostResource extends Resource
{
    /**
     * 「新規作成」ボタンの表示制御
     */
    public static function canCreate(): bool
    {
        return auth()->user()->hasAnyRole(['admin', 'editor']);
    }

    /**
     * 各レコードの編集権限
     */
    public static function canEdit(Model $record): bool
    {
        $user = auth()->user();
        return $user->hasRole('admin')
            || ($user->hasRole('editor') && $record->user_id === $user->id);
    }

    /**
     * 各レコードの削除権限
     */
    public static function canDelete(Model $record): bool
    {
        return auth()->user()->hasRole('admin');
    }

    /**
     * 一覧ページ(パネル全体)へのアクセス
     */
    public static function canAccess(): bool
    {
        return auth()->user()->hasAnyRole(['admin', 'editor', 'viewer']);
    }

    /**
     * テーブルから特定レコードを「閲覧」できるか
     */
    public static function canView(Model $record): bool
    {
        return auth()->check();
    }
}

アクションレベルでの制御

テーブルのカスタムActionにも認可チェックを組み込みます。

Action::make('publish')
    ->label('公開する')
    ->visible(fn (Post $record): bool =>
        auth()->user()->can('publish', $record)
        && $record->status === 'draft'
    )
    ->action(function (Post $record): void {
        // アクション実行前にもチェック(可視性とは別に重要)
        abort_unless(auth()->user()->can('publish', $record), 403);

        $record->update([
            'status' => 'published',
            'published_at' => now(),
        ]);
    })

ポリシーにpublishメソッドを追加:

// PostPolicy.php
public function publish(User $user, Post $post): bool
{
    return $user->hasAnyRole(['admin', 'editor'])
        && $post->status === 'draft';
}

フォームフィールドの編集制御

特定フィールドをロールによって読み取り専用にする:

public static function form(Form $form): Form
{
    return $form
        ->schema([
            TextInput::make('title')
                ->label('タイトル')
                ->required(),

            Select::make('status')
                ->label('ステータス')
                ->options([
                    'draft'     => '下書き',
                    'published' => '公開中',
                    'archived'  => 'アーカイブ',
                ])
                // 管理者のみステータスを変更できる
                ->disabled(fn (): bool => ! auth()->user()->hasRole('admin'))
                ->dehydrated(true),  // disabledでもフォーム送信時に値を含める

            TextInput::make('internal_notes')
                ->label('内部メモ(管理者のみ)')
                ->visible(fn (): bool => auth()->user()->hasRole('admin')),
        ]);
}

テーブルカラムのアクセス制御

センシティブな情報を含むカラムをロールに応じて表示/非表示にします。

public function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('title')
                ->searchable(),

            TextColumn::make('author.email')
                ->label('投稿者メール')
                // 管理者のみ表示
                ->visible(fn (): bool => auth()->user()->hasRole('admin')),

            TextColumn::make('status')
                ->badge(),

            TextColumn::make('revenue')
                ->money('JPY')
                // 特定権限がある場合のみ表示
                ->visible(fn (): bool => auth()->user()->hasPermissionTo('view.revenue')),
        ]);
}

FilamentShieldとの連携

bezhansalleh/filament-shieldパッケージを使うと、ResourceごとのCRUD権限をUIで管理できます。

composer require bezhansalleh/filament-shield
php artisan shield:install --fresh
php artisan shield:generate --all

インストール後、ShieldはResourceに応じた権限(view_any_postcreate_postupdate_postdelete_postなど)を自動生成し、ロールへの割り当てUIが管理画面に追加されます。

// Resourceでの有効化
class PostResource extends Resource
{
    // PolicyはShieldが生成したPostPolicyを使う(自動)
    // canCreate, canEdit, canDeleteは自動的にShieldの権限チェックと連動

    public static function canAccess(): bool
    {
        return auth()->user()->can('view_any_post');
    }
}

Gate(ゲート)との組み合わせ

ポリシーではなくGateでシンプルに権限定義する場合:

// app/Providers/AppServiceProvider.php

Gate::define('manage-settings', fn (User $user): bool =>
    $user->hasRole('super_admin')
);

Gate::define('view-analytics', fn (User $user): bool =>
    $user->hasAnyRole(['admin', 'analyst'])
);
// Filamentのカスタムページ
class Analytics extends Page
{
    public static function canAccess(): bool
    {
        return Gate::allows('view-analytics');
    }
}

コツ・注意点・ハマりポイント

コツ: ->visible()は表示/非表示のみでサーバー側の保護にはなりません。アクション実行時にabort_unless()でサーバー側でも必ず認可チェックを行ってください。

注意点: ->disabled(fn)でフィールドを無効化しても、dehydrated(false)を設定しない限りフォーム送信時に値が送られます。逆に->dehydrated(true)で明示することで、disabled状態でも値の更新ができます。

ハマりポイント: canDeleteAnycanDeleteは別メソッドです。一括削除ボタンを非表示にしたい場合はcanDeleteAnyも実装する必要があります。PolicyのdeleteAnyに対応しています。

まとめ

FilamentのアクセスコントロールはLaravelのPolicy・Gate・カスタムcan系メソッドの3層で構成されています。Policyで基本ルールを定義し、Resourceのオーバーライドで細かい例外を処理し、フォーム/テーブルレベルでUIの可視性を制御するという3段階のアプローチで堅牢な権限管理が実現できます。