DEV
Accueil À propos Services Projets Blog Contact
Recruter
Laravel & PHP

Clean Code en PHP/Laravel : les 8 règles qui ont transformé mon code

🇬🇧 Read in English
Clean Code en PHP/Laravel : les 8 règles qui ont transformé mon code

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.

Articles similaires

Écrire sur WhatsApp