Nous exploitons des applications Laravel en production. Certaines ont traversé quatre versions majeures du framework sans douleur ; d’autres sont devenues difficiles à modifier au bout de dix-huit mois. La différence tient à un petit nombre de décisions prises très tôt.
Ce qui vieillit mal
La logique métier dans les contrôleurs
C’est le défaut le plus répandu et le plus coûteux. Un contrôleur de 300 lignes qui valide, calcule, écrit en base, envoie un e-mail et retourne une vue est impossible à tester autrement qu’en simulant une requête HTTP complète. Le jour où la même logique doit être appelée depuis une commande artisan ou une file d’attente, elle est copiée — et les deux copies divergent.
Les tableaux associatifs qui traversent l’application
Un tableau ['name' => ..., 'email' => ...] passé de couche en couche ne dit rien sur ce qu’il contient. Personne ne sait si phone est toujours présent, l’analyse statique ne peut rien vérifier, et une faute de frappe sur une clé se découvre en production.
Les événements utilisés comme colle universelle
Les événements Laravel sont excellents pour découpler ce qui doit l’être. Utilisés partout, ils rendent le flux d’exécution impossible à suivre : on ne sait plus ce qui se déclenche quand, ni dans quel ordre.
Ce qui tient
Isoler la logique dans des actions
Une action est une classe avec une seule méthode publique, qui fait une seule chose métier. Elle est appelable depuis un contrôleur, une commande, un job ou un test, sans adaptation.
final class SubmitContactRequestAction
{
public function execute(ContactRequestData $data): Lead
{
$lead = DB::transaction(fn () => Lead::create($data->toAttributes()));
ContactRequestReceived::dispatch($lead);
return $lead;
}
}
Le contrôleur se réduit alors à trois lignes : valider, appeler, répondre. Le test de la logique métier n’a plus besoin de requête HTTP.
Des objets de transfert typés
Remplacer les tableaux par des objets immuables typés apporte trois choses immédiatement : l’analyse statique détecte les erreurs de champ, l’éditeur complète les propriétés, et le contrat entre couches devient explicite.
final readonly class ContactRequestData
{
public function __construct(
public string $name,
public string $email,
public ?BudgetRange $budget,
public string $message,
) {}
}
Le coût est réel : quelques dizaines de lignes par objet. Le bénéfice apparaît au premier refactoring.
Des contrats pour l’accès aux données
Injecter une interface plutôt qu’un modèle Eloquent permet de changer la source sans toucher à la logique. Sur ce site même, le blog est stocké en fichiers Markdown ; basculer vers une base de données ne demanderait de modifier qu’une ligne de liaison dans le conteneur de services.
Attention toutefois : sur une application simple sans perspective de changement de source, cette couche est du coût sans bénéfice. Le discernement fait partie du métier.
Ce qui coûte 15 % et en rapporte trois fois plus
Sur nos projets, l’application systématique de ces trois principes représente environ 15 % de temps supplémentaire pendant les premiers mois. À partir de la deuxième année, le coût d’une évolution de taille moyenne est divisé par un facteur compris entre deux et quatre — mesuré sur nos propres estimations comparées aux temps réels.
Le vrai bénéfice n’est pourtant pas là. Il est dans le fait qu’une équipe ose modifier le code. Une application qu’on n’ose plus toucher est morte, même si elle fonctionne encore.