Pint, Rector, Larastan et Pest : les essentiels pour réussir
Fait partie de la série Améliorer votre blog VitePress avec une API Laravel
Maintenant que le projet Laravel est en place et que sa structure nous est familière, il est temps de rendre la base de code plus facile à maintenir.
À mesure que le projet grandira, un formatage cohérent, la refactorisation automatique, l'analyse statique et les tests nous aideront à avancer rapidement sans perdre confiance. Dans cet article, nous allons configurer quatre outils qui soutiennent ce workflow : Pint, Rector, Larastan et Pest.
- Pint formate le code PHP selon un style cohérent. Dans l'écosystème JavaScript, il se rapproche des règles de formatage de Prettier ou d'ESLint.
- Rector refactorise automatiquement le code PHP. Il peut moderniser la syntaxe, supprimer les constructions obsolètes et appliquer des améliorations à l'ensemble du projet.
- Larastan effectue une analyse statique adaptée à Laravel. Il détecte de nombreux problèmes potentiels avant l'exécution du code.
- Pest est un framework de test qui privilégie des tests simples et expressifs. C'est l'équivalent PHP le plus proche d'outils comme Jest dans l'écosystème JavaScript.
Utiliser ces outils dès le début rend la base de code plus facile à maintenir et fournit une fondation claire et cohérente aux futurs contributeurs. Cela nous fait également gagner du temps à mesure que le projet grandit.
Pint
Pint est préinstallé avec Laravel, il n'y a donc rien à installer. Nous devons seulement le configurer et ajouter un script Composer pour formater le code.
Créez un fichier pint.json à la racine du projet. Ce fichier nous permet de choisir les règles à appliquer à l'ensemble de la base de code.
{
"preset": "laravel",
"rules": {
"array_push": true,
"backtick_to_shell_exec": true,
"date_time_immutable": true,
"declare_strict_types": true,
"lowercase_keywords": true,
"lowercase_static_reference": true,
"final_class": true,
"final_internal_class": true,
"final_public_method_for_abstract_class": true,
"fully_qualified_strict_types": true,
"global_namespace_import": {
"import_classes": true,
"import_constants": true,
"import_functions": true
},
"mb_str_functions": true,
"modernize_types_casting": true,
"new_with_parentheses": false,
"no_superfluous_elseif": true,
"no_useless_else": true,
"no_multiple_statements_per_line": true,
"ordered_class_elements": {
"order": [
"use_trait",
"case",
"constant",
"constant_public",
"constant_protected",
"constant_private",
"property_public",
"property_protected",
"property_private",
"construct",
"destruct",
"magic",
"phpunit",
"method_abstract",
"method_public_static",
"method_public",
"method_protected_static",
"method_protected",
"method_private_static",
"method_private"
],
"sort_algorithm": "none"
},
"ordered_interfaces": true,
"ordered_traits": true,
"protected_to_private": true,
"self_accessor": true,
"self_static_accessor": true,
"strict_comparison": true,
"visibility_required": true
}
}Décomposons cette configuration :
preset: Cette option permet de choisir une configuration prédéfinie pour Pint. Ici, nous utilisons le presetlaravel, qui contient un ensemble de règles recommandées pour les projets Laravel.rules: Cette section contient les règles spécifiques que nous voulons appliquer à notre base de code. Chaque règle peut être activée ou désactivée avectrueoufalse.array_push: Force l'utilisation de la syntaxe d'ajout à un tableau plutôt que de la fusion de tableaux.backtick_to_shell_exec: Convertit les backticks en appels à la fonctionshell_exec.date_time_immutable: Force l'utilisation d'objets de date et d'heure immuables.declare_strict_types: Force l'utilisation des types stricts dans les fichiers PHP.lowercase_keywords: Force l'utilisation de mots-clés PHP en minuscules.lowercase_static_reference: Force l'utilisation de références statiques en minuscules.final_class: Force l'utilisation de classes finales.final_internal_class: Force l'utilisation de classes finales pour les classes internes.final_public_method_for_abstract_class: Force l'utilisation de méthodes publiques finales dans les classes abstraites.fully_qualified_strict_types: Force l'utilisation de types strictement qualifiés dans les fichiers PHP.global_namespace_import: Force l'import des classes, constantes et fonctions de l'espace de noms global.mb_str_functions: Force l'utilisation des fonctions de chaînes multioctets.modernize_types_casting: Force l'utilisation d'une conversion de types moderne en PHP.new_with_parentheses: Force l'utilisation de parenthèses lors de la création de nouveaux objets.no_superfluous_elseif: Supprime les instructionselseifsuperflues.no_useless_else: Supprime les instructionselseinutiles.no_multiple_statements_per_line: Force l'utilisation d'une seule instruction par ligne.ordered_class_elements: Force l'ordre des éléments d'une classe, comme les traits, les constantes, les propriétés et les méthodes.ordered_interfaces: Force l'ordre des interfaces.ordered_traits: Force l'ordre des traits.protected_to_private: Force l'utilisation de la visibilité privée pour les propriétés et les méthodes lorsque c'est possible.self_accessor: Force l'utilisation des accesseursselfpour les propriétés et les méthodes.self_static_accessor: Force l'utilisation des accesseurs statiquesself.strict_comparison: Force l'utilisation d'opérateurs de comparaison stricts.visibility_required: Force la présence des mots-clés de visibilité pour les propriétés et les méthodes.
Note
Si vous avez des questions sur l'une de ces règles, n'hésitez pas à les poser dans les commentaires.
Ajoutons maintenant une commande personnalisée pour vérifier notre code. Ajoutez une commande lint à votre fichier composer.json :
{
"scripts": {
"lint": "pint"
}
}Désormais, nous pouvons exécuter composer lint pour formater le code. Pint traitera les fichiers des dossiers app, config, database et routes afin de conserver un style cohérent.
Vous devriez obtenir quelque chose comme ceci :
➜ composer lint
> pint
✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Laravel
FIXED ......................................................................................................................... 26 files, 26 style issues fixed
✓ app/Http/Controllers/Controller.php declare_strict_types, blank_line_after_opening_tag
✓ app/Models/User.php final_class, declare_strict_types, blank_line_after_opening_tag
✓ app/Providers/AppServiceProvider.php final_class, declare_strict_types, blank_line_after_opening_tag
✓ bootstrap/app.php declare_strict_types, blank_line_after_opening_tag
✓ bootstrap/providers.php declare_strict_types, blank_line_after_opening_tag
✓ config/app.php declare_strict_types, blank_line_after_opening_tag
✓ config/auth.php final_class, declare_strict_types, blank_line_after_opening_tag
✓ config/cache.php declare_strict_types, blank_line_after_opening_tag
✓ config/database.php declare_strict_types, blank_line_after_opening_tag
✓ config/filesystems.php declare_strict_types, blank_line_after_opening_tag
✓ config/logging.php declare_strict_types, blank_line_after_opening_tag
✓ config/mail.php declare_strict_types, blank_line_after_opening_tag
✓ config/queue.php declare_strict_types, blank_line_after_opening_tag
✓ config/services.php declare_strict_types, blank_line_after_opening_tag
✓ config/session.php declare_strict_types, blank_line_after_opening_tag
✓ database/factories/UserFactory.php final_class, self_static_accessor, declare_strict_types, blank_line_after_opening_tag
✓ database/migrations/0001_01_01_000000_create_users_table.php class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
✓ database/migrations/0001_01_01_000001_create_cache_table.php class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
✓ database/migrations/0001_01_01_000002_create_jobs_table.php class_definition, declare_strict_types, blank_line_after_opening_tag, braces_position
✓ database/seeders/DatabaseSeeder.php final_class, declare_strict_types, blank_line_after_opening_tag
✓ public/index.php declare_strict_types, blank_line_after_opening_tag
✓ routes/console.php declare_strict_types, blank_line_after_opening_tag
✓ routes/web.php declare_strict_types, blank_line_after_opening_tag
✓ tests/Feature/ExampleTest.php declare_strict_types, blank_line_after_opening_tag
✓ tests/TestCase.php declare_strict_types, blank_line_after_opening_tag
✓ tests/Unit/ExampleTest.php declare_strict_types, blank_line_after_opening_tagLa sortie affiche chaque fichier et les règles que Pint lui a appliquées. Un ✓ signifie que Pint a modifié le fichier ; un . signifie qu'il était déjà correctement formaté.
Ajoutons également une commande test:lint pour la CI. Au lieu de modifier les fichiers, elle vérifiera le formatage et retournera une erreur si un fichier doit être modifié.
{
"scripts": {
"lint": "pint",
"test:lint": "pint --test"
}
}Parfait !
Rector
Le deuxième outil est Rector, qui complète Pint. Pint modifie le formatage, tandis que Rector modifie la structure et le comportement du code en fonction de ses règles. Par exemple :
class SomeClass
{
public function getValue(int $number)
{
if ($number) {
return 100;
}
return 500;
}
}Serait refactorisé ainsi :
final class SomeClass
{
public function getValue(int $number): int
{
return $number ? 100 : 500;
}
}En appliquant la règle ReturnTypeFromStrictTernaryRector.
Installons Rector avec Composer :
composer require rector/rector --devCréez un fichier de configuration rector.php :
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withPaths([
__DIR__.'/app',
__DIR__.'/bootstrap/app.php',
__DIR__.'/config',
__DIR__.'/database',
__DIR__.'/public',
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
typeDeclarations: true,
privatization: true,
earlyReturn: true,
strictBooleans: true,
)
->withPhpSets();Cette configuration est un peu plus détaillée que celle de Pint. Voici le rôle de chaque partie :
declare(strict_types=1): Cette ligne active le typage strict dans le fichier et demande à PHP d'appliquer les déclarations de type.use Rector\Config\RectorConfig: Cette ligne importe la classeRectorConfig, utilisée pour configurer Rector.return RectorConfig::configure(): Cette ligne démarre la configuration de Rector.->withPaths([...]): Cette méthode indique les chemins des dossiers et fichiers que Rector doit analyser et refactoriser. Ici, nous incluonsapp,bootstrap/app.php,config,databaseetpublic.->withPreparedSets(...): Cette méthode active des ensembles de règles prédéfinis pour Rector. Nous activons plusieurs ensembles :deadCode: Supprime le code mort du projet.codeQuality: Améliore la qualité du code en appliquant différentes bonnes pratiques.typeDeclarations: Ajoute des déclarations de type aux fonctions et aux méthodes.privatization: Rend les propriétés et les méthodes privées lorsque c'est possible.earlyReturn: Applique des retours anticipés pour améliorer la lisibilité.strictBooleans: Force des comparaisons booléennes strictes.
->withPhpSets(): Cette méthode active un ensemble de règles spécialement conçues pour PHP.
Ajoutons maintenant une commande personnalisée à composer.json :
{
"scripts": {
"refactor": "rector"
}
}Nous pouvons désormais lancer composer refactor. Rector inspectera les fichiers de app, bootstrap/app.php, config, database et public, puis appliquera les transformations configurées. Vous devriez voir une sortie similaire à celle-ci :
➜ composer refactor
> rector
20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
8 files with changes
====================
1) database/migrations/0001_01_01_000001_create_cache_table.php:12
---------- begin diff ----------
@@ @@
*/
public function up(): void
{
- Schema::create('cache', function (Blueprint $table) {
+ Schema::create('cache', function (Blueprint $table): void {
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
- Schema::create('cache_locks', function (Blueprint $table) {
+ Schema::create('cache_locks', function (Blueprint $table): void {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
----------- end diff -----------
Applied rules:
* AddClosureVoidReturnTypeWhereNoReturnRector
# ...Note
La sortie est abrégée ici. Votre terminal devrait lister tous les fichiers modifiés par Rector.
La sortie liste chaque fichier modifié, affiche un diff et indique les règles appliquées.
Ajoutons également une commande test:refactor pour la CI. Elle exécute Rector en mode simulation et signale les changements sans modifier les fichiers.
{
"scripts": {
"refactor": "rector",
"test:refactor": "rector --dry-run"
}
}Larastan
Installez Larastan avec Composer :
composer require --dev "larastan/larastan:^3.0"Créez un fichier phpstan.neon :
includes:
- vendor/larastan/larastan/extension.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
parameters:
level: 6
paths:
- app
- config
- bootstrap
- database/factories
- routesCette configuration indique à Larastan quoi charger et quelles parties du projet analyser :
includes: Cette section inclut des fichiers de configuration supplémentaires qui étendent les fonctionnalités de Larastan. Ici, nous incluonslarastan/larastan/extension.neonetphpstan/phpstan/conf/bleedingEdge.neon.parameters: Cette section contient les principaux paramètres de configuration de Larastan.level: Ce paramètre définit le niveau de rigueur de l'analyse. Les niveaux élevés détectent davantage de problèmes potentiels, mais peuvent aussi produire plus de faux positifs. Le niveau 6 constitue un compromis raisonnable pour ce projet, mais vous pouvez commencer au niveau 2 et l'augmenter progressivement.paths: Ce paramètre indique les dossiers et fichiers que Larastan doit analyser. Ici, nous incluonsapp,config,bootstrap,database/factoriesetroutes.
Ajoutons maintenant une commande personnalisée à composer.json :
{
"scripts": {
"test:types": "phpstan analyse"
}
}Contrairement à Pint et Rector, Larastan n'est pas automatique et ne peut pas corriger le code à votre place. Il signale plutôt les erreurs et les avertissements que vous devez corriger manuellement.
Lancez composer test:types pour analyser les fichiers de app, config, bootstrap, database/factories et routes. Larastan signale les problèmes à corriger manuellement. Vous devriez voir une sortie similaire à celle-ci :
➜ composer test:types
> phpstan analyse
Note: Using configuration file /Users/esoub/dev/p/mimram/phpstan.neon.
20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ ---------------------------
Line routes/console.php
------ ---------------------------
:9 Undefined variable: $this
🪪 variable.undefined
------ ---------------------------
[ERROR] Found 1 error
Script phpstan analyse handling the test:types event returned with error code 1L'erreur vient de la commande par défaut située dans routes/console.php, dont nous n'avons pas besoin pour ce projet. Supprimez-la pour corriger le problème.
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); Relancez composer test:types :
➜ composer test:types
> phpstan analyse
Note: Using configuration file /Users/esoub/dev/p/mimram/phpstan.neon.
20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
[OK] No errorsPest
Le dernier outil est Pest, un framework de test PHP qui privilégie des tests simples et expressifs. Pest est le framework de test par défaut d'un nouveau projet Laravel, il n'y a donc rien à installer.
Nous allons le configurer et ajouter quelques commandes personnalisées à composer.json.
Commençons par modifier le fichier phpunit.xml :
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Http">
<directory>tests/Http</directory>
</testsuite>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
<directory>config</directory>
<directory>routes</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>Cette configuration définit les suites de tests, les fichiers sources de l'application et l'environnement utilisé pendant les tests :
<?xml version="1.0" encoding="UTF-8"?>: Cette ligne indique la version et l'encodage XML.<phpunit ...>: Cette ligne démarre la configuration de PHPUnit.xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance": Cet attribut indique l'espace de noms XML pour l'instance de schéma.xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd": Cet attribut indique l'emplacement du schéma XML de PHPUnit.bootstrap="vendor/autoload.php": Cet attribut indique le fichier de bootstrap de PHPUnit, chargé d'importer les dépendances nécessaires.colors="true": Cet attribut active les couleurs dans la sortie de la console.<testsuites>: Cette section définit les suites de tests du projet.<testsuite name="Http">: Cette ligne définit une suite nomméeHttp.<directory>tests/Http</directory>: Cette ligne indique le dossier contenant les tests de la suiteHttp.<testsuite name="Unit">: Cette ligne définit une suite nomméeUnit.<directory>tests/Unit</directory>: Cette ligne indique le dossier contenant les tests de la suiteUnit.<source>: Cette section définit les dossiers sources du projet.<include>: Cette section inclut les dossiers indiqués dans les sources.<directory>app</directory>: Cette ligne indique le dossierappcomme dossier source.<directory>config</directory>: Cette ligne indique le dossierconfigcomme dossier source.<directory>routes</directory>: Cette ligne indique le dossierroutescomme dossier source.<php>: Cette section définit les variables d'environnement PHP du projet.<env name="APP_ENV" value="testing"/>: Cette ligne définitAPP_ENVsurtesting.<env name="APP_MAINTENANCE_DRIVER" value="file"/>: Cette ligne définitAPP_MAINTENANCE_DRIVERsurfile.<env name="BCRYPT_ROUNDS" value="4"/>: Cette ligne définitBCRYPT_ROUNDSsur4.<env name="CACHE_STORE" value="array"/>: Cette ligne définitCACHE_STOREsurarray.<env name="DB_CONNECTION" value="sqlite"/>: Cette ligne définitDB_CONNECTIONsursqlite.<env name="DB_DATABASE" value=":memory:"/>: Cette ligne définitDB_DATABASEsur:memory:.<env name="MAIL_MAILER" value="array"/>: Cette ligne définitMAIL_MAILERsurarray.<env name="PULSE_ENABLED" value="false"/>: Cette ligne désactivePULSE_ENABLED.<env name="QUEUE_CONNECTION" value="sync"/>: Cette ligne définitQUEUE_CONNECTIONsursync.<env name="SESSION_DRIVER" value="array"/>: Cette ligne définitSESSION_DRIVERsurarray.<env name="TELESCOPE_ENABLED" value="false"/>: Cette ligne désactiveTELESCOPE_ENABLED.
Ces variables donnent aux tests un environnement prévisible, ce qui limite les comportements instables et simplifie la configuration.
Comme nous avons changé le dossier des tests d'intégration de Feature à Http, renommez tests/Feature en tests/Http.
mv tests/Feature/** tests/Http/** && sed -i '' 's/Feature/Http/g' tests/Pest.phpLe dossier de tests contient maintenant deux dossiers et deux fichiers :
tests/Http: Contient les tests d'intégration de l'application.tests/Unit: Contient les tests unitaires de l'application.tests/Pest.php: Contient la configuration de Pest.tests/TestCase.php: Contient la classe de test de base de l'application.
Enfin, ajoutons quelques commandes personnalisées à composer.json :
{
"scripts": {
"test:type-coverage": "pest --type-coverage --min=100",
"test:unit": "pest --parallel --coverage --min=100",
"test": [
"@test:lint",
"@test:refactor",
"@test:types",
"@test:type-coverage",
"@test:unit"
]
}
}Ces commandes ont les rôles suivants :
test:type-coverage: Exécute les tests avec la couverture des types et exige une couverture de 100 % grâce à--min=100.test:unit: Exécute les tests avec la couverture des lignes et exige que chaque ligne soit couverte grâce à--coverageet--min=100.test: Exécute le formatage, la refactorisation, la vérification des types, la couverture des types et la suite de tests. C'est la commande que nous utiliserons dans la CI.
Pour que test:type-coverage fonctionne, nous devons installer un plugin Pest :
composer require pestphp/pest-plugin-type-coverage --devNote
La couverture de code nécessite xdebug.
Dernières réflexions
Avec Pint, Rector, Larastan et Pest configurés, le projet dispose maintenant d'un workflow qualité reproductible. Le formatage, la refactorisation, l'analyse statique et les tests peuvent nous accompagner lorsque la base de code grandit, au lieu de devenir une corvée que nous repoussons.
Les fondations Laravel sont prêtes, nous pouvons donc commencer à construire des fonctionnalités par-dessus. Ensuite, nous ajouterons l'authentification GitHub avec Socialite afin que les utilisateurs puissent s'identifier avant de gérer leurs commentaires.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.