Structurer une application métier Laravel Inertia autour de dossiers et statuts
Concevez une application métier Laravel et Inertia avec dossiers, statuts, validations, actions, journal d'activité et recherche exploitable.
Sommaire
Structurer une application métier Laravel Inertia autour de dossiers et statuts
Beaucoup d'applications internes finissent par gérer la même chose: des dossiers, des statuts, des documents, des validations et des actions à faire. Si la structure est floue, le projet devient vite une grande table avec cinquante colonnes et des règles cachées dans les vues.
Ce tutoriel montre une base saine pour une application métier Laravel et Inertia: un modèle CaseFile, un historique de statuts, des validations explicites et des actions isolées. L'idée s'applique à de la gestion administrative, du suivi commercial, de la production documentaire ou de la relation client.
Objectif
Nous voulons obtenir:
- une liste de dossiers filtrable;
- des statuts métier cohérents;
- des validations traçables;
- un journal d'activité;
- des actions Laravel séparées de la logique d'affichage.
Le front peut être en Vue via Inertia, mais la logique métier doit rester côté Laravel.
Modèle de dossier
Commencez simple:
// app/Models/CaseFile.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class CaseFile extends Model
{
protected $fillable = [
'reference',
'customer_name',
'company_name',
'operation_type',
'status',
'assigned_to',
'due_at',
];
protected $casts = [
'due_at' => 'datetime',
];
public function validations(): HasMany
{
return $this->hasMany(CaseFileValidation::class);
}
public function activities(): HasMany
{
return $this->hasMany(CaseFileActivity::class);
}
}
La colonne status donne l'état courant, tandis que les tables liées conservent les détails: validations, commentaires, documents, emails, webhooks ou changements d'état.
Utiliser un enum pour les statuts
Les chaînes libres finissent toujours par diverger. Utilisez un enum:
// app/Enums/CaseFileStatus.php
namespace App\Enums;
enum CaseFileStatus: string
{
case Draft = 'draft';
case WaitingDocuments = 'waiting_documents';
case InReview = 'in_review';
case Ready = 'ready';
case Completed = 'completed';
case Blocked = 'blocked';
public function label(): string
{
return match ($this) {
self::Draft => 'Brouillon',
self::WaitingDocuments => 'Documents attendus',
self::InReview => 'En contrôle',
self::Ready => 'Prêt',
self::Completed => 'Finalisé',
self::Blocked => 'Bloqué',
};
}
}
Dans le modèle:
protected $casts = [
'status' => \App\Enums\CaseFileStatus::class,
'due_at' => 'datetime',
];
Cela évite les fautes de frappe et rend les transitions plus lisibles.
Isoler une transition métier
Évitez de changer les statuts directement dans les contrôleurs. Créez une action:
// app/Actions/CaseFiles/MarkCaseFileReady.php
namespace App\Actions\CaseFiles;
use App\Enums\CaseFileStatus;
use App\Models\CaseFile;
use Illuminate\Support\Facades\DB;
class MarkCaseFileReady
{
public function handle(CaseFile $caseFile, int $userId): void
{
DB::transaction(function () use ($caseFile, $userId) {
if ($caseFile->status !== CaseFileStatus::InReview) {
throw new \DomainException('Le dossier doit être en contrôle.');
}
$caseFile->update([
'status' => CaseFileStatus::Ready,
]);
$caseFile->activities()->create([
'user_id' => $userId,
'type' => 'status_changed',
'message' => 'Dossier marqué comme prêt.',
]);
});
}
}
Le contrôleur devient mince:
public function ready(CaseFile $caseFile, MarkCaseFileReady $action)
{
$action->handle($caseFile, auth()->id());
return back()->with('success', 'Dossier mis à jour.');
}
Ajouter des validations
Dans une application métier, le statut ne suffit pas. Un dossier peut être "en contrôle" mais manquer une validation.
// app/Models/CaseFileValidation.php
class CaseFileValidation extends Model
{
protected $fillable = [
'case_file_id',
'key',
'label',
'validated_at',
'validated_by',
];
protected $casts = [
'validated_at' => 'datetime',
];
public function isValidated(): bool
{
return $this->validated_at !== null;
}
}
Vous pouvez ensuite afficher une checklist dans Vue:
<template>
<ul class="space-y-2">
<li v-for="validation in validations" :key="validation.key">
<label class="flex items-center gap-3">
<input
type="checkbox"
:checked="validation.validated_at !== null"
@change="$emit('toggle', validation.key)"
>
<span>{{ validation.label }}</span>
</label>
</li>
</ul>
</template>
La validation doit rester côté serveur. Le front déclenche l'action, il ne décide pas seul.
Liste filtrable avec Inertia
Une liste métier doit permettre de retrouver vite un dossier. Côté contrôleur:
public function index(Request $request)
{
$caseFiles = CaseFile::query()
->when($request->search, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->where('reference', 'like', "%{$search}%")
->orWhere('customer_name', 'like', "%{$search}%")
->orWhere('company_name', 'like', "%{$search}%");
});
})
->when($request->status, fn ($query, $status) => $query->where('status', $status))
->latest()
->paginate(20)
->withQueryString();
return inertia('CaseFiles/Index', [
'caseFiles' => $caseFiles,
'filters' => $request->only(['search', 'status']),
]);
}
Côté Vue, utilisez un formulaire qui conserve les filtres dans l'URL. C'est utile pour partager une recherche à un collègue.
Journal d'activité
Chaque action importante doit laisser une trace:
$caseFile->activities()->create([
'user_id' => auth()->id(),
'type' => 'document_generated',
'message' => 'Le contrat PDF a été généré.',
'metadata' => [
'template' => 'contract-v3',
],
]);
Un journal simple suffit souvent: type, message, utilisateur, date, metadata JSON. Vous pourrez ensuite filtrer les erreurs, afficher l'historique dans le dossier, ou diagnostiquer une intégration externe.
Attention aux données sensibles
Si vous publiez une capture d'écran ou une démo, anonymisez les noms, sociétés, emails et références. Créez des seeders de démonstration au lieu d'utiliser des données réelles.
CaseFile::factory()
->count(30)
->sequence(fn ($sequence) => [
'customer_name' => 'Client '.($sequence->index + 1),
'company_name' => 'Société '.($sequence->index + 1),
])
->create();
C'est plus professionnel et beaucoup plus sûr.
Conclusion
Une application métier tient dans la durée quand les règles importantes sont explicites. Les dossiers, statuts, validations et activités forment une base lisible pour l'équipe comme pour le développeur.
Laravel gère très bien cette structure si vous gardez la logique dans des actions, les transitions dans des transactions, et le front Inertia comme interface de travail plutôt que comme moteur métier.