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.