For completeness, here is the tutorial for creating a user resource in Filament 4:
Tutorial: Creating a User Resource in Filament 4
This tutorial walks you through creating a comprehensive User Resource in Filament 4 to manage users in your admin panel, including viewing, creating, editing, and deleting users with custom fields.
Overview
We'll create a User Resource that provides a full CRUD (Create, Read, Update, Delete) interface for managing users, including:
- A table view with searchable and sortable columns
- A form with organized sections for user information and authentication
- Proper password handling (required on create, optional on edit)
- Custom fields (phone, company) from your registration form
- Navigation integration
Prerequisites
- Laravel 12.x
- Filament 4.x installed
- A User model with custom fields (phone, company) already migrated
- A Filament panel already configured
Step-by-Step Implementation
Step 1: Generate the User Resource
Use Filament's artisan command to generate a complete resource with all necessary files:
php artisan filament:make-resource User --generate
This command will create:
app/Filament/Resources/Users/UserResource.php - Main resource class
app/Filament/Resources/Users/Tables/UsersTable.php - Table configuration
app/Filament/Resources/Users/Schemas/UserForm.php - Form configuration
app/Filament/Resources/Users/Pages/ListUsers.php - List page
app/Filament/Resources/Users/Pages/CreateUser.php - Create page
app/Filament/Resources/Users/Pages/EditUser.php - Edit page
The --generate flag automatically generates the table, form, and page classes.
Step 2: Configure the Main Resource Class
Edit app/Filament/Resources/Users/UserResource.php to customize navigation and labels:
<?php
namespace App\Filament\Resources\Users;
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Filament\Resources\Users\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable;
use App\Models\User;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationLabel = 'Users';
protected static ?string $modelLabel = 'User';
protected static ?string $pluralModelLabel = 'Users';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup;
public static function form(Schema $schema): Schema
{
return UserForm::configure($schema);
}
public static function table(Table $table): Table
{
return UsersTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
];
}
}
Key Points:
$model - Links the resource to your User model
$navigationLabel - Label shown in the sidebar navigation
$modelLabel / $pluralModelLabel - Labels used in page titles and actions
$navigationIcon - Icon displayed in navigation (Heroicon::OutlinedUserGroup)
form() - Delegates form configuration to UserForm class
table() - Delegates table configuration to UsersTable class
getPages() - Defines the routes for list, create, and edit pages
Step 3: Configure the Table View
Edit app/Filament/Resources/Users/Tables/UsersTable.php to customize the columns displayed in the list view:
<?php
namespace App\Filament\Resources\Users\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
->label('Email')
->searchable()
->sortable()
->copyable(),
TextColumn::make('phone')
->label('Phone')
->searchable()
->toggleable(),
TextColumn::make('company')
->label('Company')
->searchable()
->toggleable(),
TextColumn::make('email_verified_at')
->label('Verified')
->dateTime()
->sortable()
->toggleable(),
TextColumn::make('created_at')
->label('Created')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label('Updated')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
Column Features:
searchable() - Enables search functionality for the column
sortable() - Allows sorting by clicking the column header
copyable() - Adds a copy button to quickly copy the value
toggleable() - Allows users to show/hide the column
toggleable(isToggledHiddenByDefault: true) - Column is hidden by default
label() - Custom label for the column header
dateTime() - Formats the value as a date/time
Table Actions:
recordActions() - Actions available for each row (Edit button)
toolbarActions() - Bulk actions available in the toolbar (Delete multiple)
Step 4: Configure the Form
Edit app/Filament/Resources/Users/Schemas/UserForm.php to create an organized form with sections:
<?php
namespace App\Filament\Resources\Users\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Password;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('User Information')
->schema([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('email')
->label('Email address')
->email()
->required()
->maxLength(255)
->unique(ignoreRecord: true),
TextInput::make('phone')
->label('Phone Number')
->tel()
->maxLength(20)
->unique(ignoreRecord: true),
TextInput::make('company')
->label('Company/Organization')
->maxLength(255),
])
->columns(2),
Section::make('Authentication')
->schema([
TextInput::make('password')
->password()
->label(fn ($livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser ? 'Password' : 'New Password (leave blank to keep current)')
->required(fn ($livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser)
->rule(Password::default())
->dehydrated(fn ($state, $livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser ? true : filled($state))
->dehydrateStateUsing(fn ($state) => filled($state) ? \Illuminate\Support\Facades\Hash::make($state) : null),
DateTimePicker::make('email_verified_at')
->label('Email Verified At'),
])
->columns(2),
]);
}
}
Key Features:
-
Sections: Organize form fields into logical groups
Section::make('Title') - Creates a collapsible section
->columns(2) - Displays fields in a 2-column layout
-
Field Validation:
->required() - Makes the field mandatory
->email() - Validates email format
->tel() - Optimizes input for telephone numbers
->maxLength(255) - Sets maximum character length
->unique(ignoreRecord: true) - Ensures uniqueness, ignoring current record on edit
-
Password Handling:
->required(fn ($livewire) => ...) - Required only when creating (not editing)
->dehydrated(...) - Only saves password if provided (on edit) or always (on create)
->dehydrateStateUsing(...) - Automatically hashes the password before saving
- Dynamic label changes based on create vs edit context
-
Important Note on Namespaces:
Section is in Filament\Schemas\Components\Section (not Forms namespace)
TextInput and DateTimePicker are in Filament\Forms\Components
Step 5: Customize Page Classes (Optional)
The page classes are automatically generated and work out of the box. You can customize them if needed:
CreateUser.php:
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}
EditUser.php:
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
The EditUser class includes a delete action in the header, allowing you to delete users from the edit page.
Understanding the Structure
Resource Organization
Filament 4 uses a modular structure for resources:
app/Filament/Resources/Users/
├── UserResource.php # Main resource class
├── Tables/
│ └── UsersTable.php # Table configuration
├── Schemas/
│ └── UserForm.php # Form configuration
└── Pages/
├── ListUsers.php # List page
├── CreateUser.php # Create page
└── EditUser.php # Edit page
This separation allows for:
- Better code organization
- Reusability of table and form configurations
- Easier maintenance and testing
Auto-Discovery
Filament automatically discovers resources in app/Filament/Resources and adds them to navigation. No manual registration needed!
Customization Options
Adding More Columns to Table
Add additional columns to display more information:
TextColumn::make('phone')
->label('Phone')
->searchable()
->toggleable()
->formatStateUsing(fn ($state) => $state ?: 'N/A'), // Show "N/A" if empty
Adding Filters
Add filters to help users find specific records:
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\Filter;
// In UsersTable.php ->filters([])
->filters([
SelectFilter::make('email_verified_at')
->label('Email Status')
->options([
'verified' => 'Verified',
'unverified' => 'Unverified',
])
->query(function ($query, $state) {
if ($state['value'] === 'verified') {
return $query->whereNotNull('email_verified_at');
}
if ($state['value'] === 'unverified') {
return $query->whereNull('email_verified_at');
}
}),
])
Adding Bulk Actions
Add more bulk actions for managing multiple users:
use Filament\Actions\BulkAction;
->toolbarActions([
BulkActionGroup::make([
BulkAction::make('verify_emails')
->label('Verify Selected Emails')
->icon('heroicon-o-check-circle')
->action(function ($records) {
$records->each(function ($user) {
$user->markEmailAsVerified();
});
})
->requiresConfirmation(),
DeleteBulkAction::make(),
]),
])
Customizing Form Sections
Add more sections or reorganize fields:
Section::make('Contact Information')
->schema([
TextInput::make('phone'),
TextInput::make('company'),
])
->collapsible(), // Makes section collapsible
Adding Custom Actions
Add custom actions to the table or form:
// In UsersTable.php
->recordActions([
EditAction::make(),
Action::make('impersonate')
->label('Impersonate')
->icon('heroicon-o-user')
->action(function ($record) {
// Impersonation logic
}),
])
Customizing Navigation
Control how the resource appears in navigation:
// In UserResource.php
protected static ?int $navigationSort = 1; // Position in sidebar
protected static ?string $navigationGroup = 'User Management'; // Group resources
protected static bool $shouldRegisterNavigation = true; // Show/hide in nav
Testing
- Navigate to
/admin in your browser
- Look for "Users" in the sidebar navigation
- Click it to see the list of users
- Test the following:
- List View: Verify all columns display correctly
- Search: Try searching by name, email, phone, or company
- Sorting: Click column headers to sort
- Create: Click "New User" and fill out the form
- Edit: Click the edit icon on a user row
- Delete: Delete a user from the edit page
- Password: Verify password is required on create, optional on edit
Troubleshooting
Resource Not Appearing in Navigation
- Clear Filament cache:
php artisan filament:optimize-clear
- Clear config cache:
php artisan config:clear
- Verify the resource is in
app/Filament/Resources directory
- Check that
$shouldRegisterNavigation is not set to false
Form Fields Not Saving
- Verify fields are in the User model's
$fillable array
- Check that database columns exist (run migrations)
- Ensure
dehydrated() is not set to false unless intentional
Password Issues
- Password field should use
->dehydrateStateUsing() to hash the password
- On edit, password should only be dehydrated if a value is provided
- Use
->required(fn ($livewire) => ...) to make it conditional
Namespace Errors
Section is in Filament\Schemas\Components\Section
- Form components (
TextInput, DateTimePicker) are in Filament\Forms\Components
- Table columns are in
Filament\Tables\Columns
Summary
You've successfully created a comprehensive User Resource in Filament 4 with:
- ✅ Full CRUD functionality (Create, Read, Update, Delete)
- ✅ Organized form with sections for user information and authentication
- ✅ Searchable and sortable table columns
- ✅ Custom fields (phone, company) integrated
- ✅ Smart password handling (required on create, optional on edit)
- ✅ Automatic password hashing
- ✅ Navigation integration
- ✅ Bulk actions for managing multiple users
- ✅ Professional UI with toggleable columns
The User Resource is now available in your Filament admin panel and automatically appears in the navigation sidebar. Users registered through your custom registration page will be visible and manageable through this interface.