Support policy responses in filament actions

A short tutorial on how to use Laravel's policy responses in your Filament actions

Lukas Frey Executive director
published 3 weeks ago

Introduction

The FilamentPHP framework has quite a good support for policies out of the box. All default filament actions automatically check your policies and authorizes the actions correctly.

However sometimes you might need a little bit more control over the authorization's response rather than a simple true/false response. This is where Policy Responses from Laravel come in handy.

As of filament v3, there is no built in support for these though. Another issue is that in filament v3, you are not able to check the policies for each record individually in a bulk delete action. With this trick, you are able to overcome this limitation. Keep in mind that this can have a bad impact on your performance if a lot of records are being deleted, which is why it is not supported by default in filament. Use this trick wisely when working with bulk delete actions!

In this quick tutorial, I will show you a simple way to add support for policy responses to your delete and bulk delete actions.

While this tutorial focuses on delete and bulk delete actions, you can use the same approach to add support for policy responses to any other actions you might like.

Writing a policy

We need to write a policy for our model which will return a Policy Response. If you know how to do this or already have one created, you can skip this step.

class MyModelPolicy {

    public function delete(User $user, Model $record): bool | Response
    {
        // Return true if allowed

        // ...

        // Here we checked that the delete is not allowed, so we return a Policy Response
        return Response::deny('You are not allowed to delete this record, because it has associations with other models.');
    }

}

Creating a wrapper action

First we need to create a wrapper action, which will be responsible for configuring the base filament action to check for policy responses.

Actions that interact with a single record (Table actions) and bulk actions both need to be implemented a little bit differently.

See the section depending on which action you are interested in.

Delete action

class ConditionableDeleteAction
{
    use Configurable;

    public function __construct(
        protected MountableAction $action,
    ) {}

    public function configure(): static
    {
        $this->action
            ->authorize('deleteAny')
            ->mountUsing(static function (MountableAction $action, Model $record) {
                $response = Gate::inspect('delete', $record);

                if ($response->allowed()) {
                    return;
                }

                // Only modify the action if the response contains a message - otherwise it might just have been a bool with the `false` value.
                if ($message = $response->message()) {
                    $action
                        ->modalDescription(Markdown::inline($message))
                        ->modalSubmitAction(fn ($action) => $action->hidden())
                    ;
                }
            })
        ;

        return $this;
    }

    public static function for(MountableAction $action): MountableAction
    {
        return self::make($action)->action;
    }

    public static function make(MountableAction $action): static
    {
        return app(static::class, [
            'action' => $action,
        ])
            ->configure()
        ;
    }
}

This action, when mounted, will check whether we are allowed to delete the related record. If not and we have returned a policy response (instead of a simple false boolean), we read the message from the response and display it in the confirmation modal. Additionally, we also hide the delete button.

Bulk delete action

class ConditionableDeleteBulkAction
{
    use Configurable;

    public function __construct(
        protected BulkAction $action,
    ) {}

    public function configure(): static
    {
        $this->action
            ->authorize('deleteAny')
            ->using(static function (BulkAction $action, Collection $records, ListRecords $livewire) {
                $deleted = 0;
                foreach ($records as $record) {
                    $response = Gate::inspect('delete', $record);

                    if ($response->allowed()) {
                        $record->delete();
                        $deleted++;

                        continue;
                    }

                    // Send a failure notification for each record that cannot be deleted
                    if ($message = $response->message()) {
                        $action
                            ->failureNotificationTitle(Markdown::inline($message))
                            ->sendFailureNotification()
                        ;
                    }
                }

                $livewire->unmountTableBulkAction();

                // Nothing deleted, do not show success notification
                if ($deleted === 0) {
                    $action->halt();
                }

                // Leave out for default message from filament or customize to your liking
                $action->successNotificationTitle("Deleted $deleted records");

                // The success notification will be sent here automatically by filament
            })
        ;

        return $this;
    }

    public static function for(BulkAction $action): BulkAction
    {
        return self::make($action)->action;
    }

    public static function make(BulkAction $action): static
    {
        return app(static::class, [
            'action' => $action,
        ])
            ->configure()
        ;
    }
}

Using in a table resource

And that's it! Now you can use it in your model resource such as:

public static function table(Table $table): Table
    {
        return $table
            ->columns([
                // ...
            ])
            ->actions([
                // ...
                ConditionableDeleteAction::for(Tables\Actions\DeleteAction::make()),
            ])
            ->bulkActions([
                Tables\Actions\BulkActionGroup::make([
                    ConditionableDeleteBulkAction::for(Tables\Actions\DeleteBulkAction::make()),
                ]),
            ])
        ;
    }

Now whenever you click on the delete action, if deleting is not possible, the reason provided in your policy response will be shown in the confirmation modal.

Similarly, when attempt to bulk delete records, for each record that you are not authorized to delete, a notification will be sent with the message from the policy response.

Using a macro

The above implementation is just one of many ways you can achieve the same result.

If you prefer, you could for example add a macro to the delete action instead:

Tables\Actions\DeleteAction::macro('checkPolicyResponses', function() {
    // Same code as in the configure method of the ConditionableDeleteAction class
});

// Usage in resource table:
Tables\Actions\DeleteAction::make()->checkPolicyResponses();
Share this article

Written by
Lukas Frey Executive director
Contact me lukas.frey@guava.cz
Get in touch