PHPStan valide nos règles de conception

Publié le 18 janvier 2023 par William Suppo
Couverture de l'article PHPStan valide nos règles de conception

Comment s’assurer du respect de nos règles de conception ? Avec PHPStan bien sûr !

Qu’est-ce que l’outil PHPStan ?

Comme son nom l’indique, PHPStan est un outil d’analyse statique pour PHP.

Tout d’abord, introduisons l’analyse statique : grossièrement, c’est un découpage en arbre de la moindre instruction de notre code. Ces instructions sont des nœuds dans l’arbre qui ont chacun leur type qui peut-être une classe, une méthode ou même un if ou un return.

Et que fait PHPStan ? Il découpe notre code puis l’analyse afin de détecter de potentiels bugs ou du code mort.

Pourquoi définir nos règles de conception ?

Les règles de conception au sein d’une équipe ou même pour son soit du futur à plusieurs vertus :

La plus évidente est que nous habituons notre esprit à lire du code ayant la même syntaxe, la même architecture. En quelques sortes, c’est votre propre processeur que vous habituez à faire de l’analyse statique.

La seconde est qu’en admettant le respect de celles-ci, nous pouvons nous concentrer sur le fond du code, la production de valeur. L’écriture et la relecture de code sont grandement facilitées, car la forme est gérée par nos règles de conception.

La conséquence, c’est que nous avançons plus vite et mieux, car nous produisons du code de meilleure qualité donc plus maintenable.

Comment valider nos règles de conception ?

Nous entrons dans le vif du sujet, alors que peuvent être nos règles de conception ?

Voilà quelques règles possibles, elles sont peut-être dans un Markdown, possiblement versionnées et un lien vers celles-ci est probablement intégré au README.

Oui, mais nous sommes humains, en tout cas jusqu’aujourd’hui, et du coup nous oublions…

Nous oublions le README, et d’autant plus le fichier dédié à nos règles et du coup que faire ? Eh bien nous pouvons écrire des règles personnalisées avec PHPStan, elles seront donc exécutées dans notre CI comme les autres, ainsi nous ne les oublierons plus !

D’ailleurs, il peut être pertinent d’indiquer dans les messages d’erreurs des références à nos documents, ainsi ceux-ci ne servent plus de simple liste de règles, mais apporte du contexte sur le choix d’une règle particulière et potentiellement des alternatives à envisager ou non.

Installation et configuration de PHPStan

Pour ce faire, nous utilisons composer en lançant cette commande :

1composer require --dev phpstan/phpstan

Ensuite, nous le configurons via un fichier déposé à la racine du projet nommé phpstan.neon. Dans celui-ci, se trouvent notamment nos paramètres et nos règles, dont voici un exemple

1parameters:
2 level: max
3 paths:
4 - app
5 - tests
6
7rules:
8 - Rules\MyRule

Voilà, pour la configuration de base, nous pouvons maintenant lancer l’analyse comme ceci :

1php vendor/bin/phpstan analyse

Écriture de notre première règle !

La première règle que nous allons mettre en place est : nos contrôleurs doivent étendre le App\Http\Controllers\Controller.

Afin de différencier la définition de mes règles, de mon code métier et de mes tests, j’ai créé un namespace Rules à la racine de mon projet que j’ai ensuite défini dans la directive autoload-dev > psr-4 de mon fichier composer.json

Commençons par définir l’ossature de notre règle pour revenir sur chacun des points importants :

1namespace Rules;
2 
3use PhpParser\Node;
4use PHPStan\Rules\Rule;
5use PHPStan\Analyser\Scope;
6 
7class ControllersExtendsBaseControllerRule implements Rule
8{
9 public function getNodeType(): string
10 {
11 //
12 }
13 
14 public function processNode(Node $node, Scope $scope): array
15 {
16 //
17 }
18}

Notre classe implémente l’interface PHPStan\Rules\Rule qui lui impose les deux méthodes suivantes :

Le type que nous allons définir est le PHPStan\Node\InClassNode qui est, si on doit l’imager, la porte d’entrée d’une classe. Donc pour chacune des portes que nous allons ouvrir nous allons passer par la méthode processNode.

Pour celles et ceux qui souhaiteraient en apprendre plus sur l’analyse de code en PHP, je vous recommande chaudement l’article de JoliCode intitulé L’analyse statique dans le monde PHP.

Passons ensuite à l’analyse de notre nœud. Première vérification à faire, est-ce que nous sommes dans le namespace dédié aux contrôleurs ? Si ce n’est pas le cas, on sort, car la classe cible n’est pas un contrôleur :

1if (! str_starts_with($scope->getNamespace(), 'App\Http\Controllers')) {
2 return [];
3}

Seconde vérification à faire : est-ce que la classe que nous sommes en train de traiter est App\Http\Controllers\Controller soit la classe mère que l’on souhaite pour nos contrôleurs ? Si c’est le cas, on sort :

1$reflectionClass = $node->getClassReflection();
2 
3if ($reflectionClass->getName() === 'App\Http\Controllers\Controller') {
4 return [];
5}

À partir de maintenant, on peut admettre que nous sommes face à un contrôleur, il est temps de vérifier que celui ci étends de notre classe mère cible et si ce n’est pas le cas, nous produirons un message d’erreur :

1if (! $reflectionClass->isSubclassOf('App\Http\Controllers\Controller')) {
2 return [
3 "Controllers should extend 'App\Http\Controllers\Controller' (see rule #49)"
4 ];
5}

Notre règle est prête à être utilisée, nous allons l’ajouter dans notre fichier de configuration :

1rules:
2 - Rules\ControllersExtendsBaseControllerRule

Il nous reste plus qu’à lancer l’analyse, on obtient ce genre de résultat :

Untitled

Pour notre second cas, nous allons faire une analyse globale

Plaçons-nous dans un cas où nous souhaitons entamer une refacto d’une méthode dépréciée. Sur la période de transition, nous aimerions nous assurer que celle-ci n’est plus de nouveau utilisée. Nous allons donc définir un seuil d’utilisation à ne pas dépasser, ce seuil correspond aux nombres d’utilisation actuelle de la méthode dans notre base de code.

La réalisation du cas va se produire en 2 étapes :

Pour la détection, nous allons utiliser les Collectors dont l’ossature ressemble grandement aux Rules :

1namespace Rules;
2 
3use PhpParser\Node;
4use PHPStan\Analyser\Scope;
5use PHPStan\Collectors\Collector;
6 
7class DeprecatedFooUsageCollector implements Collector
8{
9 public function getNodeType(): string
10 {
11 //
12 }
13 
14 public function processNode(Node $node, Scope $scope)
15 {
16 //
17 }
18}

La méthode cible est statique, ce qui définit le type de nœud à analyser :

1public function getNodeType(): string
2{
3 return \PhpParser\Node\Expr\StaticCall::class;
4}

Ensuite, dans la méthode processNode nous allons vérifier que la méthode analysée est bien notre cible et si c’est le cas, nous allons retourner des informations sur le contexte dans lequel elle est appelée, c’est-à-dire le fichier et la ligne :

1public function processNode(Node $node, Scope $scope)
2{
3 if (
4 $node->class->toString() === 'App\FooRepository'
5 && $node->name->toString() === 'get'
6 ) {
7 return [
8 'file' => $scope->getFile(),
9 'line' => $node->getLine()
10 ];
11 }
12 
13 return null;
14}

Après avoir implémenté notre collector, nous devons le renseigner dans la configuration comme ceci :

1services:
2 -
3 class: Rules\DeprecatedFooUsageCollector
4 tags:
5 - phpstan.collector

Nous avons terminé la première étape, maintenant, il reste à effectuer le décompte du nombre d’utilisation de notre méthode et pour cela, on définit une nouvelle règle :

1namespace Rules;
2 
3use PhpParser\Node;
4use PHPStan\Rules\Rule;
5use PHPStan\Analyser\Scope;
6use PHPStan\Rules\RuleErrorBuilder;
7 
8class DeprecatedFooUsageCounterRule implements Rule
9{
10 const USAGE_COUNTDOWN = 2;
11 
12 public function getNodeType(): string
13 {
14 return \PHPStan\Node\CollectedDataNode::class;
15 }
16 
17 public function processNode(Node $node, Scope $scope): array
18 {
19 //
20 }
21}

Le type de nœud analysé est CollectedDataNode généré par PHPStan via notre Collector.

La constante USAGE_COUNTDOWN permet de mettre en évidence, le nombre d’usage restant, ainsi lorsqu’un appel à notre méthode est supprimé, il suffira de décrémenter cette valeur.

Passons au contenu de processNode , on va commencer par récupérer notre collection :

1$reportedUsage = $node->get(DeprecatedFooUsageCollector::class);

Ensuite on vérifie si on dépasse le seuil, si ce n’est pas le cas, on sort :

1if (count($reportedUsage) <= self::USAGE_COUNTDOWN) {
2 return [];
3}

Nous sommes maintenant certains que le seuil est dépassé, nous allons donc produire un tableau qui contient un message d’erreur pour chacun des usages, c’est ici que nous allons nous servir du fichier et de la ligne précédemment collecté :

1return array_map(function ($usage) {
2 return RuleErrorBuilder::message('Deprecated usage of App\FooRepository::get().')
3 ->file($usage[0]['file'])->line($usage[0]['line'])->build();
4}, $reportedUsage);

Il nous reste à ajouter cette règle dans la configuration :

1rules:
2 - Rules\DeprecatedFooUsageCounterRule

Puis lancer l’analyse dont la sortie peut être celle-ci dans le cas où le seuil est dépassé :

Untitled

Conclusion et règle bonus !

Nous avons découvert ensemble comment consolider notre code en automatisant la vérification des règles de conception via PHPStan.

En alternative, je vous invite à voir la conférence de Frédéric Bouchery intitulée Des tests unitaires pour nos règles de conception qui se base sur l’outil phpunit.

Enfin, si vous souhaitez partager vos propres règles ou simplement découvrir la règle bonus, passez sur ce sujet du forum.

William Suppo avatar
William Suppo
Je mange du PHP au p'tit dej', et vous ?

A lire

Autres articles de la même catégorie