Dans le [Module 3], nous avons monté nos premières interfaces Filament : formulaires conditionnels, filtres et exploitation directe des Enums PHP 8.6.
Pourtant, un simple CRUD ne fait pas une application métier. Dans le monde réel, les données interagissent et des contraintes strictes s’imposent :
- Un équipement ne peut pas être affecté s’il est déjà en service ailleurs.
- Une restitution de matériel ne doit pas nécessiter d’ouvrir un formulaire complet : un clic sur une action rapide avec modale suffit.
- La fiche d’un client doit afficher l’historique complet de son matériel sans obliger l’utilisateur à naviguer d’un menu à l’autre.
Dans ce module, nous allons implémenter ces trois règles fondamentales avec les outils avancés de Laravel 13 et Filament 5 : scopes Eloquent, validation croisée réactive, modales d’action personnalisées et RelationManagers.
Règle métier : Empêcher la double affectation d’un matériel
Si un matériel est déjà lié à une affectation dont le statut est active ou pending_return, il ne doit pas être disponible lors de la création d’un nouveau contrat.
Étape A : Définir un Scope Eloquent sur le modèle Equipment
Dans app/Models/Equipment.php, ajoutons un scope pour isoler les matériels réellement disponibles :
use App\Enums\AssignmentStatus;
use Illuminate\Database\Eloquent\Builder;
/**
* Filtre les équipements qui ne sont pas actuellement en service.
*/
public function scopeAvailable(Builder $query, ?int $ignoreAssignmentId = null): Builder
{
return $query->whereDoesntHave('assignments', function (Builder $subQuery) use ($ignoreAssignmentId) {
$subQuery->whereIn('status', [
AssignmentStatus::Active->value,
AssignmentStatus::PendingReturn->value,
]);
if ($ignoreAssignmentId) {
$subQuery->where('id', '!=', $ignoreAssignmentId);
}
});
}
Étape B : Restreindre la liste déroulante dans Filament
Dans app/Filament/Resources/AssignmentResource.php, adaptons le champ equipment_id pour qu’il n’affiche que les matériels éligibles, tout en conservant l’équipement actuel en cas d’édition :
Forms\Components\Select::make('equipment_id')
->label('Équipement confié')
->relationship(
name: 'equipment',
titleAttribute: 'name',
modifyQueryUsing: fn (Builder $query, ?Assignment $record) =>
$query->available($record?->id)
)
->searchable()
->preload()
->required(),
Étape C : Sécuriser la validation côté serveur
L’interface empêche la sélection par défaut, mais pour parer à tout envoi concurrent, ajoutons une règle de validation personnalisée à la volée :
Forms\Components\Select::make('equipment_id')
// ... configuration précédente ...
->rules([
fn (?Assignment $record): \Closure => function (string $attribute, $value, \Closure $fail) use ($record) {
$isBusy = Assignment::where('equipment_id', $value)
->whereIn('status', [AssignmentStatus::Active->value, AssignmentStatus::PendingReturn->value])
->when($record, fn ($q) => $q->where('id', '!=', $record->id))
->exists();
if ($isBusy) {
$fail('Ce matériel est déjà en cours d’utilisation sur un autre contrat actif.');
}
},
]),
Action rapide : Clôturer une affectation depuis la table
Obliger un gestionnaire à ouvrir la page d’édition, changer le statut à « Clôturé », renseigner la date et enregistrer est une perte de temps. Nous allons créer une Table Action dédiée qui ouvre une modale de confirmation avec saisie de la date de retour.
Dans la méthode table() de app/Filament/Resources/AssignmentResource.php :
use App\Enums\AssignmentStatus;
use Filament\Notifications\Notification;
// ...
->actions([
Tables\Actions\Action::make('complete_assignment')
->label('Restituer')
->icon('heroicon-m-arrow-path-rounded-square')
->color('warning')
// L'action n'est visible que pour les matériels non encore clôturés
->visible(fn (Assignment $record): bool => $record->status !== AssignmentStatus::Completed)
->requiresConfirmation()
->modalHeading('Clôture du contrat de mise à disposition')
->modalDescription('Indiquez la date de retour effective du matériel.')
->form([
Forms\Components\DatePicker::make('returned_at')
->label('Date de restitution')
->default(now())
->maxDate(now())
->required(),
Forms\Components\Textarea::make('return_notes')
->label('État du matériel au retour')
->placeholder('Ex : Bon état, chargeur d’origine présent...'),
])
->action(function (Assignment $record, array $data): void {
$record->update([
'status' => AssignmentStatus::Completed,
'returned_at' => $data['returned_at'],
'notes' => $record->notes
? $record->notes . "\n[Retour le " . now()->format('d/m/Y') . "] " . $data['return_notes']
: $data['return_notes'],
]);
Notification::make()
->title('Matériel restitué avec succès')
->success()
->send();
}),
Tables\Actions\EditAction::make(),
])
D’un simple clic sur le bouton « Restituer« , l’opérateur valide la reprise sans quitter le listing, et une notification toast informe de la réussite de l’opération.
RelationManagers : L’historique matériel sur la fiche Client
Plutôt que d’aller dans le menu « Assignations » pour chercher quels ordinateurs possède Acme Corp, intégrons directement la liste des affectations sous la fiche du client.
Générons un gestionnaire de relation Filament :
php artisan make:filament-relation-manager ClientResource assignments client_id
Cette commande génère le fichier app/Filament/Resources/ClientResource/RelationManagers/AssignmentsRelationManager.php.
Configurons cette sous-table pour afficher le matériel, son statut et permettre l’assignation directe :
declare(strict_types=1);
namespace App\Filament\Resources\ClientResource\RelationManagers;
use App\Enums\AssignmentStatus;
use App\Models\Assignment;
use Filament\Forms;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;
class AssignmentsRelationManager extends RelationManager
{
protected static string $relationship = 'assignments';
protected static ?string $title = 'Parc matériel affecté';
protected static ?string $recordTitleAttribute = 'id';
public function form(Schema $schema): Schema
{
// On réutilise la configuration de formulaire déjà définie
return AssignmentResource::form($schema);
}
public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('equipment.name')
->label('Équipement')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('equipment.serial_number')
->label('Numéro de série')
->fontFamily('mono'),
Tables\Columns\BadgeColumn::make('status')
->label('Statut')
->formatStateUsing(fn (AssignmentStatus $state): string => $state->label())
->color(fn (AssignmentStatus $state): string => $state->color()),
Tables\Columns\TextColumn::make('monthly_cost')
->label('Coût mensuel')
->money('EUR'),
Tables\Columns\TextColumn::make('assigned_at')
->label('Début')
->date('d/m/Y'),
Tables\Columns\TextColumn::make('returned_at')
->label('Fin')
->date('d/m/Y')
->placeholder('En cours'),
])
->headerActions([
Tables\Actions\CreateAction::make()
->label('Nouvelle affectation'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
]);
}
}
Liaison dans la ressource ClientResource
Dans app/Filament/Resources/ClientResource.php, enregistrons ce RelationManager dans la méthode getRelations() :
use App\Filament\Resources\ClientResource\RelationManagers\AssignmentsRelationManager;
public static function getRelations(): array
{
return [
AssignmentsRelationManager::class,
];
}
Désormais, en éditant un client, un onglet complet présente la liste de tous ses matériels actuels et passés, avec la possibilité d’affecter un nouvel équipement directement depuis cette page.
Validation statique du code
Comme à chaque étape, validons la robustesse de notre implémentation :
composer run analyseLarastan vérifie l’intégrité des types, des closures de validation et des retours de scopes sans la moindre alerte.
Résumé du Module 4
En dépassant le cadre du simple générateur d’interfaces :
- Nous avons protégé l’intégrité de la base avec des scopes Eloquent et des règles de validation métier interdisant les conflits d’équipement.
- Nous avons fluidifié l’expérience utilisateur grâce aux modales d’action contextuelles.
- Nous avons imbriqué la gestion des matériels directement dans les fiches clients grâce aux RelationManagers.
Dans le Module 5, nous aborderons un pan critique en entreprise : Rôles, Permissions et Sécurité opérationnelle (gestion fine des accès via les Policies Laravel, masquage de colonnes selon le profil et activation de l’authentification à deux facteurs 2FA native).



