Je me souviens très bien de ce matin de mars. J'arrive sur un projet Laravel en reprise. Le client m'envoie le dépôt Git. J'ouvre le premier contrôleur : 847 lignes. Le deuxième : 1 203 lignes. Un UserController qui gère les commandes, les emails, les exports CSV, les notifications et la facturation. Tout au même endroit.
Ce jour-là, j'ai compris que écrire du code qui fonctionne et écrire du bon code sont deux disciplines très différentes. Voici les 8 règles que j'applique depuis systématiquement.
Règle #1 : Un nom qui dit la vérité
Le nommage, c'est la base. Un bon nom rend le code auto-documenté. Un mauvais nom force n'importe qui (y compris vous dans 6 mois) à lire l'implémentation pour comprendre l'intention.
// ❌ Noms qui n'expliquent rien
function d($u) { ... }
$flag = true;
$arr2 = [];
// ✅ Noms qui racontent une histoire
function deactivateUser(User $user): void { ... }
$isEmailVerified = true;
$filteredPosts = [];
Règle pratique : Si vous avez besoin d'un commentaire pour expliquer le nom, le nom est mauvais.
Règle #2 : Responsabilité Unique (Single Responsibility Principle)
C'est le S du SOLID. Une classe, une méthode, une responsabilité. Un contrôleur qui gère la logique métier, les emails ET les PDF, c'est trois classes déguisées en une.
// ❌ Un contrôleur qui fait tout
class OrderController extends Controller
{
public function store(Request $request)
{
// Valider la commande
// Créer la commande en DB
// Mettre à jour le stock
// Envoyer l'email de confirmation
// Générer la facture PDF
// Notifier le vendeur
// ...200 lignes plus tard
}
}
// ✅ Responsabilités séparées
class OrderController extends Controller
{
public function store(StoreOrderRequest $request, OrderService $orderService): JsonResponse
{
$order = $orderService->createOrder($request->validated());
return response()->json(new OrderResource($order), 201);
}
}
class OrderService
{
public function createOrder(array $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create($data);
$this->stockService->decrementStock($order);
OrderCreated::dispatch($order); // L'event gère email + facture
return $order;
});
}
}
Règle #3 : DRY — Don't Repeat Yourself
Toute connaissance doit avoir une seule représentation dans votre système. Si vous copiez-collez du code, c'est un signal : il manque une abstraction.
// ❌ Logique dupliquée dans 3 contrôleurs
$posts = BlogPost::where('is_published', true)
->whereNotNull('published_at')
->where('published_at', '<=', now())
->get();
// ✅ Un scope réutilisable
// Dans BlogPost.php
public function scopePublished(Builder $query): Builder
{
return $query->where('is_published', true)
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
// Dans vos contrôleurs — lisible, cohérent, maintenable
$posts = BlogPost::published()->get();
Règle #4 : Des fonctions courtes qui font une chose
Si vous ne pouvez pas décrire ce que fait votre méthode en une phrase sans utiliser "et", elle est trop grande.
// ❌ Méthode qui fait 4 choses
public function handleUserRegistration(array $data): User
{
$user = User::create($data);
Mail::to($user)->send(new WelcomeEmail($user));
$user->assignRole('member');
Cache::forget('users_count');
return $user;
}
// ✅ Petites fonctions, responsabilités claires
public function register(array $data): User
{
$user = $this->createUser($data);
$this->assignDefaultRole($user);
$this->sendWelcomeEmail($user);
$this->invalidateUserCountCache();
RegisteredEvent::dispatch($user);
return $user;
}
Règle #5 : Éviter les nombres et chaînes magiques
// ❌ Qu'est-ce que signifie "2" ici ?
if ($user->role === 2) { ... }
if ($order->status === 'pending_payment') { ... }
// ✅ Des constantes qui donnent du sens
if ($user->role === Role::ADMIN) { ... }
if ($order->status === OrderStatus::PENDING_PAYMENT) { ... }
// Dans OrderStatus.php
enum OrderStatus: string
{
case PENDING_PAYMENT = 'pending_payment';
case PAID = 'paid';
case SHIPPED = 'shipped';
case DELIVERED = 'delivered';
case CANCELLED = 'cancelled';
}
Règle #6 : Les commentaires expliquent le POURQUOI, pas le QUOI
Si votre code a besoin d'un commentaire pour expliquer CE QU'IL FAIT, réécrivez-le pour qu'il soit plus clair. Les commentaires doivent expliquer le POURQUOI — les décisions de conception, les workarounds, les contraintes externes.
// ❌ Commentaire inutile (le code le dit déjà)
// Récupère l'utilisateur par email
$user = User::where('email', $email)->first();
// ✅ Commentaire utile (explique la décision)
// On utilise firstOrFail() ici intentionnellement :
// si l'utilisateur n'existe pas à ce stade, c'est une erreur
// système, pas une erreur utilisateur.
$user = User::where('email', $email)->firstOrFail();
Règle #7 : Gérer les erreurs explicitement
// ❌ Ignorer silencieusement les erreurs
try {
$result = $externalApi->fetch($id);
} catch (Exception $e) {
// ...
}
// ✅ Gérer et logguer les erreurs
try {
$result = $externalApi->fetch($id);
} catch (ApiException $e) {
Log::error('External API failure', [
'endpoint' => 'fetch',
'id' => $id,
'error' => $e->getMessage(),
]);
throw new ServiceUnavailableException('Service externe indisponible.', previous: $e);
}
Règle #8 : Tester son code (vraiment)
Un code sans tests est un code que personne n'ose refactoriser. Les tests ne sont pas du luxe — ce sont vos filets de sécurité pour les évolutions futures.
// Un test PHPUnit pour un service Laravel
class OrderServiceTest extends TestCase
{
use RefreshDatabase;
public function test_it_creates_an_order_and_decrements_stock(): void
{
$product = Product::factory()->create(['stock' => 10]);
$service = app(OrderService::class);
$order = $service->createOrder([
'product_id' => $product->id,
'quantity' => 3,
]);
$this->assertDatabaseHas('orders', ['id' => $order->id]);
$this->assertEquals(7, $product->fresh()->stock);
}
}
Conclusion : le clean code est un investissement
Ces 8 règles ne ralentissent pas votre développement — elles l'accélèrent, sur le long terme. La dette technique que vous accumulez avec du code illisible, vous la payez avec intérêts à chaque évolution, chaque bug, chaque nouveau développeur qui rejoint le projet.
Le meilleur moment pour écrire du clean code, c'était il y a 3 ans. Le deuxième meilleur moment, c'est maintenant.