Archives par mot-clé : PHP CS

Comment ne plus penser à PHP CS Fixer

PHP CS Fixer c’est quoi ? A quoi sert PHP CS Fixer ?

PHP CS Fixer est un outil permettant de vérifier et corriger le formatage du code PHP selon le code style défini dans la configuration du projet (fichier .php_cs.dist ou .php_cs).

Le style code est une convention qui définit comment doit être écrit le code. Par exemple l’emplacement des accolades des structures de contrôle, des classes, des fonctions, les espaces autour d’un signe égal, etc.

Cela permet de faciliter la lecture du code pour tout le monde.

Le PHP-FIG a défini plusieurs conventions dont la dernière en date est la PSR-12. Il existe également des conventions plus spécifiques comme le Coding Standards de Symfony.

Pourquoi ne plus y penser ?

Le style de code est une nécessité, mais très franchement c’est frustrant quand l’intégration continue échoue à cause du style code.

Tout comme voir des messages de commit tels que fix style code, fix cs ou cs ou bien d’autre encore.

Il existe une solution pour ne plus y penser et l’appliquer tout de même, le hook (crochet) GIT pre-commit.

Un hook (crochet en français) est un point d’extension d’une application permettant à l’utilisateur d’exécuter des actions qu’il a lui-même définies. En général, ce sont des scripts Shell.

Pré requis

Cette procédure a été écrite pour les systèmes GNU/Linux. La compatibilité pour les Mac est quasi assurée. Pour Windows, il est nécessaire d’adapter la procédure au système.

PHP 7 et PHP CS Fixer doivent être installé sur le poste qui exécute les commandes GIT. Si PHP CS Fixer n’est pas installé, suivre la procédure d’installation globale manuelle.

Pour ce blog post, PHP CS Fixer est installé à cet emplacement /usr/local/bin/php-cs-fixer. Pour connaitre l’emplacement du binaire exécuter la commande type php-cs-fixer.

Le projet sur lequel vous choisissez de définir ce hook contient déjà une configuration pour PHP CS Fixer (fichier .php_cs.dist ou .php_cs).

Ajout d’un hook sur un dépôt

Pour ne pas perturber les projets qui ne dispose pas de configuration ou qui n’ont pas de fichier PHP, le hook vérifie la présence du fichier de configuration .php_cs.dist et n’appliquera les modifications que sur les fichiers PHP inclus dans le commit.

Pour l’ajouter, se placer dans le dossier de votre projet puis ouvrir le fichier .git/hooks/pre-commit (il peut déjà exister ou devra être ajouté).

Puis coller le contenu du script suivant :

#!/bin/sh

ROOT=$(dirname "$0")
ROOT=$(dirname "$ROOT")
ROOT=$(dirname "$ROOT")

echo "php-cs-fixer pre commit hook start"

# Modifier le chemin de PHP CS Fixer ici si votre binaire n'est pas installé au même endroit :
PHP_CS_FIXER="/usr/local/bin/php-cs-fixer"

HAS_PHP_CS_FIXER=false

if [ -x  "$PHP_CS_FIXER" ]; then
    HAS_PHP_CS_FIXER=true
else
    echo "PHP CS Fixer not installed into $PHP_CS_FIXER"
fi

#PHP_CS_CONFIG=

for file in .php_cs.dist .php_cs
do
    if [ -f "$ROOT/$file" ]; then
        echo PHP CS Fixer config file found in projet at $file
        PHP_CS_CONFIG=$file
    fi
done

if [ "x$PHP_CS_CONFIG" = "x" ]; then
    echo "No PHP CS Fixer config file found !"
    HAS_PHP_CS_FIXER=false
fi

if $HAS_PHP_CS_FIXER; then
    git status --porcelain | grep -e '^[AM]\(.*\).php$' | cut -c 3- | while read line; do
        $PHP_CS_FIXER fix --config=$ROOT/$PHP_CS_CONFIG --verbose "$line";
        git add "$line";
    done
else
    echo ""
    echo "Please install php-cs-fixer, see:"
    echo ""
    echo "  https://github.com/FriendsOfPHP/PHP-CS-Fixer#installation"
    echo ""
fi

echo "php-cs-fixer pre commit hook finish"

Il est nécessaire de s’assurer que ce fichier est exécutable grâce à cette commande : chmod u+x .git/hooks/pre-commit.

Exécuter la commande dans un terminal après s’être placé dans le dossier de votre projet.

Pour tester son efficacité, modifier le placement d’accolade dans un fichier PHP du projet puis ajouter un commit.

PHP CS Fixer est exécuté sur chaque fichier modifié pour corriger le style code avant le commit.

Ajout d’un hook pour tous les dépôts

Maintenant que notre hook fonctionne, nous pouvons l’ajouter au modèle de dépôt GIT. Ainsi tout nouveau dépôt disposera du hook.

La première étape consiste à définir un dossier de template pour les nouveaux dépôts GIT.

Exécuter la commande git config --global init.templatedir '~/.git-templates'

Le dossier n’existant pas, nous allons l’ajouter à notre dossier utilisateur avec cette commande mkdir -p ~/.git-templates/hooks

Toujours depuis le dossier du projet où le hook a été ajouté exécuter la commande suivante pour copier le hook cp .git/hooks/pre-commit ~/.git-templates/hooks/

Cette fois, nous allons nous assurer que tout le monde peut l’exécuter avec cette commande : chmod a+x ~/.git-templates/hooks/pre-commit.

Ajouter le hook sur un dépôt existant

Maintenant que le hook est dans le template de GIT, il est possible de l’ajouter à un projet existant en exécutant la commande : git init dans le dossier du projet.

Pour les dépôts ayant déjà un hook pre-commit, il ne sera pas modifié.

Besoin d’aide pour mettre en place cette solution ? Contactez-moi !

Il est nécessaire de s’assurer que ce fichier est exécutable grâce à cette commande : chmod u+x .git/hooks/pre-commit.

Exécuter la commande dans un terminal après s’être placé dans le dossier de votre projet.

Pour tester son efficacité, modifier le placement d’accolade dans un fichier PHP du projet puis ajouter un commit.

PHP CS Fixer est exécuté sur chaque fichier modifié pour corriger le style code avant le commit.

Nouvelle erreur ValueError dans PHP 8 (partie 3)

Dans la continuité du précédent article sur PHP 8, voici une modification qui aura une incidence sur notre code et son bon fonctionnement sur cette nouvelle version majeure de PHP.

Fournir un objet en argument à la place d’un entier provoque une exception TypeError, mais que se passe-t-il si le type est correct, mais que la valeur ne l’est pas ?
À partir de PHP 8 un grand nombre de méthodes lèveront une exception ValueError.

Cette modification aura plus de conséquences sur le code devant être compatible avec PHP 7 et PHP 8 et dont la valeur des arguments est fournie par l’utilisateur ou le développeur.

La solution la plus simple est de valider la donnée avant son usage. Par validation, je recommande la vérification du type de donnée, mais également de son contenu (regex, liste exhaustive des valeurs acceptées, nettoyeur HTML par exemple). La vérification aura également un avantage en termes de sécurité, mais j’en reparlerais sûrement dans un autre bloc post.

Concrètement, à quoi faut’il s’attendre dans PHP 8 ?

Voici comment réagit la fonction password_hash avec un argument invalide dans PHP 8 :

Interactive shell

php > echo phpversion();
8.0.0-dev
php > 
php > $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);

Warning: Uncaught ValueError: Invalid number of threads in php shell code:1
Stack trace:
#0 php shell code(1): password_hash('test', 'argon2i', Array)
#1 {main}
  thrown in php shell code on line 1
php >

Ici, je passe un nombre incorrect de fils d’exécution à utiliser pour le calcul de la somme de contrôle du mot de passe. PHP lève une exception spécifique de type ValueError.

Dans votre code, si vous réalisez des tests similaires à if (is_string($return) === false) {}, il sera nécessaire de capturer l’exception par une structure de code try catch pour gérer les cas d’erreur en PHP 8.

<?php
try {
    $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);
} catch (ValueError $e) {
    var_dump($e);
}

Donne la sortie suivante :

object(ValueError)#3 (7) {
  ["message":protected]=>
  string(25) "Invalid number of threads"
  ["string":"Error":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(14) "php shell code"
  ["line":protected]=>
  int(1)
  ["trace":"Error":private]=>
  array(1) {
    [0]=>
    array(4) {
      ["file"]=>
      string(14) "php shell code"
      ["line"]=>
      int(1)
      ["function"]=>
      string(13) "password_hash"
      ["args"]=>
      array(3) {
        [0]=>
        string(4) "test"
        [1]=>
        string(7) "argon2i"
        [2]=>
        array(1) {
          ["threads"]=>
          int(0)
        }
      }
    }
  }
  ["previous":"Error":private]=>
  NULL
}

Maintenant, comment rendre notre code compatible avec PHP 7 et 8 ?

Voici un exemple de code :

<?php
try {
    $return = password_hash('test', PASSWORD_ARGON2I, ['threads' => 0]);

    // Test du retour de la fonction
    if (is_string($return) === false) {
        throw new ValueError('Error with password_hash function, see the PHP logs to more details.');
    }
} catch (ValueError $e) {
    var_dump($e);
    // Traitement de l'erreur
}

Pour PHP 7, il est nécessaire de définir la classe ValueError, car elle n’existe pas. Cependant, plutôt que définir cette classe dans tous vos projets, utilisez la librairie polyfill pour PHP 8.0 réalisée par Symfony.

La mise à jour du code est donc possible dès maintenant pour nos projets et nos librairies.

Après avoir aperçu la modification du comportement, voici la liste des méthodes PHP qui peuvent émettre une exception ValueError :

array_chunk
array_combine
array_fill
array_multisort
array_pad
array_rand
assert_options
base_convert
bcadd
bccomp
bcdiv
bcmod
bcmul
bcpow
bcpowmod
bcscale
bcsqrt
bcsub
bzopen
bzread
cal_days_in_month
cal_from_jd
cal_info
cal_to_jd
count
count_chars
deflate_add
deflate_init
dirname
dns_check_record
easter_date
enchant_broker_free
explode
extract
fgetcsv
fgets
file
file_get_contents
flock
fputcsv
fread
fscanf
ftruncate
func_get_arg
gettype
gmp_pow
gzcompress
gzdeflate
gzencode
gzinflate
gzread

gzuncompress
hash
hash_file
inflate_add
inflate_init
jdtojewish
jdtounix
json_decode
log
max
mb_check_encoding
mb_chr
mb_convert_case
mb_convert_encoding
mb_convert_kana
mb_convert_variables
mb_decode_numericentity
mb_detect_encoding
mb_detect_order
mb_encode_numericentity
mb_encoding_aliases
mb_internal_encoding
mb_language
mb_ord
mb_preferred_mime_name
mb_regex_encoding
mb_strcut
mb_strimwidth
mb_stripos
mb_stristr
mb_strlen
mb_strpos
mb_strrchr
mb_strrichr
mb_strripos
mb_strrpos
mb_str_split
mb_strstr
mb_strtolower
mb_strtoupper
mb_strwidth
mb_substitute_character

mb_substr
mb_substr_count
min
mt_rand
parse_url
password_hash
pow
print
printf
proc_open
putenv
range
ReflectionParameter
scandir
settype
socket_connect
socket_create_pair
socket_send
socket_sendto
socket_set_option
spl_autoload_register
sprintf
sscanf
stream_context_get_default
stream_filter_register
stream_select
stream_set_chunk_size
stripos
strncasecmp
strncmp
str_pad
strpbrk
strpos
strripos
strrpos
str_split
str_word_count
substr_compare
substr_count
test
trigger_error
unixtojd
unserialize
version_compare
vfprintf
wordwrap
zip_close

Cette liste n’est pas exhaustive. Elle a été réalisée sur la base des tests de PHP.

Conclusion

Autant que possible, préparons dès maintenant la migration vers PHP 8 en modifiant notre code de façon à faciliter la migration.

Les tests sur PHP 8.0 ont été réalisés avec l’image docker php8-jit de Keinos. Merci pour le partage.

Restez à l’écoute, je vais réaliser d’autre article sur les modifications apportées par PHP 8 ayant une incidence sur notre code.

Migration de code vers PHP 8 MBString (partie 2)

Dans la continuité du précédent article sur PHP 8, voici les modifications de l’extension multi-byte string qui peuvent avoir une incidence sur notre code et son bon fonctionnement sur cette nouvelle version majeure de PHP.

Alias de fonction ou fonctions supprimées

  • mbregex_encoding -> mb_regex_encoding
  • mbereg -> mb_ereg
  • mberegi -> mb_eregi
  • mbereg_replace -> mb_ereg_replace
  • mberegi_replace -> mb_eregi_replace
  • mbsplit -> mb_split
  • mbereg_match -> mb_ereg_match
  • mbereg_search -> mb_ereg_search
  • mbereg_search_pos -> mb_ereg_search_pos
  • mbereg_search_regs -> mb_ereg_search_regs
  • mbereg_search_init -> mb_ereg_search_init
  • mbereg_search_getregs -> mb_ereg_search_getregs
  • mbereg_search_getpos -> mb_ereg_search_getpos
  • mbereg_search_setpos -> mb_ereg_search_setpos

func_overload supprimée

La directive de configuration INI mbstring.func_overload a été supprimée avec les constantes afférentes MB_OVERLOAD_MAIL, MB_OVERLOAD_STRING, et MB_OVERLOAD_REGEX.
Tout comme la récupération des informations func_overload et func_overload_list via la méthode mb_get_info.

Autres modifications de comportement

  • Le tableau de résultat passé en référence dans l’argument &$result est maintenant obligatoire pour la fonction mb_parse_str.
  • Le modificateur e pour la fonction mb_ereg_replace a été supprimé. Il est nécessaire d’utiliser la fonction mb_ereg_replace_callback à la place.
  • Si la valeur de l’argument pattern de la fonction mb_ereg_replace n’est pas une chaîne de caractère, il sera transformé en chaine de caractère. Pour retrouver le comportement de PHP 7, il est nécessaire d’ajouter explicitement la fonction chr. Ce comportement ne peut pas être détecté par l’analyse statique du code.
  • La valeur de l’argument needle des fonctions suivantes mb_strpos, mb_strrpos, mb_stripos, mb_strripos, mb_strstr, mb_stristr, mb_strrchr et mb_strrichr ne peuvent plus être vides. Ce comportement ne peut pas être détecté par l’analyse statique du code.
  • Il n’est plus possible de fournir l’encodage en 3e argument à la fonction mb_strrpos. Il est obligatoire de saisir 0 pour la valeur de l’argument offset.

Conclusion

Comme pour de nombreuses améliorations, il est possible de détecter les modifications à apporter grâce à l’analyse statique. Certaines modifications peuvent même être réalisées automatiquement avec un outil de migration tel que Rector.

Restez à l’écoute, je vais réaliser d’autre article sur les modifications apportées par PHP 8 ayant une incidence sur notre code.