Programmation PHP/Analyse statique de programmes

PHP offre plusieurs outils d'analyse statique de programmes (ou analyse de code statique).

microtime() et memory_get_usage()

modifier

Pour mesurer un temps d'exécution dans le code, on peut utiliser des fonctions natives :

$startTime = microtime(true);
$startMemory = memory_get_usage(true);

maFonctionMesurée();

$endTime = microtime(true);
$endMemory = memory_get_usage(true);

echo sprintf(
    'L\'exécution a pris %1$f secondes et %2$f mémoire',
    number_format($endTime - $startTime),
    number_format($endMemory - $startMemory)
);


PHP_CodeSniffer liste ou corrige les violations des normes de codage[1][2].

Installation

modifier
composer require squizlabs/php_codesniffer --dev

Lancement

modifier
vendor/bin/phpcs --standard=PSR12 src

NB : le fait de préciser l'utilisation de la PSR12 évite les erreurs inutiles sur les espaces autour des points de concaténation.

Configuration

modifier
  • phpcs : liste les mauvaises pratiques.
  • phpcbf : corrige celles qui le sont automatiquement.
  • ruleset.xml : liste des vérifications à vérifier ou à exclure (ce qui évite de tout préciser en argument de la commande).

Pour ignorer une erreur, ajouter un commentaire :

  • Sur un fichier : // phpcs:ignoreFile
  • Sur une ligne : // phpcs:ignore

Configuration

modifier

Voici un exemple de fichier phpcs.xml.dist :

<?xml version="1.0" encoding="UTF-8"?>

<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">

    <arg name="basepath" value="."/>
    <arg name="cache" value=".phpcs-cache"/>
    <arg name="colors"/>
    <arg name="extensions" value="php"/>

    <rule ref="PSR12">
        <exclude name="PSR12.Operators.OperatorSpacing" />
    </rule>

    <file>bin/</file>
    <file>config/</file>
    <file>public/</file>
    <file>src/</file>
    <file>tests/</file>
</ruleset>

phpinsights

modifier

Ajoute declare(strict_types=1); dans tous les fichiers, et formate code code et le style.

Installation

modifier
composer require nunomaduro/phpinsights --dev

Lancement

modifier

Pour la vérification :

vendor/bin/phpinsights analyse src -n

Pour réparer automatiquement, ajouter "--fix".

Configuration

modifier

Voici un exemple de fichier phpinsights.php :

<?php

declare(strict_types=1);

return [
    'preset' => 'symfony',
    'ide' => 'phpstorm',
    'exclude' => [
    ],
    'add' => [
    ],
    'remove' => [
        \PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures\DisallowYodaConditionsSniff::class,
        \PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff::class,
        \SlevomatCodingStandard\Sniffs\ControlStructures\DisallowYodaComparisonSniff::class,
        \SlevomatCodingStandard\Sniffs\ControlStructures\RequireYodaComparisonSniff::class,
    ],
    'config' => [
    ],
    'requirements' => [
        'min-quality' => 50,
        'min-complexity' => 50,
        'min-architecture' => 50,
        'min-style' => 50,
    ],
    'threads' => null,
];

PHP Mess Detector recense les mauvaises pratiques du type "code mort", paramètre inutilisé, mauvais nommage camelCase, présence d'un exit, etc.[3].

Installation

modifier
composer require  phpmd/phpmd --dev

Lancement

modifier
vendor/bin/phpmd src ansi rulesets.xml

Configuration

modifier

Comme CodeSniffer, il utilise un fichier XML pour lister les vérifications à inclure ou exclure.

Exemple de fichier rulesets.xml :

<?xml version="1.0"?>
<ruleset name="Custom ruleset"
         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>
        My custom rule set that checks my code...
    </description>

    <rule ref="rulesets/codesize.xml">
        <exclude name="CyclomaticComplexity" />
        <exclude name="NPathComplexity" />
        <exclude name="ExcessiveMethodLength" />
        <exclude name="ExcessiveParameterList" />
    </rule>
    <rule ref="rulesets/codesize.xml/CyclomaticComplexity">
        <properties>
            <property name="reportLevel" value="20" />
        </properties>
    </rule>
    <rule ref="rulesets/codesize.xml/NPathComplexity">
        <properties>
            <property name="minimum" value="600" />
        </properties>
    </rule>
    <rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
        <properties>
            <property name="minimum" value="110" />
            <property name="ignore-whitespace" value="true" />
        </properties>
    </rule>
    <rule ref="rulesets/codesize.xml/ExcessiveParameterList">
        <properties>
            <property name="minimum" value="14" />
        </properties>
    </rule>
    <rule ref="rulesets/cleancode.xml">
        <exclude name="ElseExpression" />
        <exclude name="IfStatementAssignment" />
        <exclude name="StaticAccess" />
        <exclude name="BooleanArgumentFlag" />
    </rule>
    <rule ref="rulesets/controversial.xml" />
    <rule ref="rulesets/design.xml">
        <exclude name="CouplingBetweenObjects" />
    </rule>
    <rule ref="rulesets/design.xml/CouplingBetweenObjects">
        <properties>
            <property name="maximum" value="20" />
        </properties>
    </rule>
    <rule ref="rulesets/naming.xml">
        <exclude name="ShortVariable" />
        <exclude name="LongVariable" />
    </rule>
    <rule ref="rulesets/unusedcode.xml" />

    <exclude-pattern>src/DataFixtures</exclude-pattern>
    <exclude-pattern>src/Migrations</exclude-pattern>
</ruleset>

De plus, pour exclure une classe d'une analyse, on peut placer le nom de l'analyse dans une annotation de la classe. Ex :

@SuppressWarnings(PHPMD.CyclomaticComplexity)

Ou tout ignorer :

@SuppressWarnings(PHPMD)

phpmetrics

modifier

Outil d'analyse fournissant un rapport graphique.

Installation

modifier
composer require phpmetrics/phpmetrics --dev

Lancement

modifier
vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src

Configuration

modifier

Éviter de versionner les rapports avec l'application. Par exemple ajouter "reports" au .gitignore.

phpstan

modifier

PHP Static Analysis détecte des erreurs potentielles à l'exécution (ex : mauvais type) sans réellement exécuter le code[4].

Installation

modifier
composer require phpstan/phpstan --dev

Voir plus pour vérifier l'utilisation de certaines dépendances :

composer require phpstan/phpstan-phpunit --dev
composer require phpstan/phpstan-symfony --dev
composer require phpstan/phpstan-doctrine --dev

Lancement

modifier
vendor/bin/phpstan analyse --no-progress --memory-limit=256M src

Configuration

modifier

Pour ignorer une erreur sur un élément, il y a trois solutions :

  • utiliser une annotation sur la ligne précédente :
/** @phpstan-ignore-next-line */
  • ou sur la même ligne :
/** @phpstan-ignore-line */
  • ou préciser dans le fichier de configuration (phpstan.neon), le dossier à exclure de l'analyse ou le message d'erreur à ignorer.

Exemple de fichier phpstan.neon :

includes:
	- 'vendor/phpstan/phpstan-symfony/extension.neon'
	- 'vendor/phpstan/phpstan-phpunit/extension.neon'
	- 'vendor/phpstan/phpstan-doctrine/extension.neon'

parameters:
	level: 7
	paths:
		- src
	excludePaths:
		- src/Migrations/*
	reportUnmatchedIgnoredErrors: false
	inferPrivatePropertyTypeFromConstructor: true
	checkMissingIterableValueType: false
	checkGenericClassInNonGenericObjectType: false
	ignoreErrors:
		- '#Cannot assign offset [^ ]+ to [^\.]+.#'
		- '#Call to an undefined method [a-zA-Z0-9\\_]+::getItem\(\)#'
	    - '#Call to an undefined method Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface::add\(\)#'

Le champ "level" va de 0 (violations les plus graves) à 9 (bonnes pratiques idéales). Donc plus il est haut et plus il y a de vérifications[5].

Psalm cherche à détecter des bugs, par exemple des erreurs de type[6].

Installation

modifier
composer require --dev vimeo/psalm
vendor/bin/psalm --init

Lancement

modifier
vendor/bin/psalm

Configuration

modifier

Pour ignorer une erreur, ajouter l'annotation de la fonction concernée :

/** @psalm-suppress UndefinedConstant */


En effet, les métriques telles que les taux de duplication ou de couverture du code (par les tests automatiques) sont révélatrices de la qualité du projet. Pour les mesurer il existe plusieurs outils :

php-scrutinizer

modifier

Comprend des analyses de sécurité et de performances[7].

GrumPHP

modifier

Idem[8].

composer require --dev phpro/grumphp
 

Il s'agit d'un logiciel de qualimétrie dont la version gratuite supporte PHP.

Il nécessite trois composants :

  • le serveur, qui stocke les analyses et permet de les visualiser sur un dashboard. Il est téléchargeable gratuitement, ou utilisable dans sa version payante SonarCloud pour ne pas avoir à héberger le serveur soi-même. Identifiant par défaut : admin / admin.
  • le scan, client qui envoie les analyses au serveur. A l'instar du serveur, il est disponible au téléchargement en multi-plateforme (via Java) ou sur Docker.
  • un plugin PhpStorm (SonarLint) pour suggérer des améliorations en cours de frappe.
 Afin de ne pas versionner le cache du scan, il convient d'ajouter ceci au .gitignore du projet :
/.scannerwork/

Exemples

modifier

Pour lancer le scan sur un projet Symfony installé sur Windows (dans un .cmd) :

REM Copy this file in the projet to scan, or add: -D"sonar.projectBaseDir=C:\myProjectPath" ^
C:\sonar-scanner\bin\sonar-scanner.bat ^
    -D"sonar.projectKey=MY_PROJECT_KEY" ^
    -D"sonar.login=MY_PROJECT_LOGIN" ^
    -D"sonar.host.url=http://localhost:9000" ^
    -D"sonar.sourceEncoding=UTF-8" ^
    -D"sonar.language=php" ^
    -D"sonar.sources=src" ^
    -D"sonar.tests=tests"
pause

Sur Docker :

docker run --rm \
  -v "/$(pwd)/":/var/www/symfony \
  -w "/var/www/symfony" \
  -e SONAR_HOST_URL="http://sonar-server.localhost:9000" \
  -e SONAR_LOGIN="MY_PROJECT_LOGIN" \
  sonarsource/sonar-scanner-cli \
    -Dsonar.projectKey="MY_PROJECT_KEY" \
    -Dsonar.sourceEncoding=UTF-8 \
    -Dsonar.language=php \
    -Dsonar.sources=src \
    -Dsonar.tests=tests

Pour remplir le taux de couverture de code, il faut ajouter le lien vers un rapport PhpUnit :

   -Dsonar.php.tests.reportPath=reports/phpunit.logfile.xml \
   -Dsonar.php.coverage.reportPaths=reports/phpunit.coverage.xml

Makefile d'analyse

modifier

Pour éviter de retenir toutes ces commandes, on peut les inclure dans le composer.json à la rubrique "scripts", ou dans un Makefile.

Exemple sur Symfony :

all: lint phpcs-check phpinsights-check phpmd phpstan-check psalm

lint:
	bin/console lint:container
	bin/console lint:twig templates
	bin/console lint:yaml config

phpcs-fix:
	vendor/bin/phpcbf --standard=PSR12 --ignore=Migrations src/

phpcs-check:
	vendor/bin/phpcs --standard=PSR12 --ignore=Migrations src/

phpinsights-check:
	vendor/bin/phpinsights analyse src -n

phpmd:
	vendor/bin/phpmd src ansi rulesets.xml

phpmd-report:
	vendor/bin/phpmd src html rulesets.xml --reportfile reports/phpmd-$$(date '+%Y%m%d_%H%M%S').html

phpmetrics:
	vendor/bin/phpmetrics --report-html=reports/phpmetrics-$$(date '+%Y%m%d_%H%M%S').html src

phpstan:
	vendor/bin/phpstan analyse src/

phpstan-check:
	vendor/bin/phpstan analyse --no-progress --memory-limit=256M src/

psalm:
	vendor/bin/psalm --no-cache

Références

modifier