Programmation PHP/Concevoir du code de haute qualité

Un livre de Wikilivres.

Introduction[modifier | modifier le wikicode]

La programmation est accessible à tout le monde. En revanche, créer du code de qualité demande une rigueur et une organisation qui n'est pas toujours au rendez-vous. Voici donc un tutoriel pour apprendre à programmer du code de haute qualité. Les exemples seront pris au PHP mais leur application est toujours valable quel que soit le langage de programmation.

Pourquoi bien programmer ?[modifier | modifier le wikicode]

Il est important d'aborder cette question dès le début pour légitimer ce cours, la motivation étant un facteur non négligeable de l'apprentissage.

Le bien-programmer est tout d'abord indispensable pour travailler en équipe. Lorsque vous travaillez dans un projet avec d'autres développeurs, il est important de coordonner votre action, d'éditer du code lisible... ce qui augmentera considérablement votre rendement de travail.

Vous vous apercevrez rapidement qu'un code bien programmé simplifie la vie : Lorsque vous avez créé un script une année auparavant et que vous devez vous replonger dessus, vous serez heureux de gagner du temps en retrouvant plus facilement le sens du code grâce à sa lisibilité. De plus, du bon code est plus compatible et générera moins d'erreurs ...

La quantité toujours grandissante de développeurs risque d'entraîner une saturation du marché. Vous serez jugés sur votre qualité de programmation. Un code aux normes est un sceau garantissant votre compétence de développeur.

Les critères de qualité[modifier | modifier le wikicode]

La programmation doit posséder des qualités qui ne sont pas arbitraires. Chaque critère sera accompagné d'une rapide description de son utilité.

La portabilité[modifier | modifier le wikicode]

La portabilité du code est son degré d'indépendance à son environnement.

Cette qualité est surtout requise pour des scripts destinés à être utilisés par le grand public. Par exemple, lorsqu'en PHP, vous utilisez des balises <?, vous réduisez la portabilité du code. En effet, l'usage des balises de ce type nécessite l'activation de la fonction SHORT_TAGS, donc dépend de l'environnement du script. Par définition, cela réduit la portabilité du code. Il vaut mieux utiliser la balise <?php qui marche quel que soit l'environnement.

La lisibilité[modifier | modifier le wikicode]

Il n'est pas besoin de chercher loin pour trouver les avantages que confèrent un code lisible : il permet aux autres développeurs de modifier plus facilement votre script, mais également à vous-même de comprendre plus rapidement un de vos anciens programmes. La lisibilité du code permet également d'éviter les erreurs de logique qui apparaissent plus distinctement.

La lisibilité est très liée à la définition de normes.

La lisibilité du code inclut un bon usage de la commentarisation. Les commentaires sont faits pour augmenter la rapidité de compréhension du script par le développeur, mais il ne doit pas en être fait un usage excessif, lequel serait néfaste pour la lisibilité. Voici un exemple de mauvaise utilisation des commentaires :

/* Code qui va afficher "salut" par un echo. Ce programme est fait en PHP compatible php3*/
echo "salut"; //affiche "salut"
#fin du programme

Il devient difficile de distinguer le code parmi les commentaires. Cet exemple était volontairement exagéré, mais n'est pas si loin de quelques scripts que l'on peut trouver sur le Net.

La définition de normes[modifier | modifier le wikicode]

Les normes sont des décisions le plus souvent arbitraires sur des méthodes de programmation. L'important n'est pas ce que définissent les normes, mais que des normes soient définies.

La définition de normes confère une continuité logique au code. Par exemple, en PHP, il est possible de nommer des variables de deux manières différentes :

$maSuperVariable   // Écriture en "CamelCase"
$ma_super_variable // Écriture avec des underscores

Vous serez rapidement désorienté si tantôt vous utilisez une méthode, tantôt une autre, et il se peut que vous perdiez des heures à chercher la cause d'une erreur d'écriture de variable.

De plus, lorsque vous travaillez en commun avec d'autres développeurs, si vous n'avez pas défini le nom du fichier de connexion à la base de données, lors de la fusion des scripts, vous devrez tout remodifier et perdre ainsi un temps précieux.

L'unicité du code (DRY)[modifier | modifier le wikicode]

L'unicité du code (ou DRY pour "Don't repeat yourself", Ne vous répétez pas) est le principe selon lequel aucun code ne doit être double dans le script. Ce critère a un enjeu pratique et vise à augmenter la rapidité des modifications d'un script. Prenons un exemple :

Vous faites un programme de comptabilité d'entreprise et vous vous connectez à une base de données. Si dans chaque page où vous vous connectez, vous entrez le code suivant :

<?php
$connect = mysql_connect('host','account','password');

//actions sur la BDD

Le jour où vous voudrez changer le mot de passe de la base de données, vous devrez modifier chaque script, tâche qui s'avère laborieuse. Si en revanche lorsque vous vous connectez à la base de données vous entrez le code suivant :

include 'connect.php';

Et que dans le fichier connect.php vous entrez les informations de votre base de données, vous n'aurez qu'à modifier ce seul fichier lors d'un changement de mot de passe.

La duplication de code est donc considérée comme un anti-patron de programmation.

Exemple concret[modifier | modifier le wikicode]

Créez un fichier conf/config.php dans lequel vous mettez vos informations de connexion à la base de données :

define('HOST', 'localhost');
define('USER', 'moi');
define('PASS', 'mon_mdp');
define('DB', 'mon_site');
define('PREFIX', 'mon_site_'); // Préfixe des tables SQL

Lorsque vous ferez une requête, vous la ferez de la manière suivante :

mysql_connect(HOST, USER, PASS);
mysql_select_db(DB);

$temp = mysql_query('SELECT * FROM '. PREFIX .'ma_table');

La gestion des erreurs[modifier | modifier le wikicode]

Votre programme ne doit pas afficher au client de message d'erreur. Cela ne signifie pas non plus qu'il faille les étouffer. Il faut, par du code de qualité, réduire au maximum les erreurs possibles, puis traiter les autres en les enregistrant par exemple dans un fichier.

Voir le chapitre suivant pour la mise en œuvre : Exceptions.

Les conflits de critères[modifier | modifier le wikicode]

Réaliser un programme rassemblant toutes les qualités présentées précédemment est extrêmement difficile. La plupart du temps, certains critères entrent en conflit, par exemple entre la compatibilité et la lisibilité. Il va alors falloir établir une hiérarchie.

Il n'existe pas de hiérarchie absolue. Elle dépend du type de projet que vous menez. Voici quelques exemples de hiérarchisation de critères en fonction du projet :


Projet destiné au grand public[modifier | modifier le wikicode]

Par exemple, vous décidez un jour de concevoir un CMS. Les qualités requises seront :

  • La portabilité du code (car le CMS doit fonctionner sous le plus grand nombre de serveurs, donc doit dépendre le moins possible de sa configuration)
  • La gestion des erreurs (sachant que les personnes qui vont utiliser le script ne l'ont pas fait, il ne doit pas retourner de message d'erreur car ils ne pourraient pas l'arranger)
  • La définition de normes (vous le programmerez certainement en équipe, donc il faut coordonner votre action)
  • La lisibilité du code (toujours pour des raisons de coordination)

Script pour un particulier[modifier | modifier le wikicode]

Si on vous demande de programmer un système de restriction d'accès pour un site particulier, il va falloir que le code possède les critères suivants :

  • La lisibilité du code (un autre développeur doit pouvoir facilement modifier votre script si le client le lui demande)

Erreurs à éviter[modifier | modifier le wikicode]

Attention à ne pas vous laisser avoir par des idées fausses d'autant plus dangereuses qu'elles sembleraient logiques. En voici quelques exemples :

Le code optimisé[modifier | modifier le wikicode]

Parfois le code le plus court n'est pas le plus rapide d'exécution[1]. En voici un exemple :

//Code le plus court
for ($i = 0; $i < count($array); $i++) {
    echo $array[$i];
}

//Code le plus rapide
$count = count($array);
for ($i = 0; $i < $count; $i++) {
    echo $array[$i];
}

En effet, avec le premier code, à chaque itération, la taille du tableau est recalculée, ce qui ralentit le script. Le code le plus court n'est donc pas le plus rapide d'exécution.

Guillemets simples pourquoi ?[modifier | modifier le wikicode]

Toutes les chaînes peuvent être écrites autant avec des guillemets simples ('foo') qu'avec des guillemets doubles ("bar"). Cependant, on conseille généralement d'employer les guillemets simples parce qu'ils sont plus rapides[2]. En voici la raison : à l'intérieur des guillemets doubles, les variables sont interprétées et substituées correctement.

Exemple :

echo "Votre nom est $nom et vous êtes $etatUser. Il vous reste $pointAction point(s) d'action.";
//Fait ce qu'on s'attend qu'il fasse. Alors que :
echo 'Votre nom est $nom et vous êtes $etatUser. Il vous reste $pointAction point(s) d\'action.';
//écrira bêtement la chaine avec les nom des variables. Il faudra additionner les chaines ensembles :
echo 'Votre nom est '. $nom .' et vous êtes '. $etatUser .'. Il vous reste '. $pointAction .' point(s) d\'action.';

Si à première vue ça semble être un avantage pour les guillemets doubles et qu'il est vrai que dans certaines conditions particulières, ça puisse augmenter la lisibilité du code, ça a aussi son désavantage : chacune des chaînes écrites avec des guillemets doubles doit d'abord être traversée par PHP pour y chercher de telles variables. En y regardant bien, le plus souvent c'est à des endroits où il n'y a aucune chance que se retrouvent de telles variables. Cette opération est en soi extrêmement rapide, presque qu'imperceptible, mais les chaînes se retrouvent souvent dans des endroits clés : des boucles, des fonctions exécutées des milliers des fois. Par ailleurs, même si on doit ajouter des variables dans une chaîne, ce sont les guillemets simples qui seront les plus rapides (Sauf cas extrême : echo "$a - $b,$c + $d $e $sigma $epsilon";). C'est pourquoi on conseille de s'habituer et d'employer les guillemets simples le plus souvent possible.

Par ailleurs, lorsqu'on avance dans le niveau de programmation avec les objets et les tableaux (qui ne sont pas interprétés correctement dans les guillemets doubles), les occasions de se servir de ceux-ci vont en s'amenuisant considérablement.

Logo

En cas de migration des guillemets vers les apostrophes, il faut remplacer tous les retours à la ligne \n qui ne sont plus interprétés, par des <br/>.

Conventions de codage[modifier | modifier le wikicode]

Condition Yoda[modifier | modifier le wikicode]

Lors d'une comparaison entre une variable et un littéral, on place ce dernier en premier. Ex : if (1 == $x). Cette technique sert à éviter de confondre les assignations avec les égalités, et les déréférencements de pointeurs null.

PSR[modifier | modifier le wikicode]

La programmation objet en PHP est régie par des recommandations nommées PSR (pour PHP Standards Recommendations) publiées sur http://www.php-fig.org/psr/.

En 2022, 13 de ces recommandations ont été acceptées officiellement :

PSR-1 : conventions de codages basiques[modifier | modifier le wikicode]

Les voici résumées ici[3] :

  • Un fichier .php doit être encodé en UTF8 sans BOM.
  • Les noms de classe doivent être rédigés en StudlyCaps (commencer par une majuscule).
  • Les noms des variables et méthodes de classe doivent être écris en camelCase (en commençant par une minuscule).
  • Les noms des constantes doivent être en lettres capitales et snake_case (en séparant les mots par des underscores). Comme par exemple la native DIRECTORY_SEPARATOR.

PSR-3 : interface du logger[modifier | modifier le wikicode]

Définit une méthode par niveau de criticité du log :

Numéro Graylog Niveau
0 emergency
1 alert
2 critical
3 error
4 warning
5 notice
6 informational
7 debug
 Généralement la production n'affiche pas les debug.

PSR-4 : Autoloading[modifier | modifier le wikicode]

Régit le fait que les séparateurs dans les namespaces, et les underscores dans les noms de classe, représentent des séparateurs de dossier du système de fichier.

PSR-6 : interface du cache[modifier | modifier le wikicode]

PSR-7 : interface des messages HTTP[modifier | modifier le wikicode]

PSR-11 : interface des conteneurs[modifier | modifier le wikicode]

PSR-12 : guide de style étendu[modifier | modifier le wikicode]

Cette norme remplace la PSR-2 et prolonge le PSR-1 avec :

  • Les alinéas doivent faire quatre espaces. Presser la touche "tabulation" peut le faire automatiquement en réglant les IDE.
  • Les lignes ne doivent pas dépasser 120 caractères (les IDE peuvent dessiner une ligne verticale à ce niveau).

Les normes suivantes proposent des implémentations d'architectures logicielles (voir PHP Standard Recommendation sur Wikipédia (en anglais) Article sur Wikipédia).

PSR-13 : liens hypermédia[modifier | modifier le wikicode]

PSR-14 : Event Dispatcher[modifier | modifier le wikicode]

PSR-15 : HTTP Handlers[modifier | modifier le wikicode]

PSR-16 : interface des éléments du cache[modifier | modifier le wikicode]

PSR-17 : interface des fabriques de requêtes HTTP[modifier | modifier le wikicode]

PSR-18 : interface des clients HTTP[modifier | modifier le wikicode]

SOLID[modifier | modifier le wikicode]

Les principes de programmation objet SOLID permettent des codes avec une bonne couverture en tests unitaires et peu de conflits de commits entre les branches du SGV.

Une fonction ne doit faire qu'une seule chose[modifier | modifier le wikicode]

Afin de comprendre tout ce que fait une fonction par son nom sans avoir à la relire, et de réaliser facilement ses tests unitaires, il convient de lui confier un seul rôle, et de ne pas lui injecter plus de deux arguments (en les remplaçant par une classe de configuration à plusieurs attributs[4], ou un tableau).

Séparer le code SQL dans un dossier "repository"[modifier | modifier le wikicode]

À l'instar de Doctrine, il convient de ranger les classes contenant du DQL ou de l'SQL dans un dossier séparé (qui sera le seul à évoluer en cas de changement de SGBD). Ceci est conforme au patron de conception MVC.

Optimisation des performances[modifier | modifier le wikicode]

PHP permet d'aboutir à un même résultat de plusieurs manières différentes, il s'agit donc de privilégier les plus performantes. Voici donc les manières de coder préconisées :

Profilage[modifier | modifier le wikicode]

Plusieurs outils de profilage permettent de classer les codes par temps d'exécution[5].

Xdebug's Profiler[modifier | modifier le wikicode]

Le profilage Xdebug a été décrit dans le chapitre Programmation PHP/Xdebug.

XHProf[modifier | modifier le wikicode]

Données XHProf lues par Grafana

XHProf est une extension PHP dédiée au profilage du code[6], développée par Facebook et open source depuis mars 2009.

Installation[modifier | modifier le wikicode]
Linux[modifier | modifier le wikicode]
pecl install -f xhprof

Dans php.ini :

extension=xhprof.so
Docker[modifier | modifier le wikicode]
RUN pecl install -f xhprof \
 && docker-php-ext-enable xhprof
RUN echo 'xhprof.output_dir = "/usr/src/app/traces"' > /usr/local/etc/php/conf.d/xhprof.ini
Lancement[modifier | modifier le wikicode]

Modifier le .php à analyser, en précisant les flags à analyser parmi les trois disponibles :

xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY + XHPROF_FLAGS_NO_BUILTINS);
// Code à profiler
$data = xhprof_disable();

Ensuite les data peuvent être lues via un fichier :

file_put_contents('trace.xhprof', serialize($data));
Affichage du résultat[modifier | modifier le wikicode]

Plusieurs solutions parmi lesquelles Grafana, Graphviz ou des conteneurs Docker avec leurs propres URLs.

Le résultat est présenté sous la forme d'un tableau de chaque fonction avec leur consommation de CPU et RAM en valeurs et pourcents. On peut aussi voir le graphe séquentiel de leurs appels avec les plus consommatrices de ressources mises en évidence.

Blackfire[modifier | modifier le wikicode]

Outil payant développé par le concepteur du framework Symfony[7] qui fournit des organigrammes des exécutions.

Choisir la condition sans négation[modifier | modifier le wikicode]

Cela permet de gagner une opération NOT dans le processeur, et cela simplifie également la lecture du code en simplifiant la condition.

Avant :

if (x !== 1) {
	y = 1;
} else {
	y = 0;
}

Après :

if (x === 1) {
	y = 0;
} else {
	y = 1;
}

Return early[modifier | modifier le wikicode]

L'évitement des else (et else if) par des return dans les conditions, permet de gagner en performances, en lisibilité et en taille de lignes[8]. Cela peut permettre également d'éviter trop d'imbrications de blocs de code, et la grande indentation que cela entraîne.

Mais le plus important est aussi de ne pas lancer d'instructions inutiles. Exemple si on va chercher des enfants par un appel à une base de données ou une API :

  • Pas bien :
$parent = $this->getParent();
$children = $this->getChildren($parent);

if (empty($parent) || empty($children)) {
    return 404;
}
  • Bien :
$parent = $this->getParent();
if (empty($parent)) {
    return 404;
}

$children = $this->getChildren($parent);
if (empty($children)) {
    return 404;
}

Passage par référence des tableaux[modifier | modifier le wikicode]

Utiliser les références dans les arguments tableaux volumineux (function fonction(&$tableau)), pour éviter sa duplication en mémoire.

Tester les invariants avant les boucles[modifier | modifier le wikicode]

Une condition est testée dans une boucle comme dans l'exemple ci-dessous.

for ($i=0; $i<$count; $i++) {
    if ($mode === 'display') {
        echo $array[$i];
    }
}

Cependant, la boucle n'a pas d'influence sur la valeur de la condition. La condition peut donc être testée avant la boucle pour éviter de la retester plusieurs fois.

if ($mode === 'display') {
    for ($i=0; $i<$count; $i++) {
        echo $array[$i];
    }
}

Tests de charge[modifier | modifier le wikicode]

Plusieurs outils existent :

Autres bonnes pratiques[modifier | modifier le wikicode]

  • Pour nommer une variable, éviter $data car trop générique : choisir un nom le plus descriptif possible.
  • Dans les sprintf(), numéroter les paramètres (ex : remplacer "%s" par "%9$s).
  • Arrondir les float pour éviter les erreurs d'imprécisions dues à la virgule flottante.
  • En POO, ne pas appeler les variables superglobales directement dans les classes, mais les injecter dans le constructeur comme les autres dépendances.
  • Ne pas lancer de SELECT * en SQL car son résultat peut être récupéré en PHP par indice numérique et donc être perturbé par des modifications de schéma en base.
  • Ne pas lancer de SELECT d'un élément dans une boucle si on peut la remplacer par un seul SELECT de tous les éléments.
  • Dans le cas d'un projet à plusieurs, la revue par les pairs permet d'éviter les écueils les plus évidents. Si les spécifications métier sont simples, on peut même étendre cette pratique par un test par les pairs (sinon il faut les faire par un PO).

Références[modifier | modifier le wikicode]


Voir aussi[modifier | modifier le wikicode]