Building Web Apps with Laravel Module 16 – Product features: languages, search, admin and payments
Admin panels with Filament
Build a Laravel 13 admin panel with Filament 5 resources, tables and filters, let in only the right staff, and compare Filament with Nova and your own pages.
What you will learn
- Build a Filament 5 panel with a resource, a table and a filter for a model
- Protect the panel, its records and its bulk actions with canAccessPanel, policies and tenancy
- Test a dashboard widget's numbers and limit the queries its polling adds
- Choose between Filament, Laravel Nova and admin pages of your own for a project
Before you start
What you’ll learn · Before you start
What you will learn
- Build a Filament 5 panel with a resource, a table and a filter for a model
- Protect the panel, its records and its bulk actions with canAccessPanel, policies and tenancy
- Test a dashboard widget's numbers and limit the queries its polling adds
- Choose between Filament, Laravel Nova and admin pages of your own for a project
Before you start
On this page
Pathshala’s accountant manages 1,200 students across two centres, and every list, form and filter she needs would be a controller, a view and a test written by hand. An admin panel package generates them from the model. With Filament 5.10.2, two commands gave Pathshala a login page, a students table, create and edit forms and six routes.
This lesson builds that panel, then makes it safe: only office staff get in, each action follows the model’s policy, each centre sees its own students, and the dashboard’s numbers match the database. It ends with how to choose between Filament, Laravel’s own Nova and admin pages of your own.
Two commands make a panel and a resource
php artisan filament:install --panels writes a panel provider: A service provider class, such as AdminPanelProvider, that configures one Filament panel: its path, login, colours, middleware and the folders where its resources, pages and widgets are found., AdminPanelProvider, and registers it in bootstrap/providers.php. php artisan make:filament-resource Student --generate then writes a Filament resource: A class that connects a panel to one Eloquent model, with a form schema for create and edit pages, a table class for the list page, and the pages and routes for both.: one class that names the model, plus a form schema, a table class and three pages. The run starts from Pathshala with filament/filament installed:
"""A Filament 5.10.2 admin panel for Pathshala: the project with filament/filament installed (composer require -W
filament/filament:^5.10, with PHP's zip extension), the centres and students tables migrated, then
filament:install --panels, the provider it registers, make:filament-resource Student --generate, the files it wrote,
the table class it generated from the columns, and the routes of the panel. Files: the folder admin-base/ next to
this file."""
from session import run
run(
"""
php artisan migrate --force
php artisan filament:install --panels --no-interaction --quiet
cat bootstrap/providers.php
php artisan make:filament-resource Student --generate --no-interaction --quiet
find app/Filament -type f | sort
cat app/Filament/Resources/Students/Tables/StudentsTable.php
php artisan route:list --path=admin
""",
app=True,
overlay=("admin-base",),
fixture="pathshala-filament",
php_ini=("extension=zip",),
) Output
$ php artisan migrate --force
INFO Running migrations.
2031_01_01_000002_create_centres_and_students_tables .................. DONE
$ php artisan filament:install --panels --no-interaction --quiet
$ cat bootstrap/providers.php
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\Filament\AdminPanelProvider::class,
];
$ php artisan make:filament-resource Student --generate --no-interaction --quiet
$ find app/Filament -type f | sort
app/Filament/Resources/Students/Pages/CreateStudent.php
app/Filament/Resources/Students/Pages/EditStudent.php
app/Filament/Resources/Students/Pages/ListStudents.php
app/Filament/Resources/Students/Schemas/StudentForm.php
app/Filament/Resources/Students/StudentResource.php
app/Filament/Resources/Students/Tables/StudentsTable.php
$ cat app/Filament/Resources/Students/Tables/StudentsTable.php
<?php
namespace App\Filament\Resources\Students\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class StudentsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('centre.name')
->searchable(),
TextColumn::make('name')
->searchable(),
TextColumn::make('admission_no')
->searchable(),
TextColumn::make('fee_status')
->searchable(),
TextColumn::make('dues_paise')
->numeric()
->sortable(),
TextColumn::make('guardian_phone')
->searchable(),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
$ php artisan route:list --path=admin
GET|HEAD admin filament.admin.pages.dashboard › Filament\Pages › Dashboard
GET|HEAD admin/login ... filament.admin.auth.login › Filament\Auth › Login
POST admin/logout filament.admin.auth.logout › Filament\Auth › Logout…
GET|HEAD admin/students filament.admin.resources.students.index › App\Fil…
GET|HEAD admin/students/create filament.admin.resources.students.create …
GET|HEAD admin/students/{record}/edit filament.admin.resources.students.e…
Showing [6] routes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 panel_session.py
The resource got six files and the panel six routes: the dashboard, login, logout and the students’ list, create and edit pages. --generate read the students table and put every column into the table class, guardian_phone included. Generated code is a first draft: Pathshala’s finished table drops the phone, shows the dues in rupees and adds a filter by fee status. The three parts split the work: the form schema lists the fields of the create and edit pages, the table class lists columns, filters and actions, and the pages give the routes.
admin-panel/app/Filament/Resources/Students/Tables/StudentsTable.php
<?php
namespace App\Filament\Resources\Students\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Number;
class StudentsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('admission_no')->label('Admission no.')->searchable(),
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('centre.name')->label('Centre'),
TextColumn::make('fee_status')->badge(),
TextColumn::make('dues_paise')->label('Dues')
->formatStateUsing(fn (int $state): string => Number::currency($state / 100, in: 'INR', locale: 'en_IN'))
->sortable(),
])
->filters([
SelectFilter::make('fee_status')
->options(['paid' => 'Paid', 'due' => 'Due', 'overdue' => 'Overdue']),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
} admin-panel/app/Filament/Resources/Students/Schemas/StudentForm.php
<?php
namespace App\Filament\Resources\Students\Schemas;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class StudentForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Select::make('centre_id')->relationship('centre', 'name')->required(),
TextInput::make('name')->required()->maxLength(120),
TextInput::make('admission_no')->required()->unique(ignoreRecord: true),
Select::make('fee_status')
->options(['paid' => 'Paid', 'due' => 'Due', 'overdue' => 'Overdue'])
->required(),
TextInput::make('dues_paise')->label('Dues in paise')->numeric()->minValue(0)->default(0)->required(),
TextInput::make('guardian_phone')->tel()->required(),
]);
}
} Checkpoint
Check: Panels, resources and what --generate writes
The panel lets in only the users the User model allows
A panel is a second application inside yours, so the first question is who may open it. Follow one request through the checks:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The panel has its own routes. filament:install registered AdminPanelProvider, whose routes start with /admin and run their own list of middleware, sessions and CSRF protection included.
- A guest goes to the login page. The Authenticate middleware of the panel finds nobody signed in and redirects to /admin/login, the first case of the recorded tests.
- Every user is refused at first. The User model does not implement FilamentUser yet. Outside the local environment the middleware then answers 403 for every signed-in user, which is what the testing run showed for all three staff.
- canAccessPanel() decides. Once User implements FilamentUser, the middleware asks canAccessPanel(). It returns true for the accountant and the admin, and false for the teacher, who still gets 403.
- The policy comes next. Inside the panel each resource asks its model's policy, starting with viewAny() for the list. Without a policy, Filament allows every action to anyone who passed the panel check.
- The accountant sees the table. The list page rendered all six students with their dues in Indian digit grouping, and the fee filter kept the three overdue ones.
The tests below run with APP_ENV=testing. Before the change, the User model is the skeleton’s, and every signed-in member of staff was refused. Filament’s middleware allows a User that does not implement FilamentUser only in the local environment, so a panel that worked on a laptop answers 403 in production. Implementing the contract’s canAccessPanel() decides it instead:
"""Who may open Pathshala's Filament panel: the panel and the Student resource (finished table with a fee filter),
seeded with two centres, six students and three staff, then two test classes: DefaultAccessTest with the User model
as the skeleton has it, and PanelAccessTest after User implements FilamentUser with canAccessPanel().
Files: the folders admin-base/, admin-panel/ and access/ next to this file."""
from session import run
run(
"""
export COLLISION_IGNORE_DURATION=true
php artisan test --filter=DefaultAccessTest
cp stubs/User.php app/Models/User.php
php artisan test --filter=PanelAccessTest
""",
app=True,
overlay=("admin-base", "admin-panel", "access"),
fixture="pathshala-filament",
php_ini=("extension=zip",),
) Output
$ export COLLISION_IGNORE_DURATION=true $ php artisan test --filter=DefaultAccessTest PASS Tests\Feature\DefaultAccessTest ✓ a guest is sent to the panel login ✓ outside local every signed in user is refused Tests: 2 passed (6 assertions) Duration: …s $ cp stubs/User.php app/Models/User.php $ php artisan test --filter=PanelAccessTest PASS Tests\Feature\PanelAccessTest ✓ a teacher is refused ✓ the accountant sees the students and their dues ✓ the overdue filter keeps three of six students Tests: 3 passed (13 assertions) Duration: …s
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 access_session.py
After canAccessPanel() returned true for the accountant and the admin, the teacher still got 403, while the accountant saw six students and ₹4,500.00 of dues for Asha Verma. The Livewire test drove the table itself: filterTable('fee_status', 'overdue') left three students.
Key idea
The panel trusts the User model: canAccessPanel() decides who gets in, and every resource then asks the model’s policy what each person may do.
Checkpoint
Check: Who may open the panel
Filament, Nova or admin pages of your own
Three ways can serve Pathshala’s office. Filament is an open-source package under the MIT licence, built on Livewire, Alpine.js and Tailwind CSS by its own team, outside Laravel. Laravel Nova is Laravel’s own commercial admin panel, which needs a licence key and a production URL to run in production. Admin pages of your own are Blade, Livewire or Inertia pages, often started from one of Laravel’s starter kits, with every screen written by you.
| Filament | Laravel Nova | Pages of your own | |
|---|---|---|---|
| Made by | Filament’s own team | Laravel | your team |
| Licence | MIT | commercial | yours |
| Screens | generated from models | generated from models | each one written |
| Upgrades | each major changes code | its own majors | none forced |
Filament fits an office that needs many tables and forms soon. Pages of your own fit a few screens that must look like the rest of the product. Nova fits a team already licensed for it. Whichever you pick, pin its major version in composer.json, because each Filament major has asked existing panels for changes:
- 3.0
Livewire 3 and PHP 8.1 PHP 8.1
Released in 2023, Filament 3 requires Livewire 3 and PHP 8.1, and supports Laravel 9 and 10.
- 4.0
Schemas arrive PHP 8.2
Released in 2025, Filament 4 requires PHP 8.2 and Livewire 3.5, and adds the filament/schemas package. A resource's form is a Schema, as the generated StudentForm class of this lesson shows.
- 5.0
Livewire 4 Livewire 4
Released in 2026, Filament 5 requires Livewire 4 and Tailwind CSS 4, and has its own upgrade guide. This lesson runs Filament 5.10.2 with Livewire 4.4.7 on Laravel 13.34.
The timeline has 3 events, from 3.0 to 5.0.
A constraint such as "filament/filament": "^5.10" accepts every 5.x release from 5.10 on and keeps 6.0 out until you upgrade on purpose, with the upgrade guide open.
Checkpoint
Check: Filament, Nova or pages of your own
Policies on every row, and bulk actions that ask about each record Pro: opens with a Pro, Premium or Ultimate pass
Policies decide which buttons each member of staff sees, but the generated bulk delete asks the policy a different question. This part tests both with Pathshala’s admin and accountant, and reproduces a bulk delete that removed two students with unpaid fees.
In this part:
- Who sees which action, tested for two members of staff
- A bulk delete asks deleteAny() once
- authorizeIndividualRecords(‘delete’) asks about every row
- Custom bulk actions, and the query that skips the check
2 runnable examples (BulkDeleteDefaultTest.php, PolicyTest.php, Python, StudentPolicy.php) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
One panel for many centres, and the queries tenancy does not cover Pro: opens with a Pro, Premium or Ultimate pass
Pathshala’s Andheri accountant should never see Thane’s students. Filament’s Running one application for several separate groups, here Pathshala's centres, so that each group sees and changes only its own records. puts the centre into every URL and scopes the resource’s queries, but only some queries. This part measures which.
In this part:
- The centre in every URL
- What the tenant scope covers, measured
- Jobs, reports and caches carry the centre explicitly
2 runnable examples (AdminPanelProvider.php, Python, TenancyTest.php, User.php) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Dashboard numbers you can test, and what polling costs Pro: opens with a Pro, Premium or Ultimate pass
A dashboard number that nobody tested is a number nobody trusts. This part tests a stats widget per centre, counts the queries one render runs, works out what Filament’s default polling adds, and ends with a rubric for reviewing a panel.
In this part:
- A stats widget that follows the centre
- Every open dashboard asks again every five seconds
- A rubric for reviewing an admin panel
2 runnable examples (FeeStats.php, Python, WidgetTest.php) · about 4 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise
In your own Pathshala project, install Filament with --panels, generate a Student resource with --generate, and remove every column that the office does not need. Then implement FilamentUser with a role check, and write a Livewire test that filters the table by fee status. These questions check the three opening sections:
Practice that adapts to you
Practise this lesson
9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: why does a new Filament panel work on your laptop but answer 403 in production?
Filament’s authentication middleware lets a signed-in User that does not implement FilamentUser into the panel only when the app’s environment is local. Anywhere else it answers 403, which the recorded tests showed for all three staff with APP_ENV=testing. Implement Filament\Models\Contracts\FilamentUser on the User model and return true from canAccessPanel() only for the roles that need the panel, such as an accountant or an admin.
The module’s bank keeps three panel questions for longer answers:
-
Mid to senior
Pathshala's StudentPolicy refuses to delete a student with unpaid fees, and the row's delete button is hidden for such students. Yet an admin deleted twelve students with dues from the Filament panel in one go. Explain how that happened, the fix, and the test that keeps it fixed.
-
Senior to staff
Pathshala's panel uses Filament tenancy for its centres, and every test of the students table passes. Still, the Andheri dashboard shows Thane's overdue count, and Thane's guardians received Andheri's fee reminders. Find the likely causes and fix them so that they stay fixed.
-
Mid to senior
A startup's lead asks you to choose how to build its back office: Filament, Laravel Nova, or admin pages of its own on a Laravel starter kit. The office needs about thirty screens within a quarter, and the team of four knows Livewire. Make the choice and defend it, including what it costs over the next two years.
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which two commands give a Laravel app a Filament admin panel with a students resource?
php artisan filament:install --panels writes AdminPanelProvider and registers it; php artisan make:filament-resource Student --generate writes a resource with a form schema, a table class and three pages. The panel got six routes.
-
Why is the code that make:filament-resource --generate writes only a first draft?
It read the students table and put every column into the table class, the guardian's phone number included. Pathshala's finished table drops the phone, shows dues in rupees and adds a filter by fee status.
-
Why did every member of staff get 403 from the Filament panel outside the local environment?
Filament allows a User that does not implement FilamentUser only in the local environment. Implementing its canAccessPanel() decides who gets in; every resource then asks the model's policy what each person may do.
-
How do Filament, Laravel Nova and admin pages of your own differ?
Filament is an MIT-licensed package built on Livewire, Alpine.js and Tailwind by its own team, generating screens from models. Nova is Laravel's commercial panel, needing a licence key in production. Pages of your own are Blade, Livewire or Inertia screens you write.
-
Why pin Filament with a constraint such as ^5.10?
Each Filament major has asked existing panels for changes. ^5.10 accepts every 5.x release from 5.10 on and keeps 6.0 out until you upgrade on purpose, with the upgrade guide open.
Key takeaways
filament:install --panelsadds a panel provider, andmake:filament-resource --generatewrites a resource with a form, a table and three pages from the model’s columns, which you then review.- A User that does not implement
FilamentUsergets into the panel only locally, socanAccessPanel()is what lets the right staff in elsewhere. - Filament is an open-source package built on Livewire, Nova is Laravel’s commercial panel, and pages of your own trade generated screens for full control; pin the major you choose.
Terms in this lesson
- Filament resource
- A class that connects a panel to one Eloquent model, with a form schema for create and edit pages, a table class for the list page, and the pages and routes for both.
- panel provider
- A service provider class, such as AdminPanelProvider, that configures one Filament panel: its path, login, colours, middleware and the folders where its resources, pages and widgets are found.
References
- Filament 5.x overview (Filament)
- Filament 5.x installation (Filament)
- Filament 5.x users, authorizing access to the panel (Filament)
- Filament 5.x resources, authorization (Filament)
- Filament 5.x multi-tenancy (Filament)
- Filament 5.x testing tables (Filament)
- Filament 5.x testing actions (Filament)
- Filament 5.10.2 source, CanBeDisabled (actions) (Filament (GitHub))
- Filament 5.x stats overview widgets (Filament)
- Filament 5.x upgrade guide (Filament)
- Filament licence (MIT) (Filament (GitHub))
- filament/filament on Packagist (Packagist)
- Laravel Nova installation (Laravel (nova.laravel.com))
- Laravel 13.x starter kits (Laravel (laravel.com))
- Laravel 13.x authorization (Laravel (laravel.com))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress