Tutoriels

Filament PHP : construire un back-office en 2h

Filament PHP permet de construire un back-office Laravel complet en quelques heures. Voici comment j'ai créé l'admin de ce blog : articles, catégories, tags, dashboard, le tout sans écrire une ligne de JavaScript.

T
Thomas Bourcy
| | 9 min de lecture
Filament PHP : construire un back-office en 2h

Quand j'ai voulu créer le back-office de ce blog, j'avais deux options : passer une semaine à coder des CRUDs à la main, ou utiliser Filament. J'ai choisi Filament. Deux heures plus tard, j'avais un admin panel complet avec gestion des articles, catégories, tags, et un dashboard avec des stats.

Filament, c'est un framework Laravel pour construire des interfaces d'administration. Pas un générateur de code — un vrai framework avec des composants, des conventions, et une flexibilité impressionnante quand on en a besoin.

Voici comment construire un back-office de blog fonctionnel en partant de zéro.

Pourquoi Filament

Il existe d'autres options pour les admin panels Laravel : Nova (payant), Backpack (freemium), ou coder à la main. Filament se démarque pour plusieurs raisons :

Open source et gratuit — Pas de licence à payer, même pour un usage commercial.

Basé sur Livewire + Alpine.js — Réactivité sans écrire de JavaScript. Tout reste en PHP.

TALL Stack natif — Tailwind, Alpine, Livewire, Laravel. Si vous êtes déjà dans cet écosystème, vous êtes chez vous.

Extensible — Plugins, composants custom, theming. Vous n'êtes pas enfermé dans une boîte.

Documentation excellente — C'est rare, mais la doc Filament est vraiment bien faite.

Installation

On part d'un projet Laravel frais. Si vous en avez déjà un, passez à l'étape suivante.

composer create-project laravel/laravel blog-filament
cd blog-filament

Installez Filament :

composer require filament/filament:"^3.2"
php artisan filament:install --panels

Créez un utilisateur admin :

php artisan make:filament-user

C'est tout. Lancez le serveur et allez sur /admin :

php artisan serve

Vous avez un panel d'admin vide, prêt à être rempli.

Les modèles du blog

Avant de créer les ressources Filament, il nous faut les modèles Laravel. Voici la structure classique d'un blog :

php artisan make:model Post -m
php artisan make:model Category -m
php artisan make:model Tag -m

Migration Post

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('excerpt')->nullable();
    $table->longText('content');
    $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
    $table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
    $table->string('status')->default('draft');
    $table->boolean('is_featured')->default(false);
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
});

Migration Category

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->text('description')->nullable();
    $table->string('color')->default('#3B82F6');
    $table->integer('sort_order')->default(0);
    $table->timestamps();
});

Migration Tag + pivot

Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Schema::create('post_tag', function (Blueprint $table) {
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
    $table->primary(['post_id', 'tag_id']);
});

Lancez les migrations :

php artisan migrate

Créer les Resources Filament

Une "Resource" dans Filament, c'est tout ce qu'il faut pour gérer un modèle : liste, création, édition, suppression. Une commande :

php artisan make:filament-resource Post --generate
php artisan make:filament-resource Category --generate
php artisan make:filament-resource Tag --generate

Le flag --generate analyse vos migrations et génère automatiquement les champs du formulaire et les colonnes de la table. Magique.

Allez sur /admin — vous avez déjà trois sections fonctionnelles.

admin bog post

Personnaliser la Resource Post

Le code généré est fonctionnel mais basique. Voici comment l'améliorer.

Ouvrez app/Filament/Resources/PostResource.php :

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Tables;
use Filament\Tables\Table;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\Section::make('Contenu')
                ->schema([
                    Forms\Components\TextInput::make('title')
                        ->required()
                        ->live(onBlur: true)
                        ->afterStateUpdated(fn ($state, $set) => 
                            $set('slug', Str::slug($state))
                        ),
                    
                    Forms\Components\TextInput::make('slug')
                        ->required()
                        ->unique(ignoreRecord: true),
                    
                    Forms\Components\Textarea::make('excerpt')
                        ->rows(3)
                        ->columnSpanFull(),
                    
                    Forms\Components\RichEditor::make('content')
                        ->required()
                        ->columnSpanFull(),
                ])
                ->columns(2),
            
            Forms\Components\Section::make('Métadonnées')
                ->schema([
                    Forms\Components\Select::make('category_id')
                        ->relationship('category', 'name')
                        ->searchable()
                        ->preload()
                        ->createOptionForm([
                            Forms\Components\TextInput::make('name')->required(),
                            Forms\Components\TextInput::make('slug')->required(),
                        ]),
                    
                    Forms\Components\Select::make('tags')
                        ->relationship('tags', 'name')
                        ->multiple()
                        ->searchable()
                        ->preload()
                        ->createOptionForm([
                            Forms\Components\TextInput::make('name')->required(),
                            Forms\Components\TextInput::make('slug')->required(),
                        ]),
                    
                    Forms\Components\Select::make('status')
                        ->options([
                            'draft' => 'Brouillon',
                            'published' => 'Publié',
                        ])
                        ->default('draft')
                        ->required(),
                    
                    Forms\Components\Toggle::make('is_featured')
                        ->label('Article mis en avant'),
                    
                    Forms\Components\DateTimePicker::make('published_at')
                        ->label('Date de publication'),
                ])
                ->columns(2),
        ]);
}

Points clés

Génération automatique du slug — Le champ title utilise live(onBlur: true) pour mettre à jour le slug en temps réel.

Création inline — Les selects pour category et tags ont un createOptionForm qui permet de créer une nouvelle catégorie ou un nouveau tag sans quitter le formulaire.

Sections — Les Section organisent visuellement le formulaire. Bien plus lisible qu'une liste plate de champs.

Personnaliser la table

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('title')
                ->searchable()
                ->sortable()
                ->limit(50),
            
            Tables\Columns\TextColumn::make('category.name')
                ->badge()
                ->color(fn ($record) => Color::hex($record->category?->color ?? '#gray')),
            
            Tables\Columns\TextColumn::make('status')
                ->badge()
                ->color(fn (string $state): string => match ($state) {
                    'draft' => 'gray',
                    'published' => 'success',
                }),
            
            Tables\Columns\IconColumn::make('is_featured')
                ->boolean(),
            
            Tables\Columns\TextColumn::make('published_at')
                ->dateTime('d/m/Y H:i')
                ->sortable(),
        ])
        ->filters([
            Tables\Filters\SelectFilter::make('status')
                ->options([
                    'draft' => 'Brouillon',
                    'published' => 'Publié',
                ]),
            
            Tables\Filters\SelectFilter::make('category')
                ->relationship('category', 'name'),
            
            Tables\Filters\TernaryFilter::make('is_featured')
                ->label('Mis en avant'),
        ])
        ->actions([
            Tables\Actions\EditAction::make(),
            Tables\Actions\DeleteAction::make(),
        ])
        ->bulkActions([
            Tables\Actions\BulkActionGroup::make([
                Tables\Actions\DeleteBulkAction::make(),
            ]),
        ])
        ->defaultSort('created_at', 'desc');
}

En quelques lignes : recherche, tri, filtres, badges colorés, actions. Tout est déclaratif.

Ajouter un Dashboard

Le dashboard par défaut est vide. Ajoutons des widgets utiles.

Créez un widget :

php artisan make:filament-widget StatsOverview --stats-overview

Éditez app/Filament/Widgets/StatsOverview.php :

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use App\Models\Post;

class StatsOverview extends BaseWidget
{
    protected function getStats(): array
    {
        return [
            Stat::make('Articles publiés', Post::where('status', 'published')->count())
                ->description('Total des articles en ligne')
                ->color('success'),
            
            Stat::make('Brouillons', Post::where('status', 'draft')->count())
                ->description('En attente de publication')
                ->color('warning'),
            
            Stat::make('Articles ce mois', Post::whereMonth('published_at', now()->month)->count())
                ->description('Publications du mois en cours')
                ->color('info'),
        ];
    }
}

Le widget apparaît automatiquement sur le dashboard.

admin graph

Prompt : "A dashboard with three statistics cards showing numbers and trends, plus a chart widget below, modern admin interface, clean white background with subtle shadows, purple accent color, professional design, 16:9"

Bonus : Actions personnalisées

Ajoutons un bouton "Publier" qui change le statut en un clic :

Tables\Actions\Action::make('publish')
    ->label('Publier')
    ->icon('heroicon-o-check-circle')
    ->color('success')
    ->visible(fn ($record) => $record->status === 'draft')
    ->requiresConfirmation()
    ->action(fn ($record) => $record->update([
        'status' => 'published',
        'published_at' => now(),
    ]))

Ajoutez ça dans le tableau actions de votre table. Un bouton "Publier" apparaît sur chaque brouillon.

Ce qui m'a pris 2 heures

Pour être précis, voici ce que j'ai construit en 2 heures pour ce blog :

  • 3 Resources complètes (Posts, Categories, Tags)
  • Formulaires avec RichEditor, relations, création inline
  • Tables avec recherche, filtres, tri, pagination
  • Dashboard avec 4 widgets de stats
  • Actions custom (publier, dépublier, mettre en avant)
  • Soft deletes avec corbeille

Ce qui aurait pris une journée à coder from scratch.

Les limites

Filament n'est pas parfait :

Courbe d'apprentissage — La doc est bonne, mais il y a beaucoup de concepts. Les premières heures sont un peu déroutantes.

Performance — Livewire ajoute de l'overhead. Pour un admin panel classique, c'est invisible. Pour des tables avec 100k lignes et des calculs complexes, ça peut se sentir.

Personnalisation poussée — Tant que vous restez dans les conventions, tout est fluide. Dès que vous voulez un comportement très custom, il faut comprendre comment Livewire et les composants Filament fonctionnent en interne.

Mises à jour — Filament évolue vite. Les breaking changes entre versions majeures peuvent demander du travail de migration.

Verdict

Pour un back-office standard (CRUD, relations, dashboard), Filament est imbattable. Le ratio temps investi / résultat est exceptionnel.

Je l'utilise maintenant pour tous mes projets Laravel qui ont besoin d'une interface d'admin. Ce blog, des outils internes, des MVPs clients. À chaque fois, je gagne des jours de développement.

Si vous êtes sur Laravel et que vous codez encore vos admins à la main, essayez Filament sur un petit projet. Vous ne reviendrez probablement pas en arrière.

Vous utilisez Filament ?


Ressources

Partager :

Articles similaires

MCP : le protocole qui connecte l'IA au monde réel
Tutoriels 8 min de lecture

MCP : le protocole qui connecte l'IA au monde réel

Le Model Context Protocol (MCP) est devenu le standard universel pour connecter les IA à vos outils. Adopté par Anthropic, OpenAI, Google et Microsoft, il change la façon dont on construit des applications IA. Guide complet et pratique.

T
Thomas Bourcy

Vous avez un projet ?

Discutons de votre projet et voyons comment je peux vous aider à le concrétiser.

Me contacter