🧹 Clean Code

Maîtrisez les règles et principes pour un code propre

Ensemble de pratiques pour écrire un code clair, lisible et maintenable

"Un code propre ne se contente pas de fonctionner,
il dure dans le temps"
— Robert C. Martin (Uncle Bob)

📋 Les 10 Principes du Clean Code

1. Keep It Simple, Stupid (KISS) - Simplicité avant tout, noms explicites
2. Single Responsibility (SRP) - Une fonction = une tâche
3. Don't Repeat Yourself (DRY) - Pas de duplication
4. You Aren't Gonna Need It (YAGNI) - Pas d'anticipation inutile
5. Code auto-descriptif - Le code parle de lui-même
6. Limitez les paramètres - Maximum 3-4 paramètres
7. Simplifiez les conditions - Évitez l'imbrication
8. Gestion propre des erreurs - Exceptions > codes d'erreur
9. Supprimez le code mort - Variables/fonctions inutilisées
10. Code testable - Fonctions petites et indépendantes

💻 Exemple : Nommage explicite & Conditions simplifiées

Partie 1 : Nommage

❌ Mauvais
✅ Bon
public function a($r) {
    $u = $this->em->find(User::class, $r->get('id'));
    $d = ['n' => $u->getName()];
    return $this->json($d);
}
public function getUserDetails(Request $request): JsonResponse {
    $user = $this->entityManager->find(User::class, $request->get('id'));
    $userData = ['name' => $user->getName()];
    return $this->json($userData);
}

Partie 2 : Conditions simplifiées

❌ Mauvais
✅ Bon
public function canEdit(User $user, Article $article): bool {
    if ($user->isAdmin() || 
        ($user->getId() === $article->getAuthor()->getId() && 
         $article->getStatus() === 'draft' && 
         $article->getCreatedAt() > new DateTime('-24 hours'))) {
        return true;
    }
    return false;
}
public function canEdit(User $user, Article $article): bool {
    $isAdmin = $user->isAdmin();
    $isAuthor = $user->getId() === $article->getAuthor()->getId();
    $isDraft = $article->getStatus() === 'draft';
    $isRecent = $article->getCreatedAt() > new DateTime('-24 hours');
    
    $isEditable = $isAuthor && $isDraft && $isRecent;
    
    return $isAdmin || $isEditable;
}

💻 Exemple : Single Responsibility Principle

❌ Mauvais
✅ Bon
class OrderService
{
    public function processOrder(Order $order): void
    {
        // Validation
        if (!$order->getItems()) {
            throw new Exception('No items');
        }
        
        // Calcul du prix
        $total = 0;
        foreach ($order->getItems() as $item) {
            $total += $item->getPrice() * $item->getQuantity();
        }
        
        // Envoi d'email
        $mailer = new Mailer();
        $mailer->send($order->getCustomer()->getEmail(), 'Order confirmed');
        
        // Sauvegarde
        $this->em->persist($order);
        $this->em->flush();
    }
}
class OrderService
{
    public function __construct(
        private OrderValidator $validator,
        private PriceCalculator $calculator,
        private NotificationService $notifier,
        private EntityManagerInterface $em
    ) {}

    public function processOrder(Order $order): void
    {
        $this->validator->validate($order);
        $order->setTotal($this->calculator->calculate($order));
        $this->notifier->sendOrderConfirmation($order);
        
        $this->em->persist($order);
        $this->em->flush();
    }
}

✅ Chaque classe a une seule responsabilité : validation, calcul, notification, persistance

💻 Exemple : Don't Repeat Yourself (DRY)

❌ Mauvais
✅ Bon
class ProductRepository
{
    public function findActiveProducts(): array
    {
        return $this->createQueryBuilder('p')
            ->where('p.active = :active')
            ->andWhere('p.deletedAt IS NULL')
            ->setParameter('active', true)
            ->getQuery()
            ->getResult();
    }

    public function findActiveProductsByCategory($categoryId): array
    {
        // Répétition de la même logique !
        return $this->createQueryBuilder('p')
            ->where('p.active = :active')
            ->andWhere('p.deletedAt IS NULL')
            ->andWhere('p.category = :category')
            ->setParameter('active', true)
            ->setParameter('category', $categoryId)
            ->getQuery()
            ->getResult();
    }
}
class ProductRepository
{
    private function createActiveProductsQuery(): QueryBuilder
    {
        return $this->createQueryBuilder('p')
            ->where('p.active = :active')
            ->andWhere('p.deletedAt IS NULL')
            ->setParameter('active', true);
    }

    public function findActiveProducts(): array
    {
        return $this->createActiveProductsQuery()
            ->getQuery()
            ->getResult();
    }

    public function findActiveProductsByCategory($categoryId): array
    {
        return $this->createActiveProductsQuery()
            ->andWhere('p.category = :category')
            ->setParameter('category', $categoryId)
            ->getQuery()
            ->getResult();
    }
}

✅ La logique commune est extraite et réutilisée

💻 Exemple : Returns multiples

❌ Problématique
✅ Meilleur
✅ Early Returns
public function getUserDiscount(User $user)
{
    if ($user->isPremium()) {
        return 20; // Return 1 - Quel return a été exécuté ?
    }
    
    if ($user->getOrderCount() > 10) {
        return 15; // Return 2
    }
    
    if ($user->isFirstOrder()) {
        return 10; // Return 3
    }
    
    return 0; // Return 4 - Difficile à déboguer !
}

// ❌ Problèmes : Difficile à déboguer, complexe à tester
public function getUserDiscount(User $user): int
{
    $discount = 0;
    
    if ($user->isPremium()) {
        $discount = 20;
    } elseif ($user->getOrderCount() > 10) {
        $discount = 15;
    } elseif ($user->isFirstOrder()) {
        $discount = 10;
    }
    
    return $discount; // Un seul point de sortie !
}

// ✅ Plus facile à déboguer et à tester
// ✅ Exception : Early returns pour les garde-fous

public function processOrder(Order $order): bool
{
    // Garde-fous en début de fonction ✅
    if (!$order->hasItems()) {
        return false;
    }
    
    if (!$order->getCustomer()) {
        return false;
    }
    
    if ($order->getTotal() <= 0) {
        return false;
    }
    
    // Logique métier principale
    $this->calculateTotal($order);
    $this->notifyCustomer($order);
    $this->saveOrder($order);
    
    return true; // Un seul return à la fin de la logique
}

// ✅ Évite l'imbrication excessive

💻 Exemple : Typage strict des retours

❌ Sans typage
✅ Avec typage
class ProductRepository
{
    public function findById($id)
    {
        return $this->find($id); // Product ou null ?
    }
    
    public function findActive()
    {
        return $this->findBy(['active' => true]); // Array ? Collection ?
    }
    
    public function count()
    {
        return $this->createQueryBuilder('p')
            ->select('COUNT(p.id)')
            ->getQuery()
            ->getSingleScalarResult(); // String ? Int ?
    }
}
class ProductRepository
{
    public function findById(int $id): ?Product
    {
        return $this->find($id); // Clairement Product ou null
    }
    
    /** @return Product[] */
    public function findActive(): array
    {
        return $this->findBy(['active' => true]); // Tableau de Product
    }
    
    public function count(): int
    {
        return (int) $this->createQueryBuilder('p')
            ->select('COUNT(p.id)')
            ->getQuery()
            ->getSingleScalarResult(); // Clairement un entier
    }
}
✅ Avantages du typage : Auto-complétion IDE, détection d'erreurs, documentation automatique

💻 Exemple : Refactoring de setters multiples

❌ Mauvais
✅ DTO + Factory
✅ Constructeur
public function register(Request $request): Response
{
    $user = new User();
    $user->setEmail($request->get('email'));
    $user->setFirstName($request->get('firstName'));
    $user->setLastName($request->get('lastName'));
    $user->setPhone($request->get('phone'));
    $user->setAddress($request->get('address'));
    $user->setCity($request->get('city'));
    $user->setPostalCode($request->get('postalCode'));
    $user->setCountry($request->get('country'));
    $user->setPassword(
        $this->hasher->hash($request->get('password'))
    );
    
    $this->em->persist($user);
    $this->em->flush();
    
    return $this->json($user);
}
// Solution 1 : DTO (Data Transfer Object)
class RegisterUserDTO
{
    public function __construct(
        public readonly string $email,
        public readonly string $firstName,
        public readonly string $lastName,
        public readonly string $phone,
        public readonly string $address,
        public readonly string $city,
        public readonly string $postalCode,
        public readonly string $country,
        public readonly string $password,
    ) {}
}

class UserFactory
{
    public function createFromDTO(RegisterUserDTO $dto): User
    {
        $user = new User();
        $user->setEmail($dto->email);
        $user->setFirstName($dto->firstName);
        $user->setLastName($dto->lastName);
        // ... autres setters
        $user->setPassword($this->hasher->hash($dto->password));
        
        return $user;
    }
}

public function register(RegisterUserDTO $dto): Response
{
    $user = $this->userFactory->createFromDTO($dto);
    $this->em->persist($user);
    $this->em->flush();
    
    return $this->json($user);
}
// Solution 2 : Constructeur avec paramètres nommés (PHP 8+)
class User
{
    public function __construct(
        private string $email,
        private string $firstName,
        private string $lastName,
        private string $phone,
        private string $address,
        private string $city,
        private string $postalCode,
        private string $country,
        private string $hashedPassword,
    ) {}
}

public function register(Request $request): Response
{
    $user = new User(
        email: $request->get('email'),
        firstName: $request->get('firstName'),
        lastName: $request->get('lastName'),
        phone: $request->get('phone'),
        address: $request->get('address'),
        city: $request->get('city'),
        postalCode: $request->get('postalCode'),
        country: $request->get('country'),
        hashedPassword: $this->hasher->hash($request->get('password'))
    );
    
    $this->em->persist($user);
    $this->em->flush();
    
    return $this->json($user);
}
✅ Avantages : Code plus propre, validation centralisée, objets immutables

🏛️ SOLID - Les 5 Principes

S - Single Responsibility Principle

Une classe = une responsabilité

O - Open/Closed Principle

Ouvert à l'extension, fermé à la modification

L - Liskov Substitution Principle

Une classe dérivée remplace sa parente

I - Interface Segregation Principle

Interfaces spécifiques > interface générale

D - Dependency Inversion Principle

Dépendre d'abstractions, pas de concret

💻 SOLID - Exemple : Dependency Inversion

❌ Couplage fort
✅ Dépendance abstraite
class PaymentService
{
    private StripePayment $stripe;

    public function __construct()
    {
        $this->stripe = new StripePayment(); // Couplage fort !
    }

    public function processPayment(float $amount): void
    {
        $this->stripe->charge($amount);
    }
}

// ❌ Impossible de changer de provider sans modifier le code
interface PaymentGatewayInterface
{
    public function charge(float $amount): bool;
}

class StripePayment implements PaymentGatewayInterface
{
    public function charge(float $amount): bool
    {
        // Logique Stripe
        return true;
    }
}

class PaypalPayment implements PaymentGatewayInterface
{
    public function charge(float $amount): bool
    {
        // Logique Paypal
        return true;
    }
}

class PaymentService
{
    public function __construct(
        private PaymentGatewayInterface $gateway // Dépendance abstraite !
    ) {}

    public function processPayment(float $amount): void
    {
        $this->gateway->charge($amount);
    }
}

// ✅ Facile de changer de provider sans modifier PaymentService
✅ Avantage : Flexibilité totale, facilité de test (mock), changement de provider sans impact

💡 Recommandations Personnelles

1. Logique commune → Classes abstraites

Évitez la duplication avec l'abstraction

2. Découper au maximum = Compréhension

public function isEditable(
    RegistrationTab $registrationTab, 
    Registration $registration, 
    ?int $actorType
): bool {
    try {
        $this->checkRegistrationExists($registration);
        $this->checkRegistrationTabVisibility($registrationTab, $actorType);

        $configEdition = $this->getConfigEditionForActorType($registrationTab, $actorType);
        $this->checkRegistrationTabConfigEditionExists($configEdition);

        $currentStep = $this->registrationStepService->getCurrentRegistrationStep($registration);
        $this->checkCurrentTabInferiorNextValidationTab($registrationTab, $currentStep);

        return $this->isEditableAtAllSteps($configEdition)
            || $this->isEditableUntilStep($configEdition, $registration);
    } catch (UnauthorizedEditionException) {
        return false;
    }
}

// ✅ Lecture fluide, testable, maintenable, réutilisable

💡 Recommandations Personnelles (suite)

3. Vérification d'entités → instanceof

❌ Moins safe
✅ Plus safe
$user = $this->getUser();
if (!$user) {
    throw new AccessDeniedException();
}
// $user pourrait être false, null, 0, '', []...
$user = $this->getUser();
if (!$user instanceof User) {
    throw new AccessDeniedException('User must be authenticated');
}
// ✅ Type-safety garantie + auto-complétion IDE

4. Enums (PHP 8.1+) → Plutôt que des constantes

❌ Constantes
✅ Enum
class Order {
    public const STATUS_PENDING = 'pending';
    public const STATUS_PAID = 'paid';
    public const STATUS_SHIPPED = 'shipped';
    public const STATUS_DELIVERED = 'delivered';
    public const STATUS_CANCELLED = 'cancelled';
    
    private string $status;
    
    public function setStatus(string $status): void {
        $this->status = $status; // Aucune validation !
    }
}
// ❌ Entité surchargée de constantes
enum OrderStatus: string {
    case PENDING = 'pending';
    case PAID = 'paid';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
    case CANCELLED = 'cancelled';
}

class Order {
    private OrderStatus $status;
    
    public function setStatus(OrderStatus $status): void {
        $this->status = $status; // Type-safe !
    }
}

// Utilisation
$order->setStatus(OrderStatus::PAID); // Auto-complétion IDE

// ✅ Entité légère, type-safe, réutilisable
1 / 12