
Les meilleures pratiques Laravel pour un code propre et maintenable
Introduction
Laravel est aujourd’hui l’un des frameworks PHP les plus appréciés, notamment pour sa syntaxe expressive et sa capacité à accélérer le développement d’applications web robustes. Mais derrière cette simplicité apparente, il est important d’adopter certaines bonnes pratiques Laravel pour éviter que le code ne devienne rapidement difficile à maintenir.
Dans cet article, nous allons parcourir quelques bonnes pratiques à travers des exemples concrets, souvent rencontrés dans des projets Laravel. L’objectif n’est pas d’être exhaustif, mais de poser des bases solides pour structurer un projet de manière plus propre, plus claire et plus cohérente.
1- Extraire la logique métier des contrôleurs en Laravel
L’une des erreurs fréquentes en Laravel est de placer trop de logique métier directement dans les contrôleurs. Cela rend le code difficile à maintenir, à tester et à réutiliser. Une meilleure pratique consiste à transférer cette logique vers le modèle, où elle a réellement sa place. Cela aide également à améliorer la lisibilité et à respecter les principes de séparation des responsabilités.
❌ Mauvais exemple : logique métier dans le contrôleur
public function showProducts() { $products = Product::where('status', 'active') ->whereHas('reviews', function ($query) { $query->where('score', '>', 3); }) ->get(); return view('products.index', ['products' => $products]); }
Dans cet exemple, le contrôleur gère la récupération des produits actifs avec leurs meilleures évaluations, ce qui entraîne une logique trop complexe pour un simple contrôleur. Cette logique pourrait être utilisée dans plusieurs parties de l'application, mais elle est enfermée ici.
✅ Bon exemple : logique métier dans le modèle
public function showProducts() { $products = Product::getActiveWithGoodReviews(); return view('products.index', compact('products')); } // Dans le modèle **Product** class Product extends Model { public function scopeActive($query) { return $query->where('status', 'active'); } public function scopeWithGoodReviews($query) { return $query->whereHas('reviews', function ($query) { $query->where('score', '>', 3); }); } // Méthode d'instance public function getActiveWithGoodReviews(): Collection { return $this->active() ->withGoodReviews() ->get(); } }
- - Le contrôleur est plus simple et plus facile à maintenir, car la logique est déplacée dans une méthode dédiée dans un dépôt ou un modèle.
- - La logique métier est encapsulée dans des méthodes réutilisables, rendant le code plus modulaire.
- - Le code respecte la séparation des responsabilités, ce qui le rend plus clair et moins sujet aux erreurs.
- - Il devient plus facile à tester, car la logique complexe est maintenant dans des méthodes distinctes, bien isolées.
2- Organiser la validation avec des Form Requests en Laravel
❌ Mauvais exemple : Validation dans le contrôleur
// Dans le contrôleur (PostController) public function store(Request $request) { $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', 'publish_at' => 'nullable|date', ]); // Logique de création de post }
Ici, le contrôleur gère directement la validation de la requête. Bien que cela soit simple, à mesure que l’application grandit, la validation peut devenir difficile à gérer et rendre le contrôleur plus difficile à tester.
✅ Bon exemple : Utilisation d’une Form Request personnalisée
// Dans le contrôleur (PostController) public function store(PostRequest $request) { // La validation est déjà gérée dans la classe PostRequest // Logique de création de post } // Dans app/Requests/PostRequest.php namespace App\Requests; use Illuminate\Foundation\Http\FormRequest; class PostRequest extends FormRequest { // Définir les règles de validation ici public function rules(): array { return [ 'title' => 'required|unique:posts|max:255', 'body' => 'required', 'publish_at' => 'nullable|date', ]; } // Vous pouvez également personnaliser les messages d'erreur si nécessaire public function messages(): array { return [ 'title.required' => 'Le titre est obligatoire.', 'title.unique' => 'Ce titre est déjà utilisé.', ]; } }
- - Séparation des responsabilités : Le contrôleur est désormais responsable uniquement de la logique métier, et la validation est séparée dans une classe de requête dédiée.
- - Code plus propre et plus réutilisable : La logique de validation est maintenant isolée dans un fichier spécifique, ce qui permet de la réutiliser facilement dans d'autres méthodes ou contrôleurs si nécessaire.
- - Facilité de test : Les règles de validation sont maintenant dans une classe dédiée, ce qui les rend plus faciles à tester de manière indépendante.
- - Personnalisation : Vous pouvez facilement personnaliser les messages d’erreur, gérer les règles de validation complexes, et ajouter des règles de validation supplémentaires dans un seul fichier sans encombrer vos contrôleurs.
3- Centraliser les appels env() dans les fichiers de configuration
❌ Mauvaise pratique
class SmsService { public function send(string $to, string $message): void { $apiToken = env('TWILIO_API_TOKEN'); // Appel vers l’API Twilio avec $apiToken... } }
L’appel direct à env() dans le code d’application (contrôleurs, modèles, services, etc.) est déconseillé. Cela fonctionne en développement, mais peut provoquer des comportements imprévus, notamment en production avec la mise en cache de la configuration (php artisan config:cache), où certaines variables peuvent ne plus être accessibles correctement.
✅ Bonne pratique
La bonne façon de procéder est de centraliser tous les appels env() dans les fichiers de configuration du dossier config/. Ensuite, vous pouvez utiliser la fonction config() pour accéder à ces valeurs depuis votre application.
return [ // ... 'twilio' => [ 'api_token' => env('TWILIO_API_TOKEN'), ], ];
4- Utiliser les transactions pour garantir l’intégrité des données
Quand tu exécutes plusieurs opérations en base de données qui dépendent les unes des autres, il est crucial d’utiliser une transaction pour éviter d’enregistrer des données incomplètes ou corrompues.
❌ Mauvais exemple – sans transaction
Prenons un exemple avec un système de réservation d’hôtel :
public function reserveRoom(Request $request) { $reservation = new Reservation; $reservation->user_id = $request->user_id; $reservation->room_id = $request->room_id; $reservation->save(); $invoice = new Invoice; $invoice->reservation_id = $reservation->id; $invoice->amount = 150; $invoice->save(); }
Ici, tout semble correct en apparence. Mais imagine ce scénario :
- - La réservation s’enregistre correctement.
- - Une erreur se produit pendant l'enregistrement de la facture (Invoice).
➡ Résultat ? La réservation existe… sans facture liée. C’est une incohérence métier, difficile à rattraper.
✅ Bon exemple – avec une transaction
Laravel propose une solution simple via le facade DB :
use Illuminate\Support\Facades\DB; public function reserveRoom(ReservationRequest $request) { DB::beginTransaction(); try { $reservation = Reservation::create($request->validated()); Invoice::create([ 'reservation_id' => $reservation->id, 'amount' => 150, ]); DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; } }
Avec DB::beginTransaction(), si l’une échoue, rien ne sera inscrit en base. C’est ce qu’on appelle le principe d’atomicité (le “A” de ACID en base de données).