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 ?
- Nos contrôleurs doivent étendre le
BaseController
- Le service
Cart
ne doit être utilisé qu’au sein du namespaceApp\Order
- La méthode
Cart::checkout
est dépréciée et ne doit plus être utilisé.
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: max3 paths:4 - app5 - tests67rules: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 directiveautoload-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(): string10 {11 //12 }13 14 public function processNode(Node $node, Scope $scope): array15 {16 //17 }18}
Notre classe implémente l’interface PHPStan\Rules\Rule
qui lui impose les deux méthodes suivantes :
-
getNodeType
: définis le type de nœud à analyser -
processNode
: contient l’analyse de chaque nœud du type mentionné, retourne un tableau contenant les erreurs si la règle n’est pas respectée, sinon un tableau vide.
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 :
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 :
- Détection de l’appel à la méthode
FooRepository::get()
- Vérification que le seuil n’est pas dépassé
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(): string10 {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(): string2{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\DeprecatedFooUsageCollector4 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(): string13 {14 return \PHPStan\Node\CollectedDataNode::class;15 }16 17 public function processNode(Node $node, Scope $scope): array18 {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é :
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.
A lire
Autres articles de la même catégorie
Debugger les requêtes SQL dans Laravel
Comment faire pour debugger efficacement vos requêtes SQL dans Laravel ?
Piloter son application avec Forge
De l’installation du serveur au déploiement automatique, découvrons ensemble l’outil Forge.
Les bases 4/6 : Validation des données
La validation des données dans Laravel permet de contrôler les valeurs d’un formulaire.