Skip to content

Filament PHP: The Admin Panel I Stopped Rolling Myself

Filament PHP: The Admin Panel I Stopped Rolling Myself

Confession: I spent about six years writing Laravel admin panels by hand. Blade templates, a custom CRUD scaffolder I was secretly proud of, a thousand tiny @if blocks for permissions. Then a client asked me to ship an internal tool in four days and I finally tried Filament PHP properly. I have not started a new admin from scratch since.

This post is not a Filament tutorial. The official docs are good and you should read them. This is a working-engineer take on which Filament patterns I actually keep around after the prototype stage, where it pays off compared to a hand-rolled Laravel admin, and the two spots where I still drop to raw Blade and don’t feel bad about it.

What I was doing before Filament

My old admin stack looked like this: a routes/admin.php file, a BaseAdminController, a generic resources/views/admin/crud set of Blade includes, a permissions middleware, and a tiny custom form builder I wrote so I could stop typing the same <select> over and over.

It worked. It also rotted. Every project ended up with the same three problems: filters and search were always slightly different, the table component was always almost good enough, and bulk actions were always reimplemented from scratch because nobody on the team remembered the last shape they took.

Here’s the kind of code I used to ship for a basic listing screen:

// app/Http/Controllers/Admin/PostsController.php
public function index(Request $request)
{
    $posts = Post::query()
        ->when($request->search, fn ($q, $s) =>
            $q->where('title', 'like', "%{$s}%"))
        ->when($request->status, fn ($q, $s) => $q->where('status', $s))
        ->latest()
        ->paginate(20);

    return view('admin.posts.index', compact('posts'));
}

And a paired Blade file with a hand-rolled table, pagination, search box, and bulk delete form. Multiply that by every model and you have my old life.

What Filament actually replaces

A Filament Resource is the same idea, just folded into one declarative class. Here is roughly the same listing in Filament v3 syntax:

// app/Filament/Resources/PostResource.php
public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('title')->searchable()->sortable(),
            BadgeColumn::make('status')->colors([
                'success' => 'published',
                'warning' => 'draft',
            ]),
            TextColumn::make('created_at')->dateTime()->sortable(),
        ])
        ->filters([
            SelectFilter::make('status')
                ->options(['draft' => 'Draft', 'published' => 'Published']),
        ])
        ->bulkActions([
            DeleteBulkAction::make(),
        ]);
}

No controller. No Blade. The table is searchable, sortable, filterable, paginated, and bulk-actionable, in one method that I can read end to end in under a minute. The full surface is documented in the Filament Tables docs.

The value isn’t that you can’t write the same thing yourself. You can. The value is that you don’t have to think about how, and neither does the next person on the team. If you onboard someone with even a little Laravel background, they can ship a new admin screen in their first week. That used to take me a sprint.

The four patterns I actually use

Filament has a lot of surface. I’m going to be honest, I do not use most of it. These four are the ones I reach for on every project.

Resources, not pages

Filament lets you build custom pages from scratch. I almost never do that anymore. A Resource covers the four CRUD screens for a single Eloquent model and 90% of admin panels are CRUD-on-models, even when stakeholders insist they aren’t. If I find myself reaching for a custom page, I usually find out a week later that what I really wanted was a Resource with a custom action.

Form schemas as the source of truth

This is the one Filament idea that changed how I think about Laravel admin code. The form schema() array on a Resource describes every field, its validation, and its appearance in one place. There is no second source of truth in a Blade template. So when the client says “actually the slug should auto-generate from the title”, I add ->afterStateUpdated(...) on the title field and ship it.

Forms\Components\TextInput::make('title')
    ->required()
    ->live(debounce: 500)
    ->afterStateUpdated(fn (Set $set, ?string $state) =>
        $set('slug', Str::slug($state ?? ''))),
Forms\Components\TextInput::make('slug')
    ->required()
    ->unique(ignoreRecord: true),

Compare that to the old me, who would have spent ten minutes wiring up an Alpine.js component to mirror the title into the slug field.

Policies for everything, including row visibility

Filament respects Laravel’s Policy classes by default. That’s a big deal because it means I don’t have to write a parallel permissions system inside the admin. The viewAny, view, update, delete methods on a policy are exactly what the panel checks. I cover the wider Laravel 12 changes I kept around in my post on Laravel 12 in production, and policies were the single thing that got tighter when I moved my older clients onto Filament.

Notifications and toasts

Filament’s Notification facade looks small but I use it dozens of times per project. Any successful action gets a real toast instead of a session flash. Any background job result can ping the user with Notification::send($user, ...). The UI feels like a real app instead of a 2015 Laravel admin, and I didn’t write any of it.

Where I still drop down to raw Blade

Filament is not a replacement for your public-facing app. I keep two boundaries clear.

The first is anything that ships to end users. The marketing site, the customer-facing dashboard, the embedded report — those are still hand-written Blade or Livewire or Inertia, depending on the project. Filament’s UI is optimised for staff workflows, not pixel-perfect product surfaces.

The second is reports with weird shapes. If the client wants a single screen that summarises five tables, with a chart, and a CSV export, I usually build a Filament Page with a custom Blade view inside it. Filament gives me the chrome (nav, auth, layout) and I write the actual report by hand. Trying to express a one-off dashboard as a stack of widgets always ends in pain.

What about performance and customisation

This is the part of the post I’d expect to be skeptical of, so I’ll be honest about it. Filament is heavier than a bare Blade page. The Livewire round-trips for table interactions are not free. On a small admin with 50 rows per page, you won’t notice. On a 100k-row table with eager-loaded relations and a filter bar, you have to tune defaultPaginationPageOption(), add database indexes for your sort columns, and sometimes drop down to query() with a custom builder to keep things snappy.

I’ve had to do that exactly twice in two years. Both times the fix took an afternoon. Compared to the engineering time I would have spent re-implementing the same table from scratch, that’s a deal I take every time.

What I’d do this week if you’re on Laravel

If you maintain a Laravel admin that started life as a hand-rolled Blade panel, pick one model and rewrite its admin surface as a Filament Resource. Don’t try to migrate everything. The first resource takes a couple of hours, mostly because you’re learning Filament. The second takes thirty minutes. By the fifth, you’ll be deleting old controllers without ceremony.

If you’d rather have someone else do the rewrite while you focus on shipping product, that’s the kind of work I do for clients; you can see what I’ve been building lately on my site. Either way, the next admin you build probably doesn’t need to be hand-rolled. I wish I’d believed that in 2022.