Il existe un certain nombre de packages pour créer une administration pour Laravel dont l'officiel Nova (qui est payant). J'ai déjà parlé dans ce blog de Voyager, Orchid, Infyom et aussi récemment Filament dans sa version 3. Mais depuis est sorti la version 4 et il est souhaitable de rafraîchir tout ça. Je dois avouer que je me sens souvent gêné par ces outils qui sont remarquables, mais qui obligent à jongler dans les situations particulières. C'est pour cette raison que dans les exemples pratiques que je donne, je construis toujours cette partie administration pour en conserver la totale maîtrise.
Filament nous promet de pouvoir construire rapidement de magnifiques interfaces d'administration. Le package bénéficie d'une sérieuse documentation. Il est construit avec les technologies à la mode : la TALL stack. Un acronyme qui résume les technologies utilisées : Tailwind, Alpine, Laravel et Livewire.
Vous pouvez télécharger le code final de cet article ici.
Filament est le package Laravel le plus populaire :

Installation
Pour démarrer, on crée une nouvelle application Laravel :
laravel new filament4
Lors des questions posées, n'utilisez pas de starter kit et choisissez n'importe quel outil de test. Ensuite, pour la base de données, c'est comme vous voulez, puis acceptez la création des assets. Si tout se passe bien, vous obtenez la page d'accueil classique :

Puis, on installe Filament :
composer require filament/filament:"^4.0"
php artisan filament:install --panels
On se retrouve avec un nouveau provider :

Ensuite, il faut créer un compte administrateur :
php artisan make:filament-user
Là, on répond aux questions et le compte est créé. Avec l'URL .../admin/login, on arrive sur la page de connexion :

On utilise les données de l'administrateur qu'on a créé et on arrive sur le tableau de bord :

Évidemment tout est en anglais, mais il a été ajouté un certain nombre de traductions dont le français (avec la version 3 il fallait les charger) Il nous suffit de changer la locale dans la configuration (.env) :
APP_LOCALE=fr
On en profite pour ajouter les fichiers des langues (ça nous servira plus tard) :
composer require laravel-lang/common
php artisan lang:update
Et c'est bon :
Les possibilités
Filament est devenu très riche en possibilités. Il n'est que de consulter la belle documentation pour le constater. Celle-ci se décompose en grands chapitres :

Par rapport à la première version que j'avais analysée il y a quelques années, bien du chemin a été parcouru !
Des auteurs
On va créer une migration pour une table d'auteurs avec le modèle associé et le factory :
php artisan make:model Author -mf
Dans la migration, on prévoit ce code :
public function up()
{
Schema::create('authors', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->date('birth');
$table->timestamps();
});
}
On a donc :
- le nom
- la date de naissance
- les dates de création et de modification classiques
On renseigne le factory en restant réaliste par rapport aux dates de naissance :
public function definition()
{
// Détermine la date limite supérieure : il y a 20 ans
$endDate = '20 years ago';
// Détermine la date limite inférieure : par exemple, il y a 80 ans
$startDate = '80 years ago';
return [
'name' => $this->faker->name,
// Génère une date de naissance entre 80 ans et 20 ans dans le passé
'birth' => $this->faker->dateTimeBetween($startDate, $endDate)->format('Y-m-d'),
];
}
On utilise le factory dans DatabaseSeeder pour créer 10 auteurs :
...
use App\Models\Author;
class DatabaseSeeder extends Seeder
{
...
public function run()
{
Author::factory()->count(10)->create();
}
On ajoute la propriété $fillable dans le modèle Author :
protected $fillable = [
'name',
'birth',
];
Il ne reste plus qu'à lancer :
php artisan migrate --seed
On a nos 10 auteurs :

Une ressource pour les auteurs

Filament utilise la notion de ressource définie comme un ensemble de classes statiques qui décrivent comment un administrateur interagit avec les données de la base. Globalement, on obtient une interface CRUD (Create, Read, Update et Delete). On va créer une ressource pour les auteurs :
php artisan make:filament-resource Author
Répondez oui à cette question :
Should the configuration be generated from the current database columns? (yes/no) [no]
On a la création de plusieurs dossiers et fichiers :

La ressource proprement dite réside dans la classe AuthorResource. Cette classe permet de définir les tableaux, les formulaires, les autorisations, les pages, les autorisations, les actions....
Dans le dossier Pages, vous avez des composants Livewire qui vous permettent de modifier les pages d'interaction avec la ressource.
Dans le dossier Schemas, vous pouvez modifier les formulaires et les listes.
Dans le dossier Tables, vous avez accès aux tableaux.
Il est possible de grouper les créations, mais ça me paraît moins clair à l'usage :
php artisan make:filament-resource Author --model --migration --factory
Sur le tableau de bord, on voit déjà le menu complété :

L'icône
On va quand changer l'icône. Filament a comme dépendance le package blade-heroicons. On a donc un peu de choix. À la création, on a cette icône :
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
On la remplace avec celle du user :
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUser;
Et ça marche :

Si vous vous demandez où sont déclarées ces icônes, ça se passe dans une énumération ici : Filament\Support\Icons.
Le titre
On préfèrerait aussi le titre en français, on surcharge cette propriété dans la ressource :
protected static ?string $navigationLabel = 'Auteurs';
Et maintenant, c'est mieux :

Mais en procédant ainsi, on perd l'internationalisation. Il vaut mieux surcharger la méthode correspondante :
public static function getNavigationLabel(): string
{
return __('Authors');
}
On ajoute la traduction dans fr.json :
"Authors": "Auteurs"
Et c'est bon !
Le tableau

Le tableau est déjà renseigné :

Là aussi, j'aimerais avoir le titre en français, on peut à nouveau jouer dans la ressource :
public static function getModelLabel(): string
{
return __('Authors');
}

Le tableau est défini dans la classe AuthorsTable :
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable(),
TextColumn::make('birth')
->date()
->sortable(),
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(),
]),
]);
}
On voit qu'on peut ajouter des colonnes, définir des filtres et des actions. Tout ça est déjà rempli par défaut !
Concernant les colonnes, Filament propose de nombreuses possibilités : texte, image, icône, couleur... Et on peut même prévoir un select, un input, un checkbox...
Dans notre cas, on a seulement des éléments simples de texte.
On peut trier (sortable) sur les dates de naissance et les dates. Si on veut que ça fonctionne aussi pour les noms, c'est facile :
TextColumn::make('name')->searchable()->sortable(),

En ce qui concerne la recherche (searchable), elle a été prévue sur les noms :

Vous avez dû remarquer que les colonnes created_at et updated_at n'apparaissent pas dans le tableau. Il est possible d'activer leur présence avec ce menu déroulant :
Dans les propriétés de ces colonnes, il est prévu de les cacher et de permettre leur apparition :
TextColumn::make('created_at')
...
->toggleable(isToggledHiddenByDefault: true),
On va aussi franciser les titres des colonnes :
TextColumn::make('name')
->label(__('Name'))
...
TextColumn::make('birth')
->label(__('Birth'))
...
On ajoute les traductions dans fr.json :
"Name": "Nom",
"Birth": "Naissance",

Je vous laisse découvrir toutes les possibilités dans la documentation.
Les formulaires

Voyons à présent comment les formulaires. Lorsque dans le tableau, on clique sur "modifier" sur une ligne, on affiche le formulaire de modification :

Et tout fonctionne très bien avec un datepicker pour la date. La classe pour le formulaire est AuthorForm dans le dossier Schemas :
return $schema
->components([
TextInput::make('name')
->required(),
DatePicker::make('birth')
->required(),
]);
Mais on n'a pas de validation active. D'autre part, on veut des titres en français et la date dans le bon format. On va donc compléter ça :
use App\Models\Author;
...
return $schema
->components([
TextInput::make('name')
->label(__('Name'))
->autofocus()
->required()
->maxLength(255)
->unique(table: Author::class),
DatePicker::make('birth')
->label(__('Birth'))
->displayFormat('d/m/Y')
->required(),
]);
C'est déjà mieux :

On vérifie la validation :

On a de la même manière un formulaire pour la création :

On a donc vu qu'avec peu de code, on a une gestion complète de notre ressource. Sans Filament, on aurait dû créer les routes, le contrôleur, les vues. Mais évidemment j'ai pris un cas très simple qui ne nous pose aucun problème particulier.
Pour les formulaires, vous trouvez la documentation complète ici.
Des livres
Les auteurs sont censés écrire des livres.
On va créer une migration pour une table de livres avec le modèle et le factory associés :
php artisan make:model Book -mf
Dans la migration, on prévoit ce code :
public function up()
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->timestamps();
$table->foreignId('author_id')
->constrained()
->onDelete('cascade')
->onUpdate('cascade');
});
}
On a juste le titre pour le livre, mais surtout, on crée une clé étrangère qui lie cette table à celle des auteurs.
On ajoute la propriété $fillable dans le modèle Book :
protected $fillable = ['title'];
On ajoute la relation dans Author :
use Illuminate\Database\Eloquent\Relations\HasMany;
class Author extends Model
{
...
public function books(): HasMany
{
return $this->hasMany(Book::class);
}
}
Et la réciproque dans Book :
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Book extends Model
{
...
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
}
On code le factory :
public function definition()
{
return [
'title' => $this->faker->sentence(3),
];
}
On utilise le factory dans DatabaseSeeder pour créer 10 auteurs, avec pour chacun d'eux 5 livres :
use App\Models\Book;
...
class DatabaseSeeder extends Seeder
{
...
public function run()
{
Author::factory()->has(Book::factory()->count(5))->count(10)->create();
}
On rafraichit les migrations et on remplit les tables (pensez à recréer l'administrateur qui va disparaitre dans l'opération) :
php artisan migrate:fresh --seed
On a de nouveau 10 auteurs, mais chacun d'eux a 5 livres.
Une ressource pour les livres
On va créer une ressource pour les livres :
php artisan make:filament-resource Book
Répondez oui à cette question :
Should the configuration be generated from the current database columns? (yes/no) [no]
Comme pour les auteurs, on a la création de toutes les classes utiles :

On change l'icône et le titre dans la ressource BookResource :
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBookOpen;
public static function getNavigationLabel(): string
{
return __('Books');
}
Dans fr.json :
"Books": "Livres"

Tableau
Le tableau est déjà renseigné :

On met le titre en français :
public static function getModelLabel(): string
{
return __('Books');
}
On ajoute deux traductions dans fr.json :
"Title": "Titre",
"Author": "Auteur"
Et on complète le code de la classe BooksTable :
->columns([
TextColumn::make('title')
->label(__('Title'))
->searchable(),
...
TextColumn::make('author.name')
->label(__('Author'))
->sortable()
->searchable(),
])

Encore une fois, on n'a pas eu beaucoup de travail.
Les formulaires
Les formulaires tiennent déjà compte de la relation avec les auteurs, on va se contenter d'ajouter les éléments de validation :
return $schema
->components([
TextInput::make('title')
->label(__('Title'))
->autofocus()
->required()
->maxLength(255)
->unique(table: Book::class)
->validationMessages([
'unique' => __('The title of the book already exists.'),
]),
Select::make('author_id')
->label(__('Author'))
->relationship('author', 'name')
->searchable()
->required(),
]);
On n'oublie pas la traduction dans fr.json :
"The title of the book already exists.": "Le titre du livre existe déjà."

Dans le champ de l'auteur, si on clique, on a ce message qui nous propose une recherche :

On entre des caractères et on obtient la liste des auteurs qui correspondent, il suffit ensuite de cliquer sur le bon :
On s'amuse un peu
Nombre de livres par auteur
Ce qui serait intéressant, c'est d'avoir dans le tableau des auteurs le nombre de livres pour chacun. On complète le tableau AuthorTable :
TextColumn::make('books_count')
->label(__('Books'))
->counts('books'),
On utilise le nom de la relation books. On doit respecter les conventions de Laravel en écrivant books_count. On dispose alors de la méthode counts pour compter les enregistrements en relation, donc ici les livres pour chaque auteur.
On obtient cet affichage :

Je me rends compte que le format de la date n'est pas vraiment lisible pour un français, on arrange facilement ça :
TextColumn::make('birth')
->label(__('Birth'))
->isoDate()

Groupement
Maintenant voyons comment grouper les livres par auteur :
return $table
->groups([
Group::make('author.name')
->label(__('Author'))
->collapsible(),
])
->columns([
...
TextColumn::make('author.name')
->label(__('Author'))
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->searchable(),
])

Les droits d'accès
Par défaut, tous les utilisateurs en local peuvent accéder à l'administration. En production, il faut ajouter un peu de code pour autoriser certains utilisateurs à avoir cet accès. Ça se passe dans le modèle User :
...
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
class User extends Authenticatable implements FilamentUser
{
...
public function canAccessPanel(Panel $panel): bool
{
return str_ends_with($this->email, '@chezmoi.fr') && $this->hasVerifiedEmail();
}
La fonction canAccessPanel doit retourner True pour avoir accès à l'administration. La documentation sur le sujet est ici.
Conclusion
On a vu avec un exemple simple que Filament permet de construire rapidement une administration esthétique et efficace. Je n'ai fait qu'effleurer toutes les possibilités dans cet article qui est juste une introduction rapide.
En plus d'être très bien équipé, Filament propose aussi une quantité considérable de plugins. Certains sont payants, mais il y en a de nombreux gratuits. Vous en trouverez forcément un qui répondra à vos besoins spécifiques. Mais vous pouvez aussi en créer, tout est expliqué ici.
Par bestmomo
Aucun commentaire
