PHPMD : quand votre code devient un roman de Bazac

Temps de lecture : 3 min

PHPMD : le détecteur de code pourri qui va démolir votre estime de développeur. Découvrez la complexité cyclomatique et comment refactoriser votre PHP avant qu'il ressemble à un roman de Balzac

Ton père a pris toute la compléxité pour la mettre dans ton code

Ah, la complexité cyclomatique ! Ce terme barbare qui fait fuir les développeurs plus vite qu'un commercial qui propose une "solution innovante disruptive". Pour ceux qui auraient séché les cours d'informatique pour aller boire des bières, laissez-moi vous expliquer ce concept avec la délicatesse que j’aurais dans un magasin de porcelaine.

La complexité cyclomatique, inventée par Thomas McCabe en 1976, époque où les développeurs croyaient encore aux promesses des chefs de projet, mesure le nombre de chemins d'exécution possibles dans votre code. En gros, c'est le nombre de fois où votre code peut dire "et si ?" avant de se perdre dans ses propres méandres comme un touriste parisien dans le métro.

<?php
// Complexité cyclomatique = 1 (un seul chemin)
function simpleFunction() {
    return "Hello World";
}

// Complexité cyclomatique = 3 (3 chemins possibles)
function complexFunction($age) {
    if ($age < 18) {
        return "Trop jeune pour comprendre PHP";
    } elseif ($age > 65) {
        return "Trop sage pour encore coder";
    } else {
        return "Parfait pour devenir développeur et souffrir";
    }
}

Plus votre complexité est élevée, plus votre code ressemble à un labyrinthe conçu par un sadique. Au-delà de 10, on entre dans la zone rouge. Au-delà de 20, on appelle ça de l'art contemporain.

PHPMD : le justicier masqué de votre code pourri

Et c'est là qu'intervient PHPMD (PHP Mess Detector), le superhéros dont personne ne voulait mais dont tout le monde a besoin. Créé par Manuel Pichler, ce formidable outil d'analyse statique a pour mission de vous dire que votre code pue plus qu'un fromage oublié dans une voiture en plein été.

PHPMD, c'est un peu le prof qui corrige vos copies avec un stylo rouge et qui n'hésite pas à écrire « Peut mieux faire » en gros sur votre travail. Sauf qu'au lieu de corriger votre orthographe, il corrige votre logique de développement défaillante.

Installation : parce que souffrir, ça s'apprend

Installer PHPMD, c'est comme installer une alarme anti-vol : au début, on se dit que c'est une bonne idée, puis on réalise qu'elle va sonner tout le temps.

# Via Composer (parce qu'on est civilisés)
composer require --dev phpmd/phpmd

# Via PHAR (pour les nostalgiques)
wget https://phpmd.org/static/latest/phpmd.phar
chmod +x phpmd.phar

Une fois installé, PHPMD vous attend, tapi dans l'ombre, prêt à démolir votre estime de soi de développeur.

Les règles de PHPMD : un catalogue d'horreurs quotidiennes

PHPMD propose plusieurs ensembles de règles, comme autant de façons différentes de vous expliquer que vous codez comme un pied.

CleanCode : parce que la propreté, c'est important

Les règles CleanCode s'attaquent à ces petites habitudes qui transforment votre code en déchetterie numérique.

<?php
// ❌ Mauvais : ElseExpression
function validateUser($user) {
    if ($user->isValid()) {
        return true;
    } else {
        return false; // Cette ligne offense Robert C. Martin personnellement
    }
}

// ✅ Bon : Direct et sans fioritures
function validateUser($user) {
    return $user->isValid();
}

// ❌ Mauvais : BooleanArgumentFlag
function sendEmail($message, $isUrgent = false) {
    if ($isUrgent) {
        // Logique urgente
    } else {
        // Logique normale
    }
}

// ✅ Bon : Séparer les responsabilités
function sendEmail($message) {
    // Logique normale
}

function sendUrgentEmail($message) {
    // Logique urgente
}

CodeSize : quand votre fonction ressemble à "Guerre et Paix"

Ces règles détectent quand votre code devient plus long qu'un discours de Macron.

<?php
// ❌ Trop de paramètres (TooManyParameters)
function createUser($firstName, $lastName, $email, $phone, $address, 
                   $city, $zipCode, $country, $birthDate, $gender, 
                   $preferredLanguage, $newsletter, $marketingOptIn) {
    // Félicitations, vous venez de créer un monstre
}

// ✅ Utilisez un objet de transfert de données
class UserData {
    public function __construct(
        public string $firstName,
        public string $lastName,
        public string $email,
        // ... autres propriétés
    ) {}
}

function createUser(UserData $userData) {
    // Beaucoup mieux pour votre santé mentale
}

// ❌ Classe trop longue (TooManyMethods)
class GodClass {
    public function createUser() {}
    public function updateUser() {}
    public function deleteUser() {}
    public function sendEmail() {}
    public function generateReport() {}
    public function processPayment() {}
    public function manageSessions() {}
    public function handleFiles() {}
    public function validateData() {}
    public function logErrors() {}
    // ... 50 autres méthodes parce que "c'est pratique"
}

Controversial : les règles qui font débat (et qui ont raison)

<?php
// ❌ Superglobales : Le mal incarné
function getUser() {
    return $_SESSION['user']; // PHPMD n'aime pas, et il a raison
}

// ✅ Injection de dépendance, comme les grands
class UserController {
    public function __construct(
        private SessionInterface $session
    ) {}
    
    public function getUser() {
        return $this->session->get('user');
    }
}

// ❌ Camel case pour les propriétés (Symfony style)
class User {
    public $user_name; // PHPMD pleure
    public $email_address;
}

// ✅ Cohérence avec les standards PHP
class User {
    public string $userName;
    public string $emailAddress;
}

Design : l’architecture, cette grande oubliée

<?php
// ❌ Couplage excessif (CouplingBetweenObjects)
class OrderProcessor {
    public function process(Order $order) {
        $user = new UserRepository();
        $payment = new PaymentGateway();
        $email = new EmailService();
        $logger = new FileLogger();
        $cache = new RedisCache();
        $notification = new SlackNotification();
        // Félicitations, vous venez de coupler votre classe avec la Terre entière
    }
}

// ✅ Inversion de dépendance
class OrderProcessor {
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private PaymentGatewayInterface $paymentGateway,
        private EmailServiceInterface $emailService,
        private LoggerInterface $logger
    ) {}
    
    public function process(Order $order) {
        // Maintenant c'est testable et maintenable
    }
}

// ❌ Profondeur d'héritage excessive
class Animal {}
class Mammal extends Animal {}
class Primate extends Mammal {}
class Hominid extends Primate {}
class Human extends Hominid {}
class Developer extends Human {}
class PHPDeveloper extends Developer {}
class SymfonyDeveloper extends PHPDeveloper {
    // À ce niveau, même Darwin se retournerait dans sa tombe
}

Naming : parce que $data n'est pas un nom de variable

<?php
// ❌ Noms courts et inutiles
function calc($d, $r) {
    return $d * $r;
}

$u = getUserById($id);
$tmp = processData($u);

// ✅ Noms expressifs
function calculateTotalPrice($duration, $rate) {
    return $duration * $rate;
}

$user = getUserById($id);
$processedUserData = processUserData($user);

// ❌ Nom de classe trop long (LongClassName)
class AbstractFactoryProviderSingletonBuilderDecoratorAdapterFacade {
    // Non, ce n'est pas une parodie de Java, c'est un vrai problème PHP
}

// ✅ Simplicité
class UserFactory {
    // Parfait, on comprend immédiatement
}

UnusedCode : le cimetière de vos bonnes intentions

<?php
class UserService {
    private $dbConnection; // ❌ Propriété privée non utilisée
    
    public function getUser($id) {
        $query = "SELECT * FROM users WHERE id = ?"; // ❌ Variable locale non utilisée
        
        return User::find($id);
    }
    
    private function validateEmail($email) { // ❌ Méthode privée non utilisée
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }
    
    public function updateUser($id, $data, $timestamp) { // ❌ $timestamp non utilisé
        User::find($id)->update($data);
    }
}

Configuration : personnaliser votre torture

PHPMD permet de configurer ses règles via un fichier XML, parce que souffrir, c'est bien, mais souffrir intelligemment, c'est mieux.

<?xml version="1.0"?>
<ruleset name="Mon PHPMD personnalisé"
         xmlns="http://pmd.sf.net/ruleset/1.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 
                     http://pmd.sf.net/ruleset_xml_schema.xsd"
         xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
    
    <description>
        Règles PHPMD pour projet Symfony - Configuration pour développeurs masochistes
    </description>
    
    <!-- Clean Code rules -->
    <rule ref="rulesets/cleancode.xml">
        <!-- On exclut ElseExpression parce que parfois, on a besoin d'être explicite -->
        <exclude name="ElseExpression"/>
    </rule>
    
    <!-- Code Size rules avec des limites adaptées à la réalité -->
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity">
        <properties>
            <property name="reportLevel" value="12"/> <!-- Au lieu de 10, parce qu'on n'est pas des machines -->
        </properties>
    </rule>
    
    <rule ref="rulesets/codesize.xml/TooManyParameters">
        <properties>
            <property name="minimum" value="8"/> <!-- Symfony permet parfois plus de paramètres -->
        </properties>
    </rule>
</ruleset>

Intégration dans votre workflow : automatiser la souffrance

Avec PHPStorm

PHPStorm peut intégrer PHPMD directement, transformant votre IDE en machine à humiliation continue.

Settings > Tools > External Tools > Add
- Name: PHPMD
- Program: vendor/bin/phpmd
- Arguments: $FileDir$ text cleancode,codesize,controversial,design,naming,unusedcode
- Working directory: $ProjectFileDir$

Avec GitHub Actions

name: Code Quality
on: [push, pull_request]

jobs:
  phpmd:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 8.4
          
      - name: Install dependencies
        run: composer install --no-dev --optimize-autoloader
        
      - name: Run PHPMD
        run: vendor/bin/phpmd src text phpmd.xml

Avec Docker dans un Makefile

qa-phpmd: ## Run PHP Mess Detector
	@docker run -v $(PWD):/app -w /app -t --rm php:8.4-cli-alpine sh -c "php -d error_reporting='E_ALL & ~E_DEPRECATED' ./vendor/bin/phpmd src text phpmd.xml --suffixes php --exclude vendor,var,tests,migrations 2>/dev/null; exit 0"

Les métriques qui font mal : comprendre les rapports

PHPMD vous balance ses résultats comme un prof rend les copies : sans ménagement.

/src/Controller/UserController.php:45    The method processUserRegistration() has a Cyclomatic Complexity of 23. The configured cyclomatic complexity threshold is 10.

/src/Service/PaymentService.php:12    Avoid using static access to class '\App\Helper\TaxCalculator' in method 'calculateTax'.

/src/Entity/User.php:156    The method validateComplexBusinessRules() has 127 lines of code. Current threshold is 100. Avoid really long methods.

/src/Repository/UserRepository.php:89    The class UserRepository has 47 public methods. Consider whether some could be made private or protected to reduce the visibility.

Chaque ligne est une petite gifle qui vous rappelle que votre code pourrait être mieux. Mais au moins, elle est constructive, cette gifle.

PHPMD vs les autres : le battle royal des outils d'analyse

PHPMD vs PHP_CodeSniffer

PHP_CodeSniffer s'occupe du style (espaces, tabulations, position des accolades), PHPMD s'occupe de la logique. C'est la différence entre critiquer votre façon de vous habiller et critiquer votre personnalité.

PHPMD vs PHPStan/Psalm

PHPStan et Psalm analysent les types et trouvent les bugs potentiels. PHPMD trouve les problèmes d'architecture. C'est complémentaire : l'un trouve vos erreurs, l'autre trouve vos mauvaises décisions de vie (de développeur).

Cas d'usage réels : Quand PHPMD sauve des vies (de projets)

Refactoring d'un legacy (mouillez-vous la nuque)

<?php
// Avant : Code legacy cauchemardesque
// GIF de Phoebe "my eyes my eyes"
class UserManager {
    public function handleUser($action, $userData, $options = null, $validate = true, $sendEmail = false) {
        if ($action == 'create') {
            if ($validate) {
                if (isset($userData['email'])) {
                    if (filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
                        if (!$this->userExists($userData['email'])) {
                            $user = new User();
                            $user->email = $userData['email'];
                            if (isset($userData['name'])) {
                                $user->name = $userData['name'];
                                if (strlen($userData['name']) > 2) {
                                    $user->save();
                                    if ($sendEmail) {
                                        $this->sendWelcomeEmail($user);
                                    }
                                    return $user;
                                } else {
                                    throw new Exception('Name too short');
                                }
                            } else {
                                throw new Exception('Name required');
                            }
                        } else {
                            throw new Exception('User exists');
                        }
                    } else {
                        throw new Exception('Invalid email');
                    }
                } else {
                    throw new Exception('Email required');
                }
            } else {
                // Logique sans validation...
            }
        } elseif ($action == 'update') {
            // Encore 50 lignes de if imbriqués...
        } elseif ($action == 'delete') {
            // Et encore 30 lignes...
        }
    }
}

Complexité cyclomatique : 47 (Félicitations, vous avez battu le record du monde)

<?php
// Après : Code civilisé grâce aux conseils de PHPMD
class UserCreator {
    public function __construct(
        private UserRepository $repository,
        private EmailService $emailService
    ) {}
    
    public function create(array $userData, bool $validate = true, bool $sendEmail = false): User {
        if ($validate) {
            $this->validateUserData($userData);
        }
        
        if ($this->repository->existsByEmail($userData['email'])) {
            throw new UserAlreadyExistsException('User exists');
        }
        
        $user = $this->createUserFromData($userData);
        $this->repository->save($user);
        
        if ($sendEmail) {
            $this->emailService->sendWelcomeEmail($user);
        }
        
        return $user;
    }
    
    private function validateUserData(array $userData): void {
        if (!isset($userData['email'])) {
            throw new InvalidArgumentException('Email required');
        }
        
        if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Invalid email');
        }
        
        if (!isset($userData['name'])) {
            throw new InvalidArgumentException('Name required');
        }
        
        if (strlen($userData['name']) <= 2) {
            throw new InvalidArgumentException('Name too short');
        }
    }
    
    private function createUserFromData(array $userData): User {
        $user = new User();
        $user->email = $userData['email'];
        $user->name = $userData['name'];
        
        return $user;
    }
}

// Et pour une approche encore plus moderne avec Symfony :
class UserCreator {
    public function __construct(
        private UserRepository $repository,
        private EmailService $emailService,
        private ValidatorInterface $validator
    ) {}
    
    public function create(CreateUserRequest $request): User {
        // Validation automatique via les constraints Symfony
        $violations = $this->validator->validate($request);
        if (count($violations) > 0) {
            throw new ValidationException($violations);
        }
        
        if ($this->repository->existsByEmail($request->email)) {
            throw new UserAlreadyExistsException();
        }
        
        $user = User::fromRequest($request);
        $this->repository->save($user);
        
        if ($request->sendWelcomeEmail) {
            $this->emailService->sendWelcomeEmail($user);
        }
        
        return $user;
    }
}

// Avec le DTO correspondant
class CreateUserRequest {
    #[Assert\NotBlank(message: 'Email required')]
    #[Assert\Email(message: 'Invalid email')]
    public string $email;
    
    #[Assert\NotBlank(message: 'Name required')]
    #[Assert\Length(min: 3, minMessage: 'Name too short')]
    public string $name;
    
    public bool $sendWelcomeEmail = false;
}

Complexité cyclomatique : 3 (Votre psychiatre vous remercie)

Les faux positifs : quand PHPMD se trompe (parfois)

PHPMD n'est pas parfait. Parfois, il râle pour rien, comme un voisin qui se plaint du bruit alors que vous écoutez une cantate de Bach.

<?php
// PHPMD va râler sur cette factory, mais elle est justifiée
class PaymentProcessorFactory {
    public function create(string $type): PaymentProcessorInterface {
        return match ($type) {
            'stripe' => new StripeProcessor(),
            'paypal' => new PaypalProcessor(),
            'bank_transfer' => new BankTransferProcessor(),
            'crypto' => new CryptoProcessor(),
            'apple_pay' => new ApplePayProcessor(),
            'google_pay' => new GooglePayProcessor(),
            'amazon_pay' => new AmazonPayProcessor(),
            'klarna' => new KlarnaProcessor(),
            default => throw new InvalidPaymentTypeException()
        };
    }
}

Dans ce cas, la complexité est justifiée. Vous pouvez ignorer cette règle spécifique avec :

// @SuppressWarnings(PHPMD.CyclomaticComplexity)

Métriques avancées : devenir un ninja de l'analyse

PHPMD calcule plusieurs métriques intéressantes :

# Générer un rapport avec toutes les métriques
vendor/bin/phpmd src xml codesize,unusedcode --reportfile report.xml

# Analyser uniquement les violations critiques
vendor/bin/phpmd src text cleancode --minimumpriority 1

Performance et optimisation : PHPMD sur de gros projets

Sur de gros projets, PHPMD peut être lent. Quelques astuces :

# Exclure les dossiers lourds
vendor/bin/phpmd src text cleancode --exclude vendor,var,public

# Analyser seulement les fichiers modifiés
git diff --name-only --diff-filter=AM | grep "\.php$" | xargs vendor/bin/phpmd text cleancode

# Parallélisation avec GNU parallel
find src -name "*.php" | parallel -j4 vendor/bin/phpmd {} text cleancode

Conclusion : embrasser la complexité (pour mieux la combattre)

PHPMD, c'est comme une salle de sport pour votre code : c'est douloureux, vous n'avez pas envie d'y aller, mais au final, vous vous sentez mieux après. Et comme pour la salle de sport, la régularité est la clé du succès.

Oui, PHPMD va vous humilier. Oui, il va vous montrer que votre code ressemble à un plat de spaghettis jeté contre un mur. Mais c'est exactement ce dont vous avez besoin pour progresser.

L'objectif n'est pas d'avoir un score parfait à PHPMD (sinon, vous passerez plus de temps à optimiser vos métriques qu'à développer des fonctionnalités), mais d'avoir une base de code maintenable où l'on peut naviguer sans avoir envie de tout reécrire.

Alors installez PHPMD, configurez-le intelligemment, et acceptez que parfois, la vérité fait mal. Mais au moins, elle fait progresser.

Et rappelez-vous : un code avec une complexité cyclomatique de 50, c'est comme une blague de Bigard : tout le monde comprend qu'il y a un problème, mais personne n'ose rien dire.