FilamentLaravel

      Filament PHP: Automatic Hiding of Delete Buttons in Filament Using Relation-Based Laravel Policies

      This article explains a policy-based delete strategy for Filament, written in clear and practical terms.

      This approach is considered best practice because all delete logic is defined in one central place (Laravel Policies) and automatically applies everywhere:

      • table row actions
      • edit and view pages
      • bulk delete actions

      No duplicated logic, no UI hacks, and no inconsistent behavior.


      Why Use a Policy-Based Approach?

      Instead of manually hiding buttons or writing conditional logic in multiple Filament resources, Policies let Laravel decide whether an action is allowed.

      Filament respects Laravel authorization automatically.
      If a policy method returns false, the action:

      • disappears from the UI
      • cannot be executed
      • behaves consistently across the admin panel

      Example Domain Logic: Continents and Countries

      Let’s clarify the real-world logic used in this example.

      • A Continent can have many Countries
      • A Country belongs to exactly one Continent
      • Deleting a continent that still has countries would break data integrity

      Relationship example:

      // Continent model
      public function countries()
      {
          return $this->hasMany(Country::class);
      }
      

      This means:

      • Europe → France, Germany, Italy
      • Asia → Japan, China, Georgia
      • Africa → Egypt, Kenya, Nigeria

      If Europe still has countries linked to it, it must not be deleted.

      This rule is enforced entirely by the policy.


      Optimizing the Resource Query for Performance

      Before applying delete rules, the database query should be optimized.

      We preload the number of related countries so Filament does not execute extra queries per row.

      File: app/Filament/Resources/ContinentResource.php

      public static function getEloquentQuery(): \Illuminate\Database\Eloquent\Builder
      {
          return parent::getEloquentQuery()
              ->withCount(['countries']);
      }
      

      Now every continent record already contains:

      • countries_count

      This avoids expensive COUNT(*) queries during rendering.


      Central Delete Rules Using a Policy

      The policy is where all delete rules live.

      If the policy does not exist, it can be created with:

      php artisan make:policy ContinentPolicy --model=Continent
      

      File: app/Policies/ContinentPolicy.php

      public function delete($user, $continent): bool
      {
          return ($continent->countries_count ?? $continent->countries()->count()) === 0;
      }
      
      public function deleteAny($user): bool
      {
          return true;
      }
      
      public function forceDelete($user, $continent): bool
      {
          return false;
      }
      

      What this logic enforces

      • A continent can be deleted only if it has zero countries
      • If even one country exists → delete is denied
      • Bulk delete is allowed, but each continent is checked individually
      • Force delete is fully blocked at the authorization level

      Clean Filament Tables Without Extra Conditions

      Because Filament automatically checks policies, no manual hidden() logic is needed in tables or forms.

      ->actions([
          \Filament\Tables\Actions\DeleteAction::make(),
      ])
      ->bulkActions([
          \Filament\Tables\Actions\DeleteBulkAction::make(),
      ])
      

      Filament behavior:

      • Delete button appears only for empty continents
      • Delete button disappears automatically when countries exist
      • Bulk delete removes only eligible records
      • No error messages, no broken UX

      What This Architecture Achieves

      With this setup in place:

      • Continents with countries cannot be deleted
      • Empty continents can be safely removed
      • Bulk delete skips protected records silently
      • Performance stays high due to withCount
      • All delete logic is controlled from one policy file

      If business rules change later, only the policy needs to be updated.


      Final Thoughts

      This approach follows Laravel’s authorization philosophy, not UI tricks.

      It is:

      • predictable
      • scalable
      • safe for data integrity
      • easy to maintain

      For relational data like continents and countries, this pattern should be the default choice.

      Hi, I’m elliotkartvel