Le Manifeste PHP Way of Life

  1. Introduction : PHP, le langage du Web
  2. Tableaux : Le moyen privilégié de transférer des données, simple et flexible
  3. Objets : Parfait pour organiser le code, à utiliser de préférence avec une logique procédurale
  4. Typage : Fort en principe, adaptable en pratique, mais jamais strict
  5. Interfaces web : Le HTML généré côté serveur est la clé d’un internet rapide et accessible
  6. Bases de données : Les ORM et NoSQL semblent être vos amis, mais SQL l'est vraiment
  7. Frameworks : De bons serviteurs mais de mauvais maîtres
  8. Tests automatisés : unitaires, d’intégration, fonctionnels — trouvez le bon équilibre
  9. Microservices : Il est très peu probable que vous en ayez besoin
  10. APIs : Bousculez les habitudes
  11. Sécurité : Les fondamentaux non négociables
  12. A. Ressources
  13. B. Références

Introduction : PHP, le langage du Web #

Un peu d'histoire #

L’histoire de PHP est connue et documentée [Documentation PHP 1, Lerdorf 2]. C’est un langage qui a été créé spécifiquement pour le Web, au moment où le Web s’est propagé dans les années 90.

Il a rencontré du succès auprès des développeurs, parce qu’il était plus simple que les principales solutions utilisées à l’époque pour créer des sites dynamiques, le C et le Perl. Et en même temps, il présentait une syntaxe similaire, donc les développeurs s’y sont retrouvés très vite.
Il a rencontré du succès auprès des hébergeurs, car son fichier de configuration − avec ses directives max_execution_time et memory_limit − permettait d’héberger un grand nombre de sites sur le même serveur, sans risquer qu’un bug sur un site empêche les autres de fonctionner. [Lerdorf 2]

La philosophie de PHP #

L’un des principes de base de PHP est qu’il est utilisé aussi bien par des développeurs du dimanche que par de grandes entreprises.
Comme le dit son créateur :

J'ai toujours voulu m'assurer que l'échelle soit telle que les guerriers du week-end puissent lancer des sujets intéressants sans avoir à lire 30 livres différents. [Lerdorf 2]
L’une des forces de PHP, c’est qu’il passe à l’échelle. Il peut faire tourner les plus grands sites du monde, tout en restant accessible aux développeurs du dimanche. Réussir à faire les deux avec une seule base de code est un vrai défi. [Lerdorf 3]

Cette idée est fondamentale, et il ne faut pas la perdre de vue.

PHP est un langage humble. C’est dans son ADN depuis le début:

PHP est aussi passionnant que votre brosse à dents. Vous l'utilisez tous les jours, elle fait le travail, c'est un outil simple, et alors ? Qui voudrait lire des choses sur les brosses à dents ? [Lerdorf 4]
Je n'ai jamais considéré PHP comme plus qu'un simple outil pour résoudre des problèmes. [Lerdorf 5]

Même si du temps et de l’argent sont passés depuis ces propos, cela correspond à une philosophie qui a marqué le développement du langage. Le PHP est un langage pour tous, il n'est pas élitiste.

PHP et le développement moderne #

Depuis la création de PHP, la pratique du développement web s’est enrichie considérablement. Le génie logiciel a permis de construire des applications d’envergure.
Le PHP a lui-même grandement évolué, proposant un modèle objet particulièrement complet et des performances qui n’ont cessé de s’améliorer.

Aujourd’hui, tout le monde n’adhère pas à la nouvelle complexité du développement web. L’article The New Internet de Avery Pennarun (CEO et co-founder de Tailscale) résume à lui seul l’esprit du Manifeste :

Ce qu’on a constaté, c’est que beaucoup de choses se sont améliorées depuis les années 1990. Les ordinateurs sont littéralement des millions de fois plus rapides. Et aujourd’hui, cent fois plus de personnes peuvent devenir développeurs, parce qu’ils ne sont plus coincés avec seulement du C++ et de l’assembleur.
Mais, en parallèle, certaines choses ont empiré. Plein de tâches du quotidien qui étaient simples pour les développeurs sont devenues compliquées. Et ça, on ne l’avait pas vu venir.

À la place, l’industrie tech s’est transformée en un véritable foutoir. Et ça ne s’arrange pas — au contraire, ça empire ! L'empilement de complexité est devenue tellement haut qu’on envisage sérieusement de coller des LLMs par-dessus pour qu’ils écrivent le code incompréhensible dans des frameworks incompréhensibles pour qu’on n’ait pas à le faire nous-mêmes.

J’ai lu récemment un post où quelqu’un se vantait d’utiliser Kubernetes pour faire tourner un site à 500 000 pages vues par mois. Mais ça fait 0,2 requête par seconde. Je pourrais servir ça depuis mon téléphone, sur batterie, et il passerait la majeure partie du temps à dormir.

Dans l’informatique moderne, on tolère des builds interminables, suivis de builds Docker, l’envoi vers des registres de conteneurs, des déploiements qui prennent plusieurs minutes avant que le programme ne tourne, et encore plus de temps avant que les logs apparaissent quelque part où on peut les lire. Tout ça parce qu’on nous a fait croire que tout devait forcément "passer à l’échelle". Les gens s’emballent pour des services de déploiement à la mode, juste parce qu’ils mettent quelques dizaines de secondes au lieu de plusieurs minutes. Alors qu’avec mon vieil ordi lent des années 90, je pouvais lancer un script Perl ou Python en quelques millisecondes, servir bien plus de 0,2 requête par seconde, et voir les logs s’imprimer tout de suite dans stderr — ce qui me permettait de faire du edit-run-debug plusieurs fois par minute.

En tant qu’industrie, on a consacré tout notre temps à rendre possibles les choses difficiles, mais aucun à rendre faciles les choses simples.

Le développement logiciel moderne est une usine à gaz bordélique. [Pennarun 6]

Il y a trois raisons à cette complexification :

  • Les personnes qui sont intrinsèquement incapables de faire les choses simplement, ou qui donnent de la valeur à la complexité.

    Les complexificateurs sont allergiques à la simplification. Leur réflexe naturel est de transformer les tâches simples en bourbiers, et de rejeter les idées claires jusqu’à ce qu’elles soient ensevelies (ou étouffées) sous des couches d’abstraction.

    Les simplificateurs s'épanouissent dans la concision. Ils recherchent les 6x=6y dans le monde et les transforment en x=y. Ils ne laissent jamais leur ego se mettre en travers du chemin le plus court. [Berkun 7]

    Les gens confondent de plus en plus complexité et sophistication. [Wikipedia 8]
  • Les personnes qui pensent que pour montrer leurs compétences, elles doivent continuellement surfer sur la vague des dernières technologies à la mode.

    Une équipe choisit d’adopter la dernière techno à la mode pour son projet. Peu après, elle se met à l’utiliser, toute excitée par cette nouveauté brillante, mais au lieu d’aller plus vite (comme promis) et de livrer un meilleur produit, elle rencontre des difficultés. Elle ralentit, se démotive, et peine à livrer la prochaine version fonctionnelle en production.

    Le problème du développement guidé par la hype, c’est qu’il mène très facilement à de mauvaises décisions. Les mauvaises décisions architecturales et technologiques hantent souvent l'équipe des mois, voire des années plus tard. [Kirejczyk 9]

    Il n'y a pas de fierté à gérer ou à comprendre la complexité. [Kiehl 73]
  • L’application aveugle des bonnes pratiques issues des grandes entreprises tech, sans se rendre compte que ce qui fonctionne pour une équipe de 400 personnes ne fonctionnera pas pour une équipe de 12 ou 80 personnes.
    [Les bonnes pratiques] devraient nous inspirer, mais pas être appliquées au pied de la lettre. Chaque contexte est unique et complexe, avec ses propres besoins et objectifs, qui ne peuvent pas être réduits et satisfaits par une solution générique. On ne devrait jamais appliquer une bonne pratique sans tenir compte du contexte dans lequel elle s’inscrit. [Goosens 10]
    Les bonnes pratiques, bien qu'utiles à certains égards, restent souvent des solutions « taille unique » qui ignorent la réalité propre à chaque public, chaque produit, chaque entreprise. Cela tue l’innovation, et pousse les entreprises tout droit vers la médiocrité. [Knight 11]
    Les gens regardent Amazon ou Google et se disent : « Hé, si ça marche pour ceux qui ont du succès, ça marchera pour moi aussi ! » Bzzzzzzzzt !! Faux !
    Les modèles qui ont du sens pour des organisations cent fois plus grosses que la vôtre sont souvent exactement l’opposé de ceux qui vous conviendraient. [DHH 74]

Il ne faut pas oublier que le code le plus simple est celui qui fonctionne le mieux en entreprise, que ce soit à court terme (plus rapide à développer, plus simple à tester, plus rapide à s’exécuter) ou à long terme (plus facile à modifier, plus résilient face aux régressions).

Les personnes « les plus avancées » utilisent souvent des solutions si simples qu’elles ressemblent à celles de gens qui ne savent pas ce qu’ils font. Les personnes moyennes sont souvent dans la catégorie « en savent assez pour être dangereuses » parce qu'elles réfléchissent, travaillent et traitent tout de manière excessive par manque d'expérience plus complète pour découvrir des solutions plus simples et plus propres. [Stancliff 12]

> Le meilleur code, écrit par les développeurs les plus expérimentés, ressemble souvent à du code de débutant — sauf qu’il marche.

Se pourrait-il que les meilleures pratiques soient conçues pour s'assurer que des programmeurs médiocres travaillant ensemble produisent un code décent ?
Après tout, les vrais débutants écrivent un code très proche de celui des meilleurs, sauf qu'il ne fonctionne pas. [Hacker News 13]

Sous couvert de professionnalisation, l’écosystème PHP s’est beaucoup inspiré du monde Java, et a perdu de vue ce qui faisait la force du langage − et qui n’en faisait en rien un langage amateur.
Mais cela n’est pas une fatalité.

En schématisant à l’extrême, la nature humaine suit des cycles de complexification et de simplification. En informatique, chaque technologie a tendance à se complexifier (exemples : C ➡ C++ ; Lisp ➡ Haskell ; Java ➡ JEE ; CGI ➡ WebSphere)jusqu’à ce qu'apparaisse une technologie plus simple (exemples : Fortran ➡ BASIC ; C ;➡ Perl ; C++ ➡ Java ; Perl ➡ PHP/Python ; Objective-C ➡ Swift).

Aujourd’hui, après avoir accepté de se complexifier, l’écosystème PHP est prêt à retrouver ses racines et à revenir à plus d’équilibre.

Tableaux : Le moyen privilégié de transférer des données, simple et flexible #

Les tableaux PHP sont depuis longtemps un type de données relativement unique parmi les langages de programmation. Ils sont extrêmement polyvalents et peuvent être utilisés comme des listes indexées numériquement, des tableaux associatifs, ou une combinaison des deux, tout en préservant l'ordre d'insertion.

Les toutes premières versions de PHP v1 avaient des implémentations distinctes pour les listes, les maps et les sets. Mais je les ai rapidement remplacées par une implémentation hybride unifiée de map ordonnée, que j’ai simplement appelée “Array”. L’idée, c’était que dans presque toutes les situations d’une application web, une map ordonnée peut résoudre le problème. Ça ressemble suffisamment à un tableau pour être utilisé là où on attend un tableau, tout en évitant le casse-tête de proposer 3 ou 4 types différents, avec les mots-clés et la syntaxe associés, forçant l’utilisateur à deviner lequel utiliser. Cette décision remonte à 1994, et à part quelques critiques pointilleuses sur le nom au fil des années, je pense qu’elle a plutôt bien traversé l'épreuve du temps. [Lerdorf 3]

On peut remarquer que d’autres langages de programmation ont modifié depuis leurs équivalents (appelés dictionnaire ou hash) pour qu’ils aient un comportement similaire par défaut.

Les tableaux sont faciles à comprendre et à utiliser par les personnes qui découvrent le développement. Il est facile d’y ajouter des éléments, de les parcourir, de les sérialiser/désérialiser en JSON. Il existe un grand nombre de fonctions natives qui permettent de les manipuler.

Les tableaux PHP peuvent être utilisés pour décrire des types de données aussi variés que les piles, les files ou les enregistrements.

Un code qui utilise des tableaux est facile à lire et à comprendre ; il est autonome, sans objets supplémentaires qu’il faudrait inspecter.

Quand vous devez transmettre des données, commencez par le faire en utilisant des tableaux. Leur souplesse vous permettra d’adapter votre développement aisément au fur et à mesure que le besoin évoluera.
La plupart du temps, vous verrez que cela est suffisant, même sur le long terme. Votre code sera explicite tout en étant évolutif.

Le PHP offre des structures de données plus spécialisées (SplDoublyLinkedList, SplStack, SplQueue, SplHeap, SplFixedArray…). Mais ne les utilisez que si cela est pleinement justifié. Ne cherchez pas à optimiser prématurément votre code ; les sources de performance se situent habituellement ailleurs.

Ne commencez pas non plus à créer des DTO (Data Transfer Object, objets sans logique métier servant uniquement à transporter des données) dès le début, vous ne feriez que rendre votre code inutilement complexe.

Nous sommes arrivés à la même conclusion sur plusieurs projets au travail. On avait commencé avec des implémentations naïves d'objets, puis on a fait machine arrière — juste par souci de simplicité — en revenant à la transmission de jeux de données bruts. [Atwood 14]

Évidemment, il y a des cas pour lesquels un objet sera plus adapté, notamment si vous éditez une bibliothèque (pour éviter d’écrire du code défensif, qui vérifie tout ce qu’il reçoit en entrée). Mais au sein de votre propre code métier, cela ne doit pas être votre élan premier.

Objets : Parfait pour organiser le code, à utiliser de préférence avec une logique procédurale #

Un peu de contexte #

La programmation orientée objet est un concept qui existe depuis longtemps, avec les premières implémentations dans le langage Simula en 1967 [Wikipedia 15]. La plupart des langages orientés objet partagent les concepts d’héritage, d’encapsulation et de polymorphisme. PHP propose un modèle orienté objet solide depuis la sortie de PHP 5 en 2005.

Aujourd’hui, il paraît évident que l’utilisation des objets va de soi. La pratique du génie logiciel s’est généralisée, et les design patterns font gagner du temps en fournissant un langage commun à tous les développeurs.

Toutefois, de nombreuses voix s’élèvent pour affirmer que la programmation orientée objet n’est pas la panacée ultime à tous les problèmes de développement.

Dans ce sens, certains considèrent que la POO (programmation orientée objet) convient à certains types de développements, mais pas à tous.

Je pense que les objets, les classes, le polymorphisme, et même l’héritage, peuvent être des outils valables dans certains cas. Mais contrairement à ce que prône la POO, ce sont des cas de niche, pas la norme par défaut. [Will 16]
Le concept de conception orientée objet s’est d’abord révélé utile dans les systèmes graphiques, les interfaces utilisateur graphiques et certains types de simulation. Mais à la surprise (et à la désillusion progressive) de beaucoup de gens, il s’est avéré difficile de démontrer des bénéfices significatifs de l’approche objet en dehors de ces domaines. [Raymond 17]

L'orienté objet est verbeux #

Le premier argument qui amène à ce raisonnement est le fait que l’approche orientée objet est bien plus complexe et verbeuse qu’une approche procédurale classique, amenant à écrire − et donc à maintenir − beaucoup plus de code. Il devient de plus en plus difficile de se représenter mentalement l’ensemble des objets et de leurs interactions. Le flux d’exécution devient beaucoup plus dur à suivre comparé à un code procédural.

Ajouter des objets à son code, c’est comme mettre du sel dans un plat : en petite quantité, ça relève le goût ; mettez-en trop et ça gâche tout. Parfois, mieux vaut pécher par excès de simplicité, et j'ai tendance à privilégier l’approche qui réduit la quantité de code plutôt que celle qui en ajoute. [Atwood 18]
J’ai travaillé avec des développeurs qui tenaient absolument à ce que tout passe par un modèle objet, même si ça multipliait la quantité de code. [Atwood 19]
Je pense que les grands programmes orientés objet deviennent de plus en plus complexes à mesure qu’on construit un vaste graphe d’objets mutables. Vous savez, essayer de comprendre et de garder à l'esprit ce qui va se passer quand on appelle une méthode, et quels effets secondaires cela peut entraîner. [Hickey 20]
Le problème avec la programmation orientée objet, c’est que ces langages ont été conçus pour aider les développeurs à gérer leur code… Mais aujourd’hui, il est presque impossible de suivre le flux d’exécution. On ne peut plus repérer les bugs liés à ce flux simplement en relisant le code. [Shelly 21]
Certains commentateurs affirment que sans POO, le code finit inévitablement en spaghetti. La peur du monstre spaghetti est une phobie saine chez les développeurs, mais la POO ne nous en protège pas, elle se contente de masquer les spaghettis derrière des couches d’indirection. [Will 16]

Séparation entre données et traitements #

Il y a aussi le fait que les données et les traitements sont deux choses différentes qui n’ont pas à être mélangées. La programmation se fait avec des verbes et pas avec des noms ; on fait des choses, on ne se contente pas de manipuler des concepts abstraits.

Que les données restent des données. Que les actions restent des actions.
On ne devrait pas avoir à conceptualiser tout ce qu'on veut faire dans le code en termes de données. On ne devrait pas avoir à transformer tous nos verbes en noms. [Will 22]
Il n’existe aucune preuve objective et accessible montrant que la POO est supérieure à la simple programmation procédurale. La POO n’est pas naturelle pour le cerveau humain, notre façon de penser est centrée sur l’action — se promener, parler à un ami, manger une pizza. Notre cerveau a évolué pour faire des choses, pas pour organiser le monde en hiérarchies complexes d’objets abstraits. [Suzdalnitski 23]
Les objets lient fonctions et structures de données en unités indivisibles. Je pense que c’est une erreur fondamentale, car fonctions et structures de données appartiennent à deux mondes totalement différents. [Armstrong 24]
Les fanatiques de l’orienté objet ont peur des données. Ils préfèrent les déclarations ou les constructeurs à de simples tables initialisées. Ils ne veulent pas écrire de tests pilotés par des tables. Pourquoi ? Quelle mentalité pousse à penser qu’une hiérarchie de types à plusieurs niveaux, avec des couches d’abstraction, est préférable à une table de trois lignes ? Un jour, j’ai entendu quelqu’un dire qu’il estimait que son boulot, c’était de supprimer toutes les boucles while existantes dans le code, pour les remplacer par des objets. Sérieux ? [Pike 25]

Orienté objet versus approche procédurale #

Mais faut-il réellement opposer POO et code procédural ? Oui et non.
Oui, si vous sombrez dans le “tout objet” ; non, si vous utilisez intelligemment vos objets avec une logique procédurale. Car s’il reste indéniable que les objets sont très pratiques dans un grand nombre de situations, vous pouvez les utiliser d’une manière qui permette quand même de suivre l’exécution du code pas à pas.

Prenons les exemples donnés par Yegor Bugayenko (directeur de laboratoire chez Huawei) dans son article “OOP Alternative to Utility Classes.” Ces exemples sont en Java, mais les principes restent les mêmes.

Il nous explique que, pour déterminer le plus grand de deux nombres, l’approche procédurale est d’avoir un objet utilitaire comportant une méthode max(), alors que l’approche POO serait d’avoir un objet Max.

Cela donnerait ce code :

// version procédurale
int max = NumberUtils.max(10, 5);
// version objet
int max = new Max(10, 5).intValue();

Le second est-il plus lisible ? Bien sûr que non. Sans compter que le code source de l’objet Max est bien plus verbeux que celui de NumberUtils. Et avons-nous envie de créer un objet pour chaque opération possible et imaginable ? Clairement pas.

Son deuxième exemple présente une fonction qui lit un fichier, efface les espaces en début et fin de chaque ligne, et écrit le résultat dans un autre fichier.

Voici la version procédurale :

void transform(File in, File out) {
    Collection src = FileUtils.readLines(in, "UTF-8");
    Collection dest = new ArrayList<>(src.size());
    for (String line : src) {
        dest.add(line.trim());
    }
    FileUtils.writeLines(out, dest, "UTF-8");
}

Et voici la version strictement orientée objet :

void transform(File in, File out) {
    Collection src = new Trimmed(
        new FileLines(new UnicodeFile(in))
    );
    Collection dest = new FileLines(
        new UnicodeFile(out)
    );
    dest.addAll(src);
}

Prenez le temps de lire chaque fonction.
La première peut se lire ligne après ligne, on comprend quelles sont les opérations qui sont effectuées. Même sans connaître les fonctions appelées, on devine ce qu’elles font.
La seconde fonction, de son côté, instancie plusieurs objets en les passant en paramètre les uns aux autres. Il est difficile de suivre le cours probable de l’exécution juste en lisant ce code.

On voit clairement qu’avec une programmation orientée objet stricte, l’effort cognitif nécessaire pour comprendre le code est très largement supérieur.
Alors peut-être que cela respecte le “Java Way of Life”, mais ce n’est définitivement pas en phase avec le PHP Way of Life.

Pour conclure #

Si vous voulez faire du “tout objet”, faites du Java, pas du PHP.

Typage : Fort en principe, adaptable en pratique, mais jamais strict #

PHP offre une gestion des types qui est souple. Les premières versions de PHP avait un typage faible, mais il est possible depuis longtemps de typer les paramètres et les retours des fonctions et méthodes. Il a ensuite été ajouté l’option strict_types, qui force un pseudo typage fort.

Le typage PHP classique #

Le typage des paramètres et des retours de fonctions permet d’assurer, au sein d’une fonction, que les types reçus sont bien ceux voulus.

Prenons l’exemple simple (et inutile en pratique) d’une fonction de concaténation de chaînes de caractères. En l’absence de typage des paramètres, on finit souvent par écrire du code défensif, qui vérifie les types des données en entrée :

function concat($a, $b)
{
    if (!is_scalar($a) || !is_scalar($b))
        throw new \TypeError(“Mauvais paramètre.”);
    return $a . $b;
}

Alors qu’avec un typage des paramètres, on est certain de recevoir des données manipulables sans problème :

function concat(string $a, string $b) : string
{
    return $a . $b;
}

Tant que la fonction concat() est appelée en lui passant des paramètres scalaires (booléen, entier, flottant, chaîne), PHP fera la conversion, et la fonction recevra des chaînes. Si on sait que le code appelant ne peut contenir que des scalaires, aucune gestion d’erreur ni conversion explicite n’est nécessaire :

$result = concat($str1, $str2);

Si par contre le code appelant ne maîtrise pas complètement les données qui seraient envoyées à la fonction, il suffit de gérer l’exception TypeError qui sera levée en cas de conversion impossible :

try {
    $result = concat($str1, $str2);
} catch (\TypeError $te) {
    // gestion d'erreur
}

La plupart du temps, les exceptions sont gérées à un niveau plus élevé, permettant de garder un code le plus simple possible.

Le typage PHP strict #

Si la directive strict_types est activée, l’appel ne peut se faire qu’avec des paramètres du bon type. Si on sait que le code appelant ne contient que des scalaires, il faut faire des conversions explicites :

$result = concat((string)$str1, (string)$str2);

On peut voir que, même pour un exemple aussi simple, le code est plus verbeux. Sa lecture demande un effort cognitif supplémentaire.

Si le code appelant est susceptible de contenir n’importe quelle donnée dans les variables source, il faut vérifier leurs types et gérer les erreurs, en plus de faire des conversions explicites :

if (!is_scalar($str1) || !is_scalar($str2)) {
    // gestion d'erreur
}
$result = concat((string)$str1, (string)$str2);

Même si l’activation de la directive strict_types est à la mode, son utilisation aboutit à un code difficile à maintenir. L’utilisation d’outils d’analyse statique de code permet de prévenir la quasi-totalité des cas où un typage strict est utile.

Il est intéressant de remarquer que Gina Banyard (membre de la core-team PHP) a proposé une RFC ayant pour but de supprimer la directive strict_types :

L'utilisation aveugle du mode de typage strict a eu des conséquences inattendues&nbisp;:

  • L'utilisation de casts (transtypages) explicites pour se conformer aux exigences de type, même s'ils sont moins “type safe”
  • Le besoin perçu de recourir à des casts dits “stricts”
  • Des analyses manuelles ou des conversions de type (“type juggling”) que le moteur PHP sait déjà gérer automatiquement

À cause de l’emploi systématique du mode de typage strict, imposé par les styles de codage modernes, beaucoup de développeurs ne comprennent ni la portée de declare, ni ce qu’elle implique réellement.
Beaucoup pensent que cela rend PHP plus strict en ce qui concerne la conversion de types, alors qu'en réalité, cela n'affecte que le passage de scalaires en paramètre des fonctions utilisateur, la valeur de retour scalaire des fonctions utilisateur personnalisées, et l'affectation d'une valeur à une propriété scalaire typée.
Cela n’empêche pas les conversions de type via les opérateurs, ni dans les appels à des fonctions internes, même si celles-ci sont appelées dans le code utilisateur avec le typage strict activé. Les gestionnaires d’erreur, d’exception ou d’arrêt de PHP sont de bons exemples.

Trop strict peut mener à trop laxiste
La nécessité perçue de recourir à des casts de type dits “stricts” est un symptôme clair que des casts explicites sont utilisés à des endroits où ils ne devraient pas l'être, simplement pour se conformer à la déclaration de type d'un paramètre de fonction. [Banyard 26]

Conclusion #

L’utilisation du typage pour les paramètres et les retours des fonctions a prouvé son utilité. Il rend le code plus robuste.

Les langages typés sont essentiels dans les équipes dont les niveaux d'expérience sont hétérogènes. [Kiehl 73]

Par contre, n’utilisez pas le strict_types par défaut. Il n’apporte concrètement aucune plus-value.

Interfaces web : Le HTML généré côté serveur est la clé d’un internet rapide et accessible #

Contexte général #

À la base, le PHP était pensé pour être un moteur de template, permettant d’insérer des appels à du code en C dans un fichier HTML. Et d’une certaine manière, il est toujours possible de l’utiliser de cette façon.

Aujourd’hui, il existe deux manières de créer un site web : soit en générant le HTML côté serveur, soit en fournissant une API à laquelle se connecte une application Javascript.

Les frameworks Javascript sont devenus très courants, poussés par la mode des SPA (Single Page Application). Pour le développement d’applications fonctionnellement complexes, leur utilité n’est pas à prouver.

Le problème des frameworks Javascript #

Malheureusement, avec la logique habituelle de généraliser sans discernement les “bonnes pratiques” de grosses entreprises, on peut voir une majorité de sites web utiliser des frameworks JavaScript, alors même qu’ils sont basés sur un fonctionnement purement transactionnel : des pages comportant des liens et des formulaires ; quand on clique sur un lien, on charge une autre page ; quand on envoie un formulaire, on reçoit une redirection.
Dans ce cas, la génération des pages côté navigateur ne fait qu’allonger les temps de chargement, rendre les développements plus complexes, et rendre les débogages plus difficiles.

Du côté des utilisateurs, les impacts de ces frameworks sont immédiats : les pages web sont plus lourdes, se chargent plus lentement, et sont moins accessibles.

Ces dernières années, les performances web semblent être passées au second plan. En effet, alors que de nombreux sites utilisent désormais des frameworks tels que React et Vue, que les SPA deviennent monnaie courante et que les requêtes se comptent par centaines, la page web moyenne est aujourd'hui plus volumineuse que jamais, les pages de 2 à 3 Mo étant plus courantes que jamais. [Cheatmaster30 27]
À octet égal, aucune ressource n’affecte plus la vitesse de chargement que JavaScript. JavaScript affecte les performances réseau, le temps de traitement du processeur, l’uitilisation de la mémoire et l’expérience utilisateur dans son ensemble. Des scripts inefficaces peuvent ralentir votre site, le rendant moins réactif et plus frustrant pour vos utilisateurs. [Zeman 28]
La quête effrénée des frameworks JavaScript “dernier cri” a involontairement contribué à rendre le web moins accessible, avec un impact disproportionné sur les utilisateurs.
Une approche plus raisonnable consisterait à privilégier l'accès à l'information et l'accessibilité, plutôt que les interfaces tape-à-l’œil. [Easy Laptop Finder 29]

Sans compter les dépendances à de nombreuses bibliothèques Javascript, qui ont leurs propres failles ou rythmes effrénés de mises à jour.

J’ai l’impression que peu de gens parlent de la fatigue liée à la gestion des dépendances.
Je passais beaucoup trop de temps à gérer des mises à jour de paquets, surtout des paquets React. Je mettais à jour mes paquets vers leur dernière version, pour découvrir ensuite que leurs API avaient changé de manière non rétrocompatible, m’obligeant à investir du temps pour refactorer mon code.

Si vous essayez de construire un produit qui nécessite le moins de maintenance possible une fois livré, je vous conseille de rester aussi loin que possible de l’écosystème JavaScript. [Rodriguez 30]

[à propos du paquet left-pad] Ce qui m’inquiète ici, c’est que tant de paquets et de projets aient ajouté une dépendance pour une simple fonction de padding à gauche sur une chaîne, plutôt que leurs développeurs prennent 2 minutes pour écrire eux-mêmes une fonction aussi basique.

Une installation fraîche du paquet Babel contient 41 000 fichiers.
Un simple template d’application basé sur jspm/npm démarre désormais avec plus de 28 000 fichiers.

Avons-nous oublié comment programmer ? [Haney 31]

Mais alors pourquoi continuer à utiliser ces frameworks même lorsque cela ne se justifie pas ? Certaines personnes en viennent à penser que beaucoup de développeurs font passer leur envie d’utiliser des technologies “à la pointe” avant le bien-être de leurs utilisateurs, ou encore que l’influence des grosses entreprises tech est puissante.

Le problème c'est l'état d'esprit des développeurs et des designers (…) qui considèrent que le développement web devrait avant tout être “fun”. Je suis convaincu que beaucoup de développeurs et d'ingénieurs logiciels placent leur propre satisfaction au-dessus de leurs utilisateurs ou de leurs clients.

Et c'est ce qui a conduit à toutes ces pratiques douteuses, ainsi qu'à un manque d'intérêt pour ce qui compte vraiment. Des systèmes de build lourds comme Webpack, et des dizaines de dizaines de composants préfabriqués de NPM sont intégrés pour “faire gagner du temps aux développeurs”, sans trop se soucier des kilo-octets (ou même des méga-octets) supplémentaires de JavaScript que cela ajoute au produit fini. [Cheatmaster30 27]

Pourquoi est-ce aussi compliqué ? De base, coder le front d'une appli web est complexe ; il y a plein d’éléments qui interagissent, beaucoup de choses qui peuvent mal tourner, alors pourquoi ne pas laisser les “experts” de Facebook ou Google nous dire quoi faire, hein ?

Il y a aussi des raisons plus cyniques. J'aime bien l'idée que les développeurs front-end ont été critiqués comme étant des “développeurs front-end, c'est juste une bande de nuls” pendant si longtemps que maintenant ils surcompensent faisant des trucs super compliqués, juste pour dire “Hé, moi aussi je suis aussi un informaticien”.

Une autre raison cynique est que les entreprises comme Facebook et Google, qui promeuvent ces frameworks, ont tout intérêt à gagner en influence, car c'est mieux pour elles que les gens utilisent les technologies qu'elles ont créées, ça accroît leur réputation. [Holovaty 32]

Génération serveur et amélioration progressive #

La solution est de se reposer sur ce qu’on maîtrise le mieux : générer des pages HTML côté serveur. Y associer du CSS, et y ajouter du Javascript uniquement pour l’améliorer de manière progressive si nécessaire.

Meilleure pratique pour optimiser le JavaScript : Dans la mesure du possible, n'utilisez pas de JavaScript
Générer le HTML côté serveur est une approche bien plus performante que de se reposer sur le JavaScript côté client pour tout générer.

Réduire la dépendance au JavaScript lors du chargement des pages, c’est non seulement diminuer la quantité de code que le navigateur doit télécharger, analyser, compiler et exécuter, mais aussi lui permettre de tirer parti de ses propres optimisations internes pour des performances maximales. [Zeman 28]

Le code exécuté côté serveur peut être entièrement comptabilisé.
Le code exécuté côté client, en revanche, tourne sur l’ordinateur du diable.

Par conséquent, une stratégie absurdement efficace consiste à envoyer moins de code. En pratique, cela signifie qu'il faut privilégier le HTML et le CSS par rapport au JavaScript, car ils se dégradent proprement et présentent des taux de compression plus élevés.
La seule chose qui améliore réellement une expérience web, c’est de se soucier sincèrement de l’expérience utilisateur. [Russell 33]

L'enrichissement progressif est une méthode de création de sites et d'applications web basée sur l'idée qu'il faut d'abord faire fonctionner la page en HTML. Ce n'est qu'ensuite qui'on y ajoute du CSS et du JavaScript.

Si vous pensez que votre service ne peut être construit qu'avec du JavaScript, envisagez d'utiliser des solutions plus simples basées sur du HTML et du CSS, et qui répondront aux besoins des utilisateurs.

Si vous utilisez JavaScript, il ne devrait servir qu'à améliorer le HTML et le CSS, ainsi les utilisateurs pourront continuer à utiliser le service même en cas de problème d'exécution du JavaScript.

Ne construisez pas votre service comme une application monopage (SPA). [UK Government 34]

L’amélioration progressive est un principe de conception et de développement qui consiste à construire par couches, qui s’activent automatiquement en fonction des capacités du navigateur. Par défaut, ces couches sont désactivées, ce qui garantit une base solide et accessible à tous.

On applique ce principe avec une approche déclarative, qui est déjà intégrée dans la manière dont le navigateur traite le HTML et le CSS. Quant à JavaScript — qui est impératif — on ne l’utilise que pour enrichir l’expérience, et non comme une dépendance obligatoire. Il n’est chargé que lorsque les éléments fondamentaux — HTML et CSS — offrent déjà une expérience utilisateur de qualité. [Bell 35]

Si vous avez vraiment besoin d’ajouter du Javascript dans vos pages, évitez d’utiliser un framework complet. Votre site restera plus léger, réactif et accessible en utilisant du Javascript basique, ou une bibliothèque spécialisée comme Vik, Turbo, htmx, ou même le bon vieux jQuery. Mais vous ne devriez jamais avoir besoin de créer un "contrôleur" en Javascript.

React nous a bien servi, mais des choses qui étaient faciles sont devenues plus difficiles. Ça nous a frappés lorsque l'ajout dun simple champ dans un formulaire a fini en pull request de 700 lignes. En passant de React à StimulusJS, nous avons supprimé environ 60 % de notre JavaScript. Fait intéressant, la quantité de JavaScript dans notre application est restée globalement stable depuis.

StimulusJS permet de consolider davantage la logique et l'état de l'application dans le backend. Certes, on perd un peu en réactivité côté client, mais la gestion de l'état côté client est une illusion. StimulusJS vous pousse à écrire le moins de JavaScript possible.
Les lignes de code sont un fardeau, pas un atout.

Nous sommes maintenant sur une stack technique contre laquelle on ne se bat pas. [Sutton 36]

Je pense que l'ère des “gros clients” et des frontaux surchargés en JavaScript touche à sa fin. L'engouement pour les “edge applications” est déplacé et inutile pour la création de nombreux types d'entreprises prospères. De nombreuses interactions sont impossibles sans JavaScript, mais ce n’est pas une raison pour en écrire plus que nécessaire.

Le meilleur, c'est qu'on n'a pas besoin de supporter la charge mentale de la gestion d'état côté frontend. Tout n'est qu'une page HTML avec une pincée de JavaScript, il n'y a pas d'état à maintenir entre les changements de page. Il n'y a pas de gestion d'état compliquée sur le client. [Sutton 37]

Un front-end simple pour un build simple #

Pour aller encore plus loin, en simplifiant le développement front-end, vous simplifiez votre processus de build. Au lieu de passer par des étapes complexes, vous pouvez idéalement n’avoir aucun pré-traitement à effectuer avant de pouvoir déployer votre site. Plus votre cycle développement-déploiement-test-débogage sera rapide, plus vous gagnerez en efficacité.

Le transpiling avec Babel a marqué le début d’une ère de pipelines et d’outils horriblement complexes. Écrire le JavaScript du futur n'était pas gratuit. Le prix à payer, c’était un enchevêtrement de complexité toujours plus vaste.
Je ne pense plus que ce compromis en vaille la peine pour la plupart des nouvelles applications. [DHH 38]

Rien n'est plus rapide que de ne pas faire de build

Je travaille sans aucune véritable étape de build côté front-end. Tout est tellement… simple. Et c’est rapide. Vraiment rapide. Infiniment rapide.
Pour la première fois depuis peut-être 15 ans, l’état de l’art n’est plus d’inventer des moyens toujours plus sophistiqués de compiler du JavaScript ou du CSS. C’est de ne rien compiler du tout. [DHH 39]

Bases de données : Les ORM et NoSQL semblent être vos amis, mais SQL l'est vraiment #

Dans une application web, l’accès aux données peut se faire de plusieurs manières différentes : à travers des APIs, grâce à des bases de données relationnelles (SGBDR), des bases de données non relationnelles (NoSQL), des fichiers, etc.

Les bases NoSQL #

Il existe une multitude de bases NoSQL différentes, basées sur des mécanismes variés. Certaines d’entre elles sont particulièrement efficaces sur des cas d’usage très spécifiques.

Les bases orientées document ont gagné en notoriété, mais attention à ne pas les prendre pour ce qu’elles ne sont pas. Si elles promettent d’indexer l’intégralité des documents stockés, faire des requêtes complexes portant sur un grand nombre de paramètres − comme on le ferait avec des requêtes relationnelles utilisant des jointures − aboutit à des performances décevantes.

Les jointures dans MongoDB sont très fragiles (au moindre changement, il faut réécrire les applications en profondeur), et dans bien des cas, les performances de MongoDB sont très médiocres.

Les jointures sont généralement lentes dans MongoDB. Il ne dispose pas d’optimiseur de requêtes et la stratégie d’exécution est codée en dur dans l’application. Dès que le tri par fusion ou la jointure par hachage serait la meilleure option, les performances de Mongo chutent. [Stonebraker 40]

MongoDB a d'excellentes performances en écriture, ce qui en fait un excellent choix pour les applications nécessitant une insertion rapide des données. À l’inverse, MySQL est plus performant en lecture, en particulier pour les requêtes qui peuvent tirer parti d’une indexation efficace.
Si les performances en écriture sont essentielles, MongoDB est la meilleure option. Cependant, si votre application exige des lectures rapides sur des champs indexés, MySQL sera problablement un meilleur choix. [Verma 41]

DynamoDB est le pire choix possible pour le développement d'applications généralistes. [Kiehl 73]

De manière générale, les bases NoSQL se manipulent à travers des API spécifiques, qui ne sont ni plus efficaces ni plus lisibles que des requêtes SQL.

Notez que le code [utilisant MongoDB] est beaucoup plus complexe que le code utilisant Postgres, car MongoDB n'a pas de notions de jointure relationnelle et utilise un langage de niveau inférieur à SQL. De plus, le développeur doit construire algorithmiquement un plan de requête pour la jointure. [Stonebraker 40]
Aussi moche soit-il, SQL reste bien plus concis et lisible que plusieurs lignes d’appels à une API. D’ailleurs, pour expliquer comment utiliser son API, la documentation de MongoDB liste les requêtes SQL équivalentes. C’est un aveu plutôt clair en faveur de la lisibilité et de l’ergonomie de SQL. [Voss 42]

Quand vous démarrez un projet, il y a de grandes chances qu’une base de données relationnelle soit ce dont vous avez besoin.
Si vous avez des besoins très variés, utilisez chaque système là où il excelle : MySQL/PostgreSQL pour les données relationnelles, Redis pour les paires clé-valeur, ElasticSearch pour l’indexation full-text, MongoDB/CouchDB/CouchBase pour les documents sans schéma, un système de fichiers réseau ou un stockage cloud pour les fichiers binaires, etc.

Le modèle relationnel a quelque chose de magique. Créez un modèle d’entités, versez-y des données, et vous obtenez des réponses.
Si vous ne connaissez pas toutes les questions que vous pourriez avoir à poser sur vos données, le plus sûr est de les stocker dans un SGBDR. Et lorsque vous démarrez un projet, vous ne connaissez presque jamais toutes les questions que vous devrez poser. Mon conseil : utilisez toujours un SGBDR. Mais ne vous limitez pas uniquement à un SGBDR. [Voss 42]

Les ORMs #

Concernant l’accès aux bases relationnelles, on oppose habituellement les requêtes SQL et l’utilisation d’ORM (Object-Relational Mapping).

Les ORMs offrent :

  • La possibilité de manipuler les données avec du code orienté objet, sans avoir besoin d’écrire de requêtes SQL ;
  • un moyen de mapper les informations stockées en base de données sur des objets (dits entités) ;
  • une couche d'abstraction permettant de passer d’un système de gestion de base de données à un autre de manière transparente.

Ils sont vus la plupart du temps comme un design pattern moderne et efficace, mais certaines personnes y voient un anti-pattern.

Je veux être très, très clair à ce sujet : Les ORMs sont une idée stupide.
L’ORM est né du fait que SQL semble moche et intimidant. Alors on balance une couche d’abstraction par-dessus, et on fait comme si le SGBDR n’existait même plus en dessous. C’est évidemment absurde. [Voss 42]
Un défenseur de l’ORM dira que tout le monde n’a pas besoin de faire des jointures complexes, que l’ORM est une solution “80/20”, où 80 % des utilisateurs n’ont besoin que de 20 % des fonctionnalités de SQL, et que l’ORM peut le gérer. Tout ce que je peux dire, c’est qu'au cours de mes quinze années de développement d’applications web utilisant des bases de données, ça n’a jamais été vrai pour moi. Ce n'est qu'au tout début d'un projet que l'on peut s'en sortir sans jointures ou avec des jointures naïves. [Voss 43]
L'ORM est un anti-modèle terrible qui viole tous les principes de la programmation orientée objet. Il n'y a aucune excuse à l'existence d'un ORM dans une application, que ce soit une petite application web ou un système d'entreprise avec des milliers de tables et des opérations CRUD.
J’affirme que l’idée même derrière les ORMs est fondamentalement fausse. [Bugayenko 44]
Chaque fois qu’on enveloppe une fonctionnalité complexe dans une nouvelle couche, on prend le risque d’accroitre la complexité globale lorsque cette couche se complexifiera. Cela s’accompagne souvent de fuites dans les abstractions : la couche n’arrive pas à envelopper complètement la fonctionnalité sous-jacente, et les développeurs doivent alors se débattre avec les deux couches en même temps. [Bendersky 45]
Personnellement, je pense que la seule solution viable au problème des ORM, c’est de trancher : soit on abandonne les bases relationnelles, soit on abandonne les objets. Si on retire le O ou le R de l’équation, le problème de mapping disparaît.
Les deux approches se défendent. J’ai tendance à pencher pour le camp de la base de données, parce que je trouve que les objets sont surestimés. [Atwood 46]

[Les ORMs] devraient être évités, car ils introduisent souvent des langages, des paradigmes et des systèmes entièrement nouveaux, mais n'apportent aucun avantage réel, que ce soit pour des mappings simples ou complexes. Dans le cas d'un mappage objet-relationnel simple, la tâche est simple et aucun outil n'est nécessaire. Dans les cas complexes, l'ORM ajoute de la complexité et une intervention manuelle est souvent nécessaire, ce qui annule l'intérêt de l'automatisation.

Le code SQL doit rester visible dans vos objets métier. Les développeurs ont besoin de comprendre ce qui se passe lorsqu’on récupère des objets ou qu’on manipule, en base, les données qui les représentent. [Maffey 47]

Les ORMs sont le diable dans tous les langages et toutes les implémentations. Écrivez simplement ce foutu SQL. [Kiehl 73]

Même le co-créateur de Propel (l’un des principaux ORMs PHP dans les années 2010) dit :

Personnellement, je n'utilise plus d'ORM. [Zaninotto 48]

Écrire des requêtes uniquement en PHP laisserait croire que le SQL est un langage inutile, qu'il faudrait le cacher au maximum. Pourtant, on ne cesse de dire que tout développeur doit connaître plusieurs langages, que c’est absolument nécessaire.

Apprendre plusieurs langages est une excellente idée — non seulement cela vous donne une plus grande flexibilité dans votre recherche d’emploi, mais cela élargit votre esprit et votre vision de ce qu’est la programmation, tout simplement. [Martelli 49]

Pourquoi le SQL serait le seul langage qui ne mérite pas de considération ? C’est pourtant un langage puissant, qui a prouvé sa stabilité au fil des ans et son efficacité pour manier des données relationnelles.

Aucun autre langage informatique n'est resté aussi populaire et n'a constamment étendu sa portée depuis 50 ans. En tant que développeurs, nous passons notre temps à apprendre de nouveaux langages, variantes et concepts pour rester à jour. Rien ne reste figé. SQL est une exception magnifique et rafraîchissante à ce chaos. L’apprendre, c’est probablement acquérir l’une des seules compétences techniques sur laquelle on pourra compter pour rester utile, pertinente et portable pendant longtemps.
SQL est puissant. Grâce à sa syntaxe essentiellement déclarative, ce qu’on exprime en trois lignes de SQL peut demander 20 à 30 lignes dans un langage procédural. [Maffey 47]
SQL est presque toujours le meilleur moyen di'éviter de tout réapprendre avec un nouvel outil qui, tôt ou tard, finira inévitablement par vous ralentir ou par ne simplement pas fonctionner du tout à long terme. [Righetti 50]
Il est difficile de rivaliser avec des décennies de recherche et d’améliorations sur les bases de données relationnelles. [Kiehl 73]

Le fait que les ORM mappent les données sur des objets est séduisant quand on veut développer avec une approche “tout objet”. Mais nous avons vu plus haut que ce type d’approche n’est pas souhaitable en soi.

Surtout, en ne manipulant que des objets, les développeurs perdent de vue la matérialité des données en base, avec trois effets négatifs :

  • L’ORM ne sachant pas comment on utilise les données, il récupère tous les champs des tables, même ceux dont on n’a pas besoin, amenant à plus de consommation mémoire et plus de latence dans les échanges de données. Et si l’ORM n’a pas la connaissance des relations entre les tables, on peut vite se retrouver à faire plusieurs requêtes là où une seule requête avec plusieurs jointures suffirait.
  • Dans un code applicatif, les entités se retrouvent à être manipulées sans connaissance du schéma de la base. Un développeur peut accéder aux propriétés des entités récupérées sans se rendre compte que chaque accès déclenche − de manière invisible − des requêtes supplémentaires. Un code mal maîtrisé peut ainsi générer des centaines de requêtes, juste parce que les développeurs font confiance aux objets sans savoir ce qu’il se passe derrière.
    Votre ORM est un pistolet pointé vers votre pied, qui attend tranquillement que quelqu’un appuie sur la détente.
  • En ne connaissant pas le schéma de la base, et en ne sachant pas faire des requêtes SQL, il devient impossible de tester convenablement un développement en vérifiant les données enregistrées. De la même manière, un débogage portant sur les données devient un véritable calvaire.

Les performances des ORMs présentent de sérieuses limitations. S’ils fonctionnent bien sur des cas simples, ils ont du mal à optimiser leurs traitements lorsqu’il faudrait générer des requêtes complexes. On se retrouve alors à faire plusieurs requêtes là où une seule suffirait.
La réponse apportée à cette problématique est d’écrire des requêtes dans un langage proche du SQL, faisant perdre le prétendu avantage de ne pas avoir besoin de connaître la syntaxe SQL ni le schéma de la base de données sous-jacente.

Cela nous amène naturellement à un autre problème des ORM : l’inefficacité. Quand vous récupérez un objet, de quelles propriétés (colonnes de la table) avez-vous besoin ? L’ORM ne peut pas le deviner, donc il les récupère toutes (ou il vous demande de les spécifier, ce qui casse l’abstraction). Au départ, ce n'est pas un problème, mais lorsque vous récupérez un millier d'enregistrements à la fois, la récupération de 30 colonnes alors que vous n'en avez besoin que de 3 devient une source pernicieuse d'inefficacité. De nombreux ORMs sont aussi particulièrement mauvais pour déduire les jointures et se rabattent sur des dizaines de requêtes individuelles pour récupérer les objets liés. Comme mentionné plus haut, de nombreux ORM reconnaissent eux-mêmes sacrifier l’efficacité, et certains proposent des mécanismes pour ajuster les requêtes problématiques. Leur manque de sensibilité au contexte les empêche de consolider les requêtes ; ils doivent alors se reposer sur la mise en cache ou d’autres artifices pour tenter de compenser. [Voss 43]

Les ORM encouragent de mauvaises pratiques, car il est très facile de s’appuyer sur la logique du langage hôte pour combiner des données.
Les ORM ne sont pas aussi efficaces que les requêtes SQL brutes. Ils sont souvent un peu moins performants, et dans certains cas, nettement inefficaces.

Le premier problème, c’est la surcharge massive de calcul que certains ORM génèrent en convertissant les requêtes en objets.
Le deuxième problème, c’est qu’ils effectuent parfois plusieurs allers-retours vers la base de données en bouclant sur des relations one-to-many ou many-to-many. C’est ce qu’on appelle le problème N+1 (1 requête principale + N sous-requêtes).

Mais le plus gros problème, c’est le manque de visibilité. Comme un ORM agit comme un générateur de requêtes, il ne joue pas le rôle de répartiteur d’erreurs en dehors de scénarios très simples (comme un type primitif invalide). Dans tous les autres cas, l’ORM doit interpréter l’erreur SQL renvoyée et la traduire pour l’utilisateur. [Chuong 51]

Sans compter que les ORMs ne tirent pas parti de fonctionnalités basiques des bases SQL, comme les vues, les procédures stockées ou les triggers, qui peuvent servir à améliorer les performances d’une base et la cohérence de ses données.

De plus, l’utilisation d’ORM induit une hétérogénéité entre les différents accès aux données. Les entités se manipulent d’une manière propre à chaque ORM (avec des différences importantes selon qu’il utilise le patron de conception Active Record ou celui du Data Mapper). Si le code doit aussi accéder à d’autres sources de données (via une base noSQL ou une API, par exemple), il le fera avec une logique et une syntaxe complètement différentes.

Quant à l’argument de l’abstraction du SGBD, permettant d’en changer facilement, il est fallacieux. Les ORMs se calent sur le plus petit dénominateur commun. Changer de base de données n’a de sens que pour utiliser des fonctionnalités spécifiques fournies par un SGBD, et que les autres ne supporteraient pas ; mais dans ce cas, il y a de grandes chances que l’ORM ne supporte pas ces spécificités nativement, ou au moins pas complètement.

Concrètement, il est extrêmement rare qu’une entreprise change de base de données, tout en restant sur un périmètre fonctionnel pour lequel un ORM est suffisant.

Dans toute ma carrière de développeur, je n’ai jamais vu une entreprise migrer d’une base SQL vers une autre. [Maffey 49]

Et dans le cas où une plateforme aurait besoin de passer d’une base relationnelle à une base noSQL ou à une source de données par API, l’ensemble du code basé sur la présence de l’ORM doit être réécrit.

Un interfaçage plus simple avec les données peut offrir un accès plus unifié aux différents types de sources de données.

Les query Builders #

Face aux difficultés soulevées par les ORMs, certains avancent que les query builders sont une solution élégante. Ils servent à construire des requêtes uniquement avec du code.

Voici un exemple d’utilisation d’un query builder :

DB::table('Users')
    ->select('Groups.id')
    ->join('Groups', 'Groups.userId', 'Users.id')
    ->where('Users.created_at', '>', $date1)
    ->where('Groups.created_at', '<', $date2)
    ->get();

Et l’appel équivalent sans query builder :

DB::select(
    'SELECT Groups.id
     FROM Users
          INNER JOIN Groups ON (Groups.userId = Users.id)
     WHERE Users.created_at > :date1
       AND Groups.created_at < :date2',
    ['date1' => $date1, 'date2' => $date2]
);

On peut voir que le query builder n’est pas plus lisible ou compréhensible que la requête SQL, et qu’il ne fait − au mieux − que tenter de reprendre la sémantique SQL avec du code.

L’abstraction est extrêmement limitée, il faut connaître le SQL et avoir une idée de la requête finale pour utiliser correctement un query builder. Et pour des requêtes plus complexes, il y a peu de chances que le query builder permette autant de souplesse que le SQL natif.

Je me remets à écrire de bonnes vieilles requêtes SQL et je suis surpris que cela m'ait pris autant de temps. « Pourquoi ? », vous demandez-vous peut-être. Eh bien, principalement pour les raisons suivantes :

  • On ne retrouve pas 100 % de l’expressivité du SQL
  • Ça ne fonctionne tout simplement pas pour les requêtes complexes, ou ça perd tout son intérêt
[Righetti 52]

Repenser le modèle avec la conception orientée données #

Le paradigme de la conception orientée données (ou Data Oriented Design − DOD) est apparu dans le cadre du développement de jeux vidéo en C++. Le but était alors de casser le modèle objet traditionnel en utilisant des structure de données plus efficace, garantissant notamment une meilleure utilisation du cache processeur.

Ses principes sont de recentrer le développement autour des données, qui deviennent l’élément le plus important du code, et qui sont traitées par un flux d’exécution clair et explicite.

L’objectif de tout programme, et de chacune de ses parties, est de transformer des données d’une forme à une autre.
Si vous ne comprenez pas les données, vous ne comprenez pas le problème.
Si vous ne comprenez pas le coût de résolution du problème, vous ne comprenez pas le problème.
Tout est un problème de données. Y compris l’ergonomie, la maintenance, le débogage, etc.

Les trois grands mensonges :

  1. Le logiciel est une plateforme en soi.
  2. Le code doit être conçu autour de modèles mentaux du monde réel.
  3. Le code est plus important que les données.
[Acton 53]

La conception orientée données déplace la perspective. On ne programme plus autour des objets, mais autour des données elles-mêmes : leur type, leur disposition en mémoire, et la manière dont elles seront lues et traitées.

La programmation, par définition, consiste à transformer des données : c’est l’acte de créer une séquence d’instructions machine pour traiter des données en entrée et produire un résultat en sortie. [Llopis 54]

La programmation DOD donne la priorité à l’organisation efficace des données, à l’optimisation des performances et à la montée en charge. Contrairement au paradigme OOP traditionnel, centré sur les objets et leurs comportements, la programmation DOD met fortement l’accent sur les données et leur manipulation. En se concentrant sur les données, la programmation DOD offre une perspective unique qui s'aligne parfaitement sur la nature intensive en données des projets modernes.

Les grands principes sont les suivants :

  • Conception centrée sur les données : Les applications sont principalement structurées autour du modèle du domaine de données, des relations et des schémas d'accès, plutôt que de se concentrer sur des abstractions ou des hiérarchies orientées objet.
  • Flux de données explicite : Le parcours des données dans le système est clairement visible dans le code, via des structures de données partagées et des fonctions sans état. Les données circulent entre les composants, plutôt qu’au travers d’appels de méthodes imbriqués.
  • Faible couplage : En privilégiant des découpages orientés données plutôt qu’entités, le code DOD favorise naturellement des fonctions et composants faiblement couplés, indépendants, plus faciles à tester, à comprendre, et à paralléliser.
[Dang 55]

Cela diffère profondément de l’approche strictement orientée objet. Le code est sans état, il sert à traiter et transformer les données lorsque cela est nécessaire.

Parce que la conception orientée données place les données au premier plan, on peut structurer l’ensemble du programme autour du format de données idéal. On n’atteindra pas toujours cet idéal, mais cela reste l’objectif principal. Et une fois qu’il est atteint, la plupart des problèmes tendent à disparaître.

La conception orientée données améliore à la fois les performances et la facilité de développement. Quand on écrit du code spécifiquement pour transformer des données, on obtient de petites fonctions, avec très peu de dépendances sur d'autres parties du code. Le code devient plus plat, composé de nombreuses fonctions “feuilles”, indépendantes les unes des autres. Ce niveau de modularité, et cette absence de dépendances, rendent le code bien plus simple à comprendre, à remplacer et à faire évoluer. [Llopis 54]

Les approches OOP traditionnelles tendent à modéliser les concepts ou abstractions du monde réel sous forme d'« objets » qui encapsulent à la fois les données et le comportement. À l’inverse, le DOD sépare les structures de données de la logique métier, et met l’accent sur l’organisation et le traitement des données de façon optimisée pour le cache. Ce changement de perspective conduit à des programmes DOD qui privilégient :

  • Des structures de données indépendantes plutôt que des types d’objets regroupés.
  • Des fonctions isolées et sans état plutôt que des méthodes.
  • Le passage de données par valeur (immuables) au lieu de références d'objets.
  • Éviter le partage d'états mutables entre les composants.
[Dang 55]
  • Séparer les données de la logique
    • Les données sont vues comme des informations qui doivent être transformées.
  • La logique englobe les données
    • Elle n'essaie pas de les cacher
    • Elle conduit à des fonctions qui opèrent sur des tableaux
  • Réorganise les données en fonction de leur utilisation
    • Si une donnée ne sert à rien, pourquoi l’embarquer ?
  • Évite les “états cachés”
[Nikolov 56]

Au final, ce modèle peut être étendu à d’autres domaines que les jeux vidéo, et notamment au développement web.

Souvenez-vous que le rôle de la couche modèle n’est pas de représenter des objets, mais de répondre à des questions. Fournissez une API qui réponde aux besoins de votre application de la manière la plus simple et la plus efficace possible. Parfois, ces réponses seront extrêmement spécifiques, au point de sembler “mal conçues” même aux yeux d’un développeur orienté objet chevronné ; mais avec l’expérience, vous apprendrez à repérer des points communs qui vous permettront de regrouper plusieurs méthodes de requête en une seule. [Voss 43]

Frameworks : De bons serviteurs mais de mauvais maîtres #

Au fait, c’est quoi un framework ? #

Wikipedia donne une définition des frameworks :

Les frameworks sont largement utilisés pour leur capacité à améliorer la productivité des développeurs, à proposer des modèles structurés adaptés aux applications à grande échelle, à simplifier la gestion des cas particuliers, et à offrir des outils d’optimisation des performances. [Wikipedia 57]

Mozilla en fournit une plus complète :

Les frameworks web côté serveur (ou « frameworks d’applications web ») sont des environnements logiciels conçus pour faciliter l’écriture, la maintenance et la montée en charge des applications web. Ils proposent des outils et des bibliothèques qui simplifient les tâches courantes du développement web, dont le routage des URLs, l’accès aux bases de données, la gestion des sessions et des autorisations utilisateurs, le formatage des réponses (HTML, JSON, XML, etc.), ainsi que le renforcement de la sécurité face aux attaques web. [Mozilla 58]

Pour expliquer la différence entre une bibliothèque et un framework, on dit souvent que le code métier appelle les bibliothèques, mais que c’est le framework qui appelle le code métier.

Un peu d’histoire #

L’histoire des frameworks est maintenant longue, et peut être résumée en trois grandes phases.

Les premiers frameworks sont apparus dans la seconde moitié des années 90 (ColdFusion en 1995, WebObjects en 1996, WebLogic en 1997, WebSphere en 1998, Java EE en 1999). Ils s’adressaient principalement aux grosses organisations comme les banques et les premiers sites e-commerce (la licence WebObjects coûtait $50,000 avant l’an 2000). Ils se caractérisaient par des infrastructures complexes et l’utilisation généralisée du langage Java.
L’écosystème Java EE s’est ensuite consolidé − et complexifié − avec des composants comme Tomcat, Struts, Hibernate, JBoss, Spring…

En réponse, des frameworks plus simples sont apparus dans les années 2000, basés sur des langages de script au typage moins strict, permettant un développement plus rapide (Ruby on Rails en 2004 ; CakePHP, Django puis Symfony en 2005 ; CodeIgniter et Zend Framework en 2006 ; Temma en 2007).
À l’époque, l’environnement Java était présenté comme coûtant cher aux entreprises à cause de sa complexité qui allongeait les temps de développement, porté par des entreprises qui avaient tout à gagner à créer un tel écosystème − que ce soit pour vendre des solutions, de la formation ou du temps de développement.

Dans les années 2010, le monde PHP a cherché à se crédibiliser via une “professionnalisation” des frameworks, Cela s’est fait en s’inspirant de plus en plus du monde Java.
Cette démarche a eu plusieurs effets :

  • Une standardisation des pratiques, aboutissant à des projets plus homogènes, et surtout une diminution drastique des projets développés “n’importe comment”.
  • Une complexification de ce qui est présenté comme le développement “normal” en PHP, avec une courbe d’apprentissage ardue.

En entrant en compétition avec les technologies Java, l’écosystème PHP a laissé le champ libre au Python et à Node.js pour les développements légers et rapides, qui sont devenus les nouvelles plateformes “cool”.

Dans les années 2000, il était reproché au monde Java d’être supporté par des entreprises, et que la complexité ainsi que les évolutions permanentes leur permettait de vendre de la prestation de service et de la formation.
On peut remarquer que les principaux projets du monde PHP sont aussi supportés par des entreprises, qui ont leurs propres buts.

Faut-il remettre en cause les frameworks ? #

Comme pour toutes les bonnes pratiques informatiques, il est bon de se demander si une recommandation faite par une grosse entreprise s’applique à soi ; une pratique qui fonctionne ailleurs peut vous causer plus de problèmes qu’elle n’en résoudra. Si un framework est utilisé par des équipes de plusieurs centaines de personnes, est-ce vraiment celle dont votre équipe de 12 personnes a besoin ?

Des problèmes différents exigent des solutions différentes.
Résoudre des problèmes que vous n’avez pas, c’est en créer d’autres que vous aurez pour de bon. [Acton 53]
Il est essentiel de se rappeler que les créateurs de frameworks ont leurs propres priorités. Ils résolvent LEURS problèmes, pas les vôtres. Les géants de la tech ont montré la voie en créant, puis en mettant sous licence libre, des outils que nous utilisons aujourd’hui. Mais ces outils n’ont pas été conçus pour être universels. Ils répondent à des besoins très spécifiques que la plupart des entreprises ne rencontreront jamais. [Bobrov 60]

Car si les frameworks sont devenus incontournables dans le monde PHP, des voix commencent à remettre en question le dogme imposé par les gros frameworks complexes. Que ce soit le carcan de développement, la difficulté de monter rapidement en compétence, ou le rythme rapide de mises à jour qui impose des changements incessants de code, il semble sain de revoir les habitudes.

Les frameworks fournissent des gabarits et des outils préconçus qui simplifient de nombreuses tâches de développement. Utilisés aussi bien par les startups que par les géants de la tech, ils sont largement plébiscités pour leur capacité à rationaliser les workflows et à standardiser les solutions.
Mais à mesure que leur usage se généralise, les inquiétudes grandissent quant à leur utilisation excessive et aux inconvénients potentiels pour les développeurs et les logiciels. Les frameworks sont-ils une solution miracle ou un frein silencieux à la créativité et à la progression technique ?

Un développeur travaillant exclusivement avec des frameworks risque de manquer de compréhension plus approfondie des langages de programmation et des principes de base pour résoudre des problèmes plus complexes.
Les frameworks imposent des conventions et des patterns strictes. C’est très efficace pour réduire la complexité mais cela peut aussi brider la créativité. Les règles du framework finissent souvent par restreindre les développeurs et limiter leur capacité à innover.
La grande variété de cadres disponibles constitue souvent un défi pour les développeurs qui essaient de se tenir au courant des derniers outils, les épuisant par la nécessité constante de s'adapter. [.NET Expert blog 59]

Les frameworks seraient-ils… surfaits ? Tout un écosystème pousse les développeurs à les adopter, et ils réalisent, après avoir sauté le pas, qu’ils sont enfermés dans des plateformes coûteuses. Cela ressemble un peu à un piège, n'est-ce pas ?

Dernièrement, une forme de rébellion gronde, les gens commencent à en avoir assez des frameworks. Les développeurs en ont assez des changements constants. Vous l’avez sûrement vécu : à chaque mise à jour majeure, il faut réécrire une grosse partie de votre code pour rester dans la course. Et je ne vous parle même pas du cycle sans fin des ruptures de compatibilité.

Cette frustration a donné lieu à un regain d'intérêt pour des piles plus simples et plus stables chez les développeurs qui privilégient la productivité plutôt que de rester à la pointe de la technologie. Oui, cela peut sembler un peu “vieux jeu”, mais c'est loin d'être obsolète. Avec une stack plus simple, on peut itérer rapidement et livrer encore plus vite. Parfois, vous n'avez pas besoin de ce qui est à la mode. Parfois, s'en tenir à ce qui fonctionne peut vous épargner bien des maux de tête. [Bobrov 60]

Évitez à tout prix d’utiliser un framework, car il causeront plus tard plus de problèmes qu'il n'apporteront de solutions.

Si vous vous reposez trop sur un framework, vous risquez de perdre l'occasion d'apprendre le langage sous-jacent. Avec un framework, vous n'interagissez qu'avec les niveaux supérieurs du système et avez moins de chances de résoudre des problèmes complexes.

Tout framework embarque une multitude d’outils, de bibliothèques et de fonctionnalités pour couvrir un large éventail de cas d’usage, mais qui sont sans intérêt pour votre projet. Lorsque vous développez des applications web simples, ce code inutile nuit aux performances globales. [TechAffinity blog 61]

Une dépendance excessive aux bibliothèques et frameworks peut entraîner un manque de compréhension du code et des principes sous-jacents. Cela limite la flexibilité et complique l’adaptation du code aux besoins spécifiques d’un projet.

Utiliser trop de bibliothèques ou de frameworks complexifie la base de code, la rendant plus difficile à maintenir et à déboguer avec le temps. C’est discutable, mais dans certains cas, ces outils peuvent nuire aux performances comparé à des solutions personnalisées, taillées sur mesure pour un cas d’usage précis.

Les bibliothèques et frameworks peuvent devenir obsolètes au fil du temps, ce qui entraîne des problèmes de compatibilité et la nécessité de réécrire une grande partie du code pour rester à jour de l’évolution des technologies. [Mishra 62]

Chaque framework impose sa propre manière de faire les choses. Pour pouvoir maintenir un projet, un développeur PHP ne suffit pas, il faut obligatoirement un développeur Laravel/Symfony/CodeIgniter… Et même un développeur expérimenté peut ne pas connaître toutes les fonctionnalités associées de près ou de loin avec un framework, et avoir des difficultés à comprendre une base de code.

Car les frameworks ajoutent des abstractions qui masquent les fonctionnalités natives du langage de programmation. Une première fonctionnalité fait gagner du temps sur des développements simplistes ; puis une seconde fonctionnalité s’appuie sur la première, et ainsi de suite jusqu’à ce que l’ensemble devienne un empilement complexe de dépendances.
Un jour, on s’aperçoit que la première fonctionnalité est inutile parce qu’on a dépassé son cas d’usage optimal ; et pourtant, on est obligé de garder l’intégralité de la pile technologique, parce qu’elle s’est infiltrée dans tous les aspects du développement et a des ramifications dans tout le code.

En réalité, mes anciens projets CodeIgniter sont aujourd’hui tellement dépendants du framework qu’il est devenu très difficile d’ajouter quoi que ce soit de nouveau… même des choses aussi simples que des tests ! Donc si un jour je veux (ou dois) m’en détacher, ça va être compliqué. Et le souci ne s’arrête pas là ; si, par exemple, le moteur de templates par défaut du framework ne convient plus au projet, s’en débarrasser n’a rien d’évident.

Quand on choisit un framework full-stack, on choisit aussi de lier son projet à ce framework. Oui, on peut théoriquement construire un projet découplé du framework, mais ce n’est clairement pas la voie la plus courante, ni la plus facile. [Junior 63]

Et les micro-frameworks ? #

Les microframeworks peuvent sembler être une alternative séduisante aux gros frameworks. Mais si leur extrême simplicité est facile à appréhender, elle a du mal à passer l’épreuve du feu des projets réels. Le mélange des routes avec le code a du mal à scaler, et l’ajout de fonctionnalités nécessaires peut devenir outrageusement compliqué.

Eh bien, pour les très, très petites applications, sans connexion à une base de données, sans consommation d'Apis et fondamentalement sans plus de quelques lignes de code pour chaque route (ou du prototypage avec des données fictives), les microframeworks seront vraiment plus faciles à utiliser et ne nécessiteront pas des tonnes de fichiers de configuration et d'autres services en cours d'exécution comme l'exigent certains frameworks full-stack. [Junior 63]
En raison de la nature plutôt déclarative des microframeworks et de la correspondance généralement 1:1 d'une route à un contrôleur, les microframeworks n'incitent pas à la réutilisation de code. Cela s'étend à la manière dont les applications basées sur un microframework sont organisées : en général, il n'y a pas de conventions claires sur la manière d'organiser les routes et les contrôleurs, et encore moins de les séparer en plusieurs fichiers. Cela peut entraîner des problèmes de maintenance au fur et à mesure que l'application grandit, ainsi que des problèmes logistiques chaque fois que vous devez ajouter de nouvelles routes et de nouveaux contrôleurs. [O’Phinney 64]
J'aime beaucoup la syntaxe et le style du framework Echo de Labstack pour Golang. Mais mon expérience a changé lorsque j'ai ajouté une base de données à mon application. La simplicité s'est effondrée car l'ajout d'une variable globale rend les tests difficiles. Sans état, je n'ai pas ce problème. Il y a beaucoup de microframeworks où cela se produit. Vous pouvez presque le prévoir si vous regardez la table des matières de la documentation et que vous voyez qu'il n'y a rien sur la gestion de base de données. [Dillon 65]

Il existe de nombreux microframeworks dans tous les langages, mais tout a commencé avec Sinatra. Les compromis de Sinatra ne sautent pas aux yeux dans un simple Hello World. À mes yeux, la promesse de simplicité séduit surtout les développeurs juniors justement parce qu’ils ne perçoivent pas encore les compromis que cela implique. La surface de l'API est réduite, ce qui facilite l'apprentissage. Mais des choses déroutantes arrivent vite après. Dès qu’on sort du cas le plus basique, plein de questions émergent, et beaucoup d’incertitudes avec.

Au fil de la croissance du projet, ou simplement en continuant à travailler avec, on se retrouve à devoir corriger, chercher ou bricoler Sinatra par soi-même. Plus d’une fois, j’ai copié des fichiers entiers depuis un projet Rails par défaut. Des choses comme où placer les fichiers de config, les fixtures de test, ou le concept de dev/test/prod. [Dillon 66]

La pérennité des micro-framework est particulièrement discutable. Dans le monde PHP, on peut observer que Silex et Lumen ont disparu (en faveur des full-featured frameworks correspondants).

Les microframeworks se multiplient peut-être parce qu'il y a moins de choses à jeter ou parce qu'ils sont faciles à inventer ? C’est bien plus simple de lancer un microframework amateur, avec un périmètre réduit. On pourrait presque se permettre d’ignorer les microframeworks parce qu'il seront les premiers à disparaître. [Dillon 66]

La bonne approche #

Il semble aujourd’hui impossible de revenir en arrière et de refaire des sites sans framework. Lorsqu’ils sont bien utilisés, ils diminuent les temps de développement.

Mais il faut sélectionner un framework en fonction des besoins réels, pas en fonction des effets de mode. Il faut mettre le curseur au bon endroit, en choisissant un outil qui offre un cadre de développement, tout en laissant la possibilité d’écrire du code personnalisé.

Un framework doit être une aide et non une contrainte.

Bien que les frameworks soient excellents pour accélérer le développement, le code personnalisé est parfois la meilleure solution. Utilisez les frameworks pour leurs points forts (comme les templates, le routage et les fonctionnalités de base), mais faites preuve d'audace et de courage pour sortir hors de leurs limites. En alliant la maîtrise d'un framework avec des code sur-mesure, vous pourrez répondre efficacement à des besoins spécifiques. [.NET Expert blog 59]
N'oubliez pas que les frameworks doivent servir VOTRE architecture, et non la dicter. Grâce à une planification minutieuse et à une abstraction stratégique, vous pouvez récolter les avantages des frameworks sans vous laisser piéger par des dépendances à long terme. Le tout est de garder le contrôle. Alors la prochaine fois que vous vous apprêtez à plonger dans un framework, prenez un peu de recul et rappelez-vous : c'est vous qui décidez. [Bobrov 60]
Si vous estimez qu’une dépendance excessive aux bibliothèques et frameworks pose problème, envisagez d’autres approches comme le développement de solutions sur mesure, l'utilisation d'outils simples, la maîtrise des technologies de base, ou combiner des composants existants avec du code personnalisé. [Mishra 62]
Le problème est que de nombreux débutants s'habituent à un framework et ont tendance à l'utiliser pour tout. Si vous êtes un débutant, je vous recommande de construire des choses, beaucoup de choses, sans aucun framework. Faites un tas de projets “jouets”, puis testez plusieurs frameworks différents, et vous serez alors en mesure de faire de meilleurs choix. [Junior 63]

Tests automatisés: unitaires, d’intégration, fonctionnels — trouvez le bon équilibre #

Définitions rapides #

Dans le développement informatique, quand on parle de tests automatisés, cela concerne du code qui teste du code. On distingue trois grands types de tests automatisés :

  • Les tests unitaires servent à tester individuellement les objets métier. Pour chaque méthode publique de chaque objet, différents scénarios envoient des données (correctes ou incorrectes) en entrée et vérifient que le retour correspond à la sortie attendue. Les objets sont testés isolément, donc s’ils ont besoin d’autres objets, ceux-ci sont habituellement mockés (remplacés par de faux objets dont on maîtrise le comportement).
  • Les tests d’intégration permettent de tester les interactions entre plusieurs objets. Cela veut dire que les objets vont communiquer entre eux. Seules les dépendances externes restent mockées.
  • Les tests fonctionnels (aussi appelés tests end-to-end) testent la plateforme dans son ensemble, en simulant les accès d’un utilisateur (sur un site web) ou les connexions à une API.

Les tests automatisés sont une grande aide au développement agile. On peut modifier le code source sans avoir peur de briser une dépendance. Il suffit d’exécuter les tests pour se rendre compte d’une éventuelle régression du code, et la corriger au plus vite.

L’objectif des tests est de produire de l’information sur votre programme. (Les tests n’améliorent pas la qualité en soi ; ce sont la conception et le code qui le font. Les tests fournissent simplement les retours qui manquaient à l’équipe pour concevoir et implémenter correctement.) [Coplien 67]

Comment ça se passe en vrai #

On considère habituellement que les tests unitaires sont plus rapides à écrire et à exécuter que les tests fonctionnels [Modus Create blog 68]. En conséquence, il est conseillé de toujours tester unitairement chaque partie de son code [Twilio blog 69]. Le corollaire est qu’il est censé être préférable d’écrire beaucoup plus de tests unitaires que de tests fonctionnels [Modus Create blog 68].

Cela est vrai en théorie, mais la pratique peut être très différente.

Pour tester un objet unitairement, il est souvent possible d’écrire de très nombreux tests, qui vérifieront son comportement dans tous les cas possibles et imaginables. Il peut devenir difficile de savoir s’arrêter, de définir la limite au-delà de laquelle les tests deviennent excessifs.

On parle du taux de couverture des tests, mais c’est une mesure difficile à déterminer. Certaines personnes pensent qu’en dessous de 90% de code testé, cela ne sert à rien de faire des tests ; mais on sait bien qu’il vaut mieux un minimum de tests que pas de test du tout. Et même un taux de 100% ne veut pas dire grand chose ; cela peut vouloir dire que toutes les méthodes sont testées dans leur cas nominal, mais leurs cas d’erreurs sont-ils aussi vérifiés ?

Peu de développeurs admettent qu’ils ne font que des tests partiels ou aléatoires. Beaucoup ivous diront qu’ils font des tests complets, selon une idée plus ou moins vague de ce que cela signifie. Par exemple : « Chaque ligne de code a été vérifiée », ce qui, du point de vue de la théorie de l’informatique, est un non-sens total si l’on veut savoir si le code fait ce qu’il doit faire.

Les programmeurs croient tacitement qu'ils peuvent penser plus clairement (ou mieux deviner) en écrivant des tests qu'en écrivant du code, ou qu'il y a plus d'informations dans un test que dans le code. Il s'agit là d'un non-sens formel. [Coplien 67]

La couverture du code n'a absolument rien à voir avec la qualité du code (dans de nombreux cas, elle est inversement proportionnelle). [Kiehl 73]

L’utilisation de mocks est aussi très répandue, car très souvent nécessaire pour pouvoir faire des tests unitaires. Mais ces faux objets rendent parfois les tests plus longs à écrire, plus difficiles à maintenir, avec des résultats qui ne sont plus aussi fiables.

Quand on écrit des tests, il peut sembler plus simple d’ignorer les dépendances en les mockant. Mais parfois, ne pas utiliser de mocks permet d’écrire des tests plus simples et plus utiles.

L'utilisation excessive de mocks peut entraîner plusieurs problèmes : Les tests peuvent être plus difficiles à comprendre. Les tests peuvent être plus difficiles à maintenir. Les tests peuvent fournir moins d'assurance que votre code fonctionne correctement.
Si, en lisant un test avec des mocks, vous devez mentalement rejouer le code testé pour comprendre ce qu’il fait, c’est probablement que vous utilisez trop de mocks. [Trenk 70]

De l’autre côté, un test d’intégration ou un test fonctionnel va à lui seul valider plusieurs couches de code. Si un tel test tombe en échec, on peut alors se focaliser sur les objets par lesquels l’exécution est passée ; avec de bons rapports de log, l’erreur est rapide à identifier et donc à corriger.
Cela peut sembler moins rigoureux que d’avoir tous les objets testés unitairement. Mais c’est plus efficace, car on peut considérer que le temps gagné (en n’écrivant pas tous les tests unitaires possibles) sera supérieur au temps perdu (en recherchant l’origine précise d’une erreur).

À propos du Test Driven Development (TDD) #

La pratique du Test Driven Development (TDD) permet d’intégrer totalement l’écriture des tests unitaires au développement, en commençant par écrire les tests avant de produire le code. Cela les rend moins douloureux à écrire que lorsqu’ils arriven en bout de course, et ils participent d’une certaine manière à la spécification technique du développement : en définissant les critères d’acceptation d’un objet ou d’une méthode, on détermine finement son comportement attendu ; c’est le rôle d’une spécification technique.

L’un des avantages cachés du TDD, c’est qu’en ayant une telle spécification technique extrêmement cartésienne, la phase de développement est plus courte, car on sait où on doit aller. Il y a moins d’errements et de tâtonnements pour mener le développement à bien.

Toutefois, le TDD n’est pas non plus une panacée absolue. Il ne s’applique pas sur du code existant, et il se limite aux tests unitaires qui − comme vu précédemment − ne sont pas les seuls tests à mettre en œuvre.
Et il y a des situations pour lesquelles le TDD ne s’applique pas, notamment lorsqu’il n’y a pas de spécification suffisamment précise, et que le développement joue donc un rôle exploratoire.

Le fondamentalisme du “test d’abord”, c’est comme l’éducation sexuelle fondée sur l’abstinence : Une campagne de moralisation irréaliste et inefficace, qui nourrit la culpabilité et le dégoût de soi. Le fanatisme actuel autour du TDD pousse à se concentrer uniquement sur les tests unitaires. Je ne pense pas que ce soit sain. Cette approche “test d'abord” mène à un enchevêtrement d’objets intermédiaires et d’indirections inutiles. [DHH 71]

Quelle stratégie appliquer ? #

Au final, comme toujours, il faut mettre en place une stratégie intelligente, sans fondamentalisme, qui tire parti de tous les outils disponibles :

  • Faire des tests unitaires sur les couches les plus fondamentales et importantes du code, pour s’assurer de leur stabilité.
  • Faire des tests d’intégration et des tests fonctionnels pour valider l’ensemble du code applicatif, sans risquer d’avoir des zones d’ombre qui passeraient sous les radars.

Les tests automatisés doivent être maintenus à jour au fil des modifications du code. Cette maintenance à un coût.

Intérioriser les avantages des tests n'est que le premier pas vers l'illumination. Savoir ce qu'il ne faut pas tester est le plus difficile.

Les tests ne sont pas gratuits. Chaque ligne de code que vous écrivez a un coût. Il faut du temps pour l'écrire, il faut du temps pour le mettre à jour et il faut du temps pour le lire et le comprendre. Il faut donc que le bénéfice obtenu soit supérieur au coût de réalisation. Dans le cas des tests excessifs, par définition ce n'est pas le cas. [DHH 72]

Les tests à faible risque ont des retombées faibles (voire potentiellement négatives). [Coplien 67]

Dans une base de code, tous les objets n’évoluent pas au même rythme. Certains bougent très peu, et leurs tests unitaires seront facilement rentabilisés. Ce sont souvent les couches les plus stables du code, celles dont il faut justement s’assurer que les régressions soient détectées le plus rapidement possible.
Pour les objets qui évoluent rapidement, maintenir une couverture de test de 100% peut devenir très coûteux. Cela peut concerner les objets les plus proches du rendu utilisateur, par exemple. Dans ce cas, on fera un minimum de tests unitaires, et on complètera avec des tests d’intégration qui permettront de couvrir les erreurs tout en étant moins coûteux en maintenance.

Sans oublier que, dans un monde idéal, les tests automatisés doivent être complétés par des tests humains, effectués par des personnes différentes de celles qui font les développements, pour identifier des bugs auxquels les développeurs ne penseraient pas.

Microservices: Il est très peu probable que vous en ayez besoin #

La raison d’être des architectures à microservices #

Les architectures à microservices sont arrivées en réponse à deux problématiques bien particulières : la montée en charge et le développement collaboratif à grande échelle.

  • Pour la montée en charge, quand un serveur atteignait vite ses limites, il devenait nécessaire de faire de la scalabilité horizontale, en repensant les applications de manière à ce qu’elles soient répartissables et redondables sur plusieurs serveurs.
  • Pour le développement collaboratif, il fallait faire en sorte que des équipes de plusieurs centaines de développeurs puissent faire évoluer une application sans se marcher sur les pieds les uns les autres, et sans qu’une modification à un endroit puisse causer des bugs de régression ailleurs.

La solution a donc été de découper finement les applications, en rendant leurs composants les plus autonomes possibles les uns des autres. Ainsi, des équipes peuvent prendre en charge le développement de chaque partie séparément ; et des ressources serveur peuvent être allouées à chaque brique sans impacter les autres.

Ça marche très bien, et en fait, dans certains cas, c’est absolument obligatoire.

L’architecture orientée microservices consiste à découper une application en une multitude de petites parties, à exécuter chacune d’elles comme une application autonome, puis à laisser cette constellation résoudre le grand problème que vous cherchez à traiter.

Il s'agit d'un excellent modèle. Si vous êtes Amazon, Google ou toute autre organisation logicielle comptant des milliers de développeurs, c'est un excellent moyen de paralléliser les possibilités d'amélioration. À partir d'une certaine échelle, il n'y a tout simplement aucun autre moyen raisonnable de coordonner les efforts. [DHH 74]

Les inconvénients des microservices #

Le problème, c’est que − comme d’habitude − certaines personnes considèrent que si cette pratique est bonne dans certaines conditions, elle l’est dans toutes les conditions. Et le développement monolithique est alors vu comme dépassé.

C’est évidemment une erreur. Découpler une architecture en plusieurs composants distincts n’a aucun sens pour une petite application :

  • Cela prend plus de temps à développer. Dans une application monolithique, les objets se connaissent et se parlent directement, sans avoir besoin de mettre en place des couches d’abstraction et d’interfaçage.
  • C’est beaucoup plus compliqué à tester et à déboguer. Chaque composant est facile à tester de manière unitaire, bien sûr. Mais lorsque vous voulez tester tous les composants ensemble, vous verrez apparaître des bugs aléatoires, soit parce qu’il y a des interactions qui n’ont pas été anticipées, soit parce que c’est la communication entre les composants qui entraîne des bugs.
  • C’est plus gourmand en ressources. Là où une application monolithique tournera efficacement sur un petit serveur, une architecture à microservices demandera plus de puissance et coûtera beaucoup plus cher.

Le problème, quand on transforme trop tôt une application en une constellation de services, c’est qu’on viole la règle n°1 de l’informatique distribuée : Ne distribuez pas votre informatique ! Du moins, si vous pouvez l’éviter.

Chaque fois que vous transformez une collaboration entre objets en une collaboration entre systèmes, vous vous exposez à un monde de douleur avec une myriade de responsabilités et d'états d'échec. Que faire lorsque les services sont en panne, comment migrer de concert, et toutes les difficultés liées à l'exploitation de nombreux services en premier lieu. [DHH 74]

Car les illusions de l’informatique distribuée sont bien connues depuis les années 90 [Wikipedia 75] :

  1. Le réseau est fiable.
  2. Le temps de latence est nul.
  3. La bande passante est infinie.
  4. Le réseau est sûr.
  5. La topologie du réseau ne change pas.
  6. Il y a un et un seul administrateur réseau.
  7. Le coût de transport est nul.
  8. Le réseau est homogène.

Donc à moins d’être dans l’un des cas très particuliers pour lesquels une architecture à microservices est utile, restez sur une architecture monolithique. C’est éprouvé, c’est fiable, c’est maintenable et c’est scalable.

Le monolithique reste plutôt bon. [Kiehl 73]
La grande majorité des applications web devraient démarrer sous la forme d’un monolithe majestueux : une base de code unique qui prend en charge tout ce que l’application doit faire. C’est l’opposé d’une constellation de services, qu'ils soient micro ou macro, qui morcelle l’application en une série de petits îlots, chacun responsable d’une partie du tout. [DHH 76]
Vous devez être grands comme ça pour utiliser les microservices.
Les développeurs aiment travailler avec de petites unités, en espérant une meilleure modularité qu’avec un monolithe. Mais comme pour toute décision architecturale, il y a des compromis à faire. Les microservices ont de graves conséquences pour les opérations, qui doivent désormais gérer un écosystème de petits services plutôt qu'un monolithe unique et bien défini. Autrement dit, si vous ne maîtrisez pas un certain nombre de compétences de base, n’envisagez même pas d’adopter une architecture à base de microservices. [Fowler 77]
L’équipe Prime Video d’Amazon a publié une étude de cas assez remarquable sur sa décision d’abandonner son architecture microservices sans serveur, et de la remplacer par un monolithe. Cette décision a conduit à une économie spectaculaire de 90% (!!) sur les coûts d'exploitation, et a rendu leur système plus simple à gérer. Quelle victoire ! |DHH 78]

Au-delà du monolithe #

Si une partie de votre application devient complexe au point de faire peser le poids de sa complexité sur l’ensemble de l’application, ce n’est pas une raison pour basculer vers une architecture à microservices. Sortez simplement cette partie, pour la gérer séparément, mais sans altérer pour autant le reste du monolithe.

C’est ce que David Heinemeier Hansson appelle “la citadelle” :

L'étape suivante est la Citadelle, qui maintient le monolithe majestueux au centre, mais le soutient avec un ensemble d'avant-postes, chacun prenant en charge un petit sous-ensemble de responsabilités de l'application. Ces avant-postes permette au monolithe majestueux de déléguer certains comportements divergents, que ce soit pour des raisons d'organisation, de performance ou de mise en œuvre.

On n’a pas tenté de découper toute l’application en microservices écrits chacun dans un langage différent. Non, on a juste extrait un seul avant-poste. C’est ça, une architecture Citadelle.

Alors que de plus en plus de personnes réalisent que la course aux microservices s'est terminée dans une impasse, le pendule va balancer dans l'autre sens. Le monolithe majestueux est toujours là, attendant les réfugiés des microservices. Et la Citadelle est là pour leur offrir une vraie tranquillité d’esprit : le modèle saura évoluer si, un jour, leur application a besoin de méga-scalabilité. [DHH 76]

APIs : Bousculez les habitudes #

Il existe plusieurs manières de créer des API. Mais là encore, les élans successifs ont amené à une standardisation d’une inutile complexité. Il est nécessaire de se poser quelques questions, pour créer des API plus faciles à développer et à maintenir.

La simplicité d’un webhook lorsque c’est possible #

Que l’on parle d’un webhook ou d’une API, on en revient à faire une requête HTTPS sur une URL, en envoyant des données et en en récupérant d’autres. La différence entre les deux est plus philosophique que technique.

Les webhooks sont habituellement présentés comme faisant partie d’un sous-ensemble des APIs (ou carrément des “sous-APIs”) qui ne s’utilisent que de manière événementielle.

À la base, le terme désignait une URL que l’on fournit à un système distant, et qu’il appellera pour notifier qu’un événement a eu lieu. C’est plus efficace que de se connecter régulièrement à une API en demandant si l’événement en question a eu lieu.

Depuis, le terme a été élargi pour désigner de simples URLs vers lesquelles il est possible d’envoyer des données en POST (plus rarement en GET), et de récupérer des données en réponse − la plupart du temps au format JSON.
Un webhook est alors une URL unique qui contient tout ce qui est nécessaire pour être utilisée directement ; pas de mécanisme compliqué d’authentification, peu d’options.

L’exemple typique est la messagerie d’entreprise qui offre une API complète donnant accès à toutes ses fonctionnalités, mais aussi des webhooks permettant d’envoyer facilement des messages dans des salons de discussion.

Pour faire simple, ne créez pas une API lorsqu’un webhook se montre suffisant. Cela complexifierait inutilement les choses − aussi bien le développement de votre côté que celui des clients qui se connectent à votre système.

De très nombreux services proposent des webhooks, seuls ou en complément d’une API REST : Slack, Discord, Twilio, Stripe, PayPal, GitHub, GitLab, IFTTT, GoCardless

Agir comme un traitement de formulaire pour les données simples #

Quand on doit traiter des données entrantes (que ce soit via un webhook ou une API), on a l’habitude les sérialiser dans un format standard − du JSON la plupart du temps. Et ça fonctionne globalement bien.

Lorsque les données entrantes ne présentent pas de structures complexes, il est encore plus simple de les recevoir comme si elles avaient été envoyées par un formulaire HTML :

  • Tous les langages de programmation permettent d’envoyer des données en tant que paramètres POST, encore plus facilement que pour envoyer un payload JSON.
  • La réception des données se fait sans aucun traitement de désérialisation.
  • Dans le cas d’un webhook, il est possible de le tester avec juste une page comportant un formulaire, permettant de saisir les valeurs à la main et tester le retour.

Plusieurs services reçoivent les données par des paramètres GET ou POST, pour une partie ou la totalité de leur API : Twilio, Vonage, OpenWeatherMap, Google Maps Static, Pingdom

Faire des appels à procédures distantes plutôt que du REST #

Dans l’histoire de l’informatique distribuée, l’architecture REST est plus récente que l’approche RPC.

L’approche RPC consiste à appeler des méthodes appartenant à des objets distants, en leur fournissant des paramètres, et qui envoient un résultat en retour.
La philosophie REST se base sur la notion de ressources, qui sont manipulées au travers d’un nombre limité d’opérations, lesquelles se matérialisent au travers des méthodes HTTP GET (lecture), POST (création), PUT (remplacement), PATCH (modification) et DELETE (effacement). Donc des opérations CRUD de base.

Il y a un grand nombre de cas pour lesquels l’approche REST fonctionne merveilleusement bien. Le souci, encore une fois, est que c’est devenu une bonne pratique qui s’est tellement généralisée que certaines personnes pensent que si vous dérogez au credo REST, ça veut dire que vous ne savez pas faire une « vraie API ».

Prenons un exemple concret. Imaginons un système de discussion.
Si vous voulez récupérer la liste des salons de discussion, le REST fonctionnera très bien. Vous vous connecterez à l’URL :
GET /api/channels

Pour récupérer les messages d’un salon (dont l’identifiant est 123) :
GET /api/channels/123/messages

Maintenant, pour abonner un utilisateur (identifiant 789) au salon, on pourrait imaginer faire l’appel suivant :
POST /api/channels/123/users/789

Quand on voit cette requête POST, on peut croire que ça va uniquement ajouter une liaison entre l’utilisateur et le salon, et rien de plus. On n’imagine pas forcément que des statuts vont être changés, ou encore que des notifications vont être envoyées par email.

Par contre, dans une application cliente, il y a de grandes chances qu’on écrive quelque chose comme ça :

$channelManager->subscribeUser($channelId, $userId);

Quand on voit cette ligne de code, on pense bien qu’il peut se passer un certain nombre de choses. On ne manipule pas une ressource, on demande à effectuer une action. Et ça change tout.

Et donc, on ne se poserait pas plus de question si l’URL était celle-ci :
/api/channels/subscribeUser/123/789

De manière générale, il n’y a pas de raison pour que les appels à des APIs fonctionnent fondamentalement différemment de ceux que nous faisons à l’intérieur de notre code. Que l’on dise à un objet local de faire quelque chose, ou qu’on le dise à un objet distant, ça ne devrait pas changer grand-chose.

Le REST restreint l'expressivité du code. Est-ce qu’on imaginerait faire du génie logiciel avec du CRUD pour seul vocabulaire ?

Aujourd’hui, de plus en plus d’APIs s’éloignent du modèle REST pour proposer un fonctionnement par appels à objets distants. Quelques exemples : Telegram Bot API, Slack API, MetaWeblog API, Google Cloud gRPC API, Bitcoin API

Authentification : dites oui au HTTP Basic, non aux JWT #

Pour donner accès à une API, il faut nécessairement authentifier l’utilisateur qui se connecte. Pour cela, plusieurs moyens existent, mais le mode est aux jetons JWT.

Les JWT sont des jetons cryptographiques, c’est-à-dire qu’ils peuvent contenir des informations qui ne peuvent pas être modifiées (sinon le jeton n’est plus valide). L’idée semble séduisante : le client envoie son identifiant et son mot de passe (ou ses clés publique et privée), et récupère en retour un jeton qui contient ses droits d’accès. Ensuite, le jeton est envoyé dans toutes les requêtes sur l’API ; le serveur n’a pas besoin de vérifier les droits de l’utilisateur, il peut faire confiance aux données incluses dans le jeton.

Sauf que dans la vraie vie, ce n’est pas aussi simple. Si on retire les droits d’accès d’un utilisateur, on n’a pas envie qu’il puisse continuer à utiliser l’API ; et pourtant, tant que le jeton est valable, l’utilisateur sera accepté.
La solution est souvent de réduire la durée de vie du jeton, pour forcer à le régénérer fréquemment. Mais dans ce cas, la moindre requête sur l’API se retrouve à effectuer trois connexions :

  1. Le client tente d’utiliser l’API, en fournissant le jeton qu’il a gardé en cache.
    → L’API répond que le jeton est périmé.
  2. Le client demande un nouveau jeton, en envoyant son identifiant et son mot de passe.
    → L’API répond en fournissant un nouveau jeton.
  3. Le client tente à nouveau d’utiliser l’API, en fournissant le jeton qu’il vient d’obtenir.
    → L’API effectue son traitement et retourne le résultat.

Pour éviter cette complexité inutile, il est possible de baser l’authentification d’une API sur le mécanisme HTTP Basic. C’est une technique très simple, supportée par absolument tous les clients HTTP, qui implique d’envoyer les identifiants d’authentification dans toutes les requêtes.

Cette manière de faire traîne une mauvaise réputation. Elle est vue comme étant moins sécurisée, car les identifiants circulent dans chaque requête, ce qui était effectivement un problème à l’époque où le chiffrement SSL/TLS n’était pas répandu. Sauf que de nos jours, il n’y a aucune excuse pour ne pas avoir de certificat SSL (grâce à Let’s Encrypt). Il n’y a donc aucun problème à faire circuler les clés de connexion, car aucune personne extérieure ne pourra les lire.

Par contre, la cinématique devient beaucoup plus simple :

  1. Le client se connecte à l’API, en fournissant ses credentials.
    → L’API effectue son traitement et retourne le résultat.

Un grand nombre de services utilisent une authentification HTTP Basic, soit seule, soit en complément de jetons : Azure API, Twilio, Stripe, GitHub, IBM MQ, IBM App Connect

Dual-Purpose Endpoints #

Aussi appelée “content negociation” ou “API mode switch”, cette technique cherche à éviter de développer des APIs from scratch quand ce n’est pas nécessaire. Cela peut être utile pour fournir des données à une application mobile lorsqu’on a déjà un site web fonctionnel, par exemple.

Une partie ou la totalité des pages du site peuvent retourner un flux JSON à la place du flux HTML habituel. Plusieurs critères peuvent être utilisés pour faire la distinction :

  • Un en-tête HTTP Accept qui vaut application/json au lieu de text/html.
  • Un en-tête HTTP X-Requested-With qui vaut XMLHttpRequest.
  • Un préfixe /api/ ajouté au début de l’URL.
  • Un paramètre GET ?api=1 ou ?format=json.

Suivant comment le site est développé, cela peut être très simple à gérer, avec un plugin/middleware/hook qui fait utiliser une vue JSON au lieu du moteur de templates qui génère le HTML en temps normal. Toutes les données fournies normalement au template sont alors sérialisées en JSON.

Les appels d’API qui sont réservés aux utilisateurs authentifiés bénéficient naturellement de la gestion des droits utilisateurs du site web. L’authentification peut se faire de deux manières possibles :

  • Soit chaque URL peut recevoir les clés d’authentification (en HTTP Basic, voir plus haut), et ainsi procéder à l’authentification, la vérification des droits et le traitement applicatif dans la même requête.
  • Soit passer par le processus d’authentification web (mais appelé en mode API), en fournissant le couple identifiant/mot de passe, et en récupérant le cookie de session qui sera utilisé comme jeton d’authentification pour les requêtes suivantes.

Cette technique est plus utilisée qu’il n’y paraît : Jekyll/GitHub Pages, Discourse, MediaWiki/Wikipedia, IBM UrbanCode Release…

Sécurité : Les fondamentaux non négociables #

La sécurité des développements web ne doit pas être prise à la légère. Il existe deux catégories de failles de sécurité :

  • celles qui rendent vulnérables les serveurs qui hébergent l’application, pouvant donner accès aux données qu’ils stockent ;
  • celles qui rendent vulnérables les utilisateurs du service, permettant d’y accéder sous leur identité ou de leur faire exécuter des actions malgré eux.

De nombreux sites sont consacrés à la sécurité des développements. comme la Fondation OWASP (Open Worldwide Application Security Project), qui liste notamment les dix risques de sécurité critiques qui touchent les applications web [OWASP 79], et dont la lecture est hautement recommandée.

Principes de base #

Le principe fondamental quand on développe un système qui communique avec d’autres systèmes et avec des utilisateurs externes, c’est de ne jamais jamais faire confiance à ce qui vient de l’extérieur. Tout ce qui entre doit être vérifié et filtré avant d’intégrer le système interne. Tout ce qui sort doit être traité de manière à ne pouvoir être utilisé de manière malicieuse.

Près de 9 attaques sur 10 sont dues à une mauvaise validation des entrées. [Vijayan 80]

Configuration du serveur HTTP #

Il est important de sécuriser les échanges entre le serveur et les navigateurs en utilisant des certificats de chiffrement, pour éviter qu’un observateur externe puisse intercepter les flux de données. Si ces certificats étaient coûteux à une époque, le projet Let’s Encrypt permet aujourd’hui d’en obtenir gratuitement.

Il y a eu plusieurs versions des protocoles SSL puis TLS au fil des années. Il est recommandé de désactiver les versions les plus anciennes (SSLv3, TLSv1.0, TLSv1.1) car elles ne sont pas suffisamment sécurisées. À moins d’avoir spécifiquement besoin de supporter de très vieux navigateurs, il est même recommandé de désactiver le protocole TLSv1.2, pour ne garder que le TLSv1.3. Plusieurs sites expliquent comment procéder [Better Stack 81, Nek 82].

Il existe plusieurs en-têtes HTTP qui doivent être définis de manière à renforcer la sécurité d’un site : Content-Security-Policy, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Strict-Transport-Security
Vous trouverez plusieurs sites expliquant comment les configurer [Starr 83, OWASP 84].

L'injection SQL #

L’injection SQL reste l’attaque la plus fréquente rencontrée par les services web.

Les attaques par injection SQL représentent les deux tiers des attaques ciblant les applications web [Vijayan 80]

Le principe de l’injection SQL est − pour un utilisateur malintentionné − de pervertir les données envoyées dans un système, par exemple via un formulaire envoyé au serveur, pour modifier le comportement d’une requête sur la base de données.

Par exemple, dans un formulaire d’authentification, il serait possible de saisir comme identifiant la valeur "admin'; --", sans fournir de mot de passe. Un système vulnérable exécutera la requête SQL suivante :

SELECT * FROM users WHERE login = 'admin'; -- AND password = '';

En SQL, les deux tirets (--) marquent le début d’un commentaire, donc la requête retournerait les données de l’utilisateur “admin”.

La solution est d’échapper toutes les données en entrée, soit en utilisant une fonction comme mysqli_real_escape_string() ou PDO::quote(), soit en utilisant des requêtes préparées. La requête finale serait alors :

SELECT * FROM users WHERE login = 'admin\'; --' AND password = '';

La requête ne retournera alors aucun résultat.

Il est à noter que les requêtes préparées sont souvent présentées comme la seule bonne pratique à utiliser en la matière, car elle permet de facilement échapper les paramètres. Toutefois, elles ont été créées dans l’idée qu’une même requête soit utilisée plusieurs fois avec des paramètres différents.
Certaines requêtes complexes générées dynamiquement ne peuvent pas se satisfaire de requêtes préparées, aussi il est nécessaire de connaître les différentes techniques permettant d’échapper les données.

Cross-site scripting (XSS) #

Le XSS est une faille de sécurité qui arrive assez classiquement lorsque des données saisies par un utilisateur peuvent se retrouver telles quelles dans une page web vue par une autre personne.

Par exemple, cela peut être un site proposant à ses visiteurs de laisser des commentaires ; le texte saisi par une personne est enregistré en base de données, puis affiché tel quel à chaque fois que quelqu’un vient consulter la page. Si un commentaire contient une balise "<script>" avec du code JavaScript, ce code s’exécute sur les navigateurs des visiteurs.
C’est la porte ouverte à la récupération d’informations personnelles, la récupération de cookie de session (et donc l’usurpation d’identité), l’exécution de programmes malveillants (minage de cryptomonnaie, DDOS).

La solution dépend du type de donnée traitée.

S’il faut accepter du texte brut, c’est à l’affichage qu’il faut s’assurer que le texte est proprement échappé, afin qu’une balise "<script>" apparaisse comme du texte ("&lt;script&gt;") et ne soit pas interprétée par le navigateur.
Pour cela, la fonction htmlspecialchars() est utile, à moins d’utiliser un moteur de template qui échappe les variables par défaut ou à la demande.

S’il faut accepter du code HTML envoyé par un éditeur WYSIWYG, il faut nettoyer ce flux entrant pour s’assurer qu’il ne contient que des balises HTML autorisées. Des bibliothèques comme HTMLPurifier permettent de faire cela.

L’OWASP propose le Cross-Site Scripting Prevention Cheat Sheet, qui liste d’autres types de vulnérabilités XSS moins communes, et comment s’en prémunir.

Serverside request forgery (SSRF) #

Les attaques SSRF ont pour but de faire exécuter au serveur des requêtes non désirées. Dans tous les cas, la faille se situe au niveau d’une donnée entrante qui est utilisée en toute confiance alors qu’elle n’a pas été vérifiée.

Un site peut par exemple recevoir en paramètre une URL vers laquelle il doit se connecter pour aller récupérer des informations. Un hacker pourrait alors fournir une URL pointant vers un fichier local, ou vers une URL interne au réseau, exposant ainsi des données secrètes.

Autre exemple, un site peut recevoir en paramètre un chemin vers un fichier local à inclure. Si le paramètre pointe vers une URL externe, le système va aller chercher ce contenu et l’interpréter, permettant de faire exécuter du code malicieux sur le serveur.

La solution est là encore de ne jamais faire confiance aux données provenant de l’extérieur, et de toujours les vérifier avant de les utiliser. Et ne jamais inclure un fichier dont le chemin est fourni en paramètre (GET ou POST).

L’OWASP propose le Server-Side Request Forgery Prevention Cheat Sheet, qui liste les vulnérabilités SSRF et comment s’en prémunir.

Cross-site request forgery (CSRF) #

Contrairement aux autres attaques, qui reposent sur l’injection de données corrompues dans un système, le CSRF consiste à pousser les utilisateurs à effectuer des actions sans s’en rendre compte.

Par exemple, une interface d’administration nécessite de s’authentifier pour y accéder. Une fois authentifié, un cookie de session est déposé sur le navigateur. Cette interface permet d’effacer des articles en cliquant sur des liens de type /article/delete/[articleId], qui redirige ensuite vers la liste des articles.
Imaginons maintenant qu’un mail de phishing ou une publicité contiennent un lien qui fasse une redirection vers https://admin.mysite.com/article/delete/1234
En cliquant dessus, l’utilisateur efface un article sans s’en rendre compte et ne comprend pas pourquoi il arrive sur la liste des articles.

La solution la plus connue est de générer un jeton à transmettre dans l’URL, et de vérifier ce jeton avant de traiter l’exécution demandée. Cette solution est tellement répandue que beaucoup semblent penser que c’est la seule solution envisageable.
Attention, car une mauvaise implémentation peut générer un faux sentiment de sécurité. Et un implémentation bancale, basée sur les sessions, empêchera d’utiliser le même site dans plusieurs onglets en même temps.

Il existe pourtant des bonnes pratiques, bien plus simples à mettre en œuvre et tout aussi efficaces.

La première est de n’accepter que des requêtes POST pour les actions délicates. En effet, lors d’une redirection (ou suite à un clic sur un lien dans un mail), le navigateur n’est capable de faire qu’une requête GET.

La deuxième bonne pratique est d’utiliser le paramètre SameSite lors de la création du cookie d’authentification. Si ce paramètre est positionné à la valeur “Lax”, le cookie ne sera pas envoyé dans le cas d’une requête POST provenant d’un autre site. Avec la valeur “Strict” c’est encore plus restrictif, le cookie ne sera pas envoyé non plus pour les requêtes GET.

La troisième, si nécessaire, est de vérifier le contenu de l’en-tête REFERER envoyé par le navigateur. Lors d’une redirection, le REFERER contiendra un domaine différent du domaine courant.

Lorsqu’elles sont utilisées conjointement, ces bonnes pratiques sont très efficaces.

Là encore, l’OWASP propose le Cross-Site Request Forgery Prevention Cheat Sheet, pour aller plus loin avec les failles CSRF.

Ressources #

  • PHP Zen : Une sélection d’articles et de ressources, régulièrement mise à jour, en accord avec le Manifeste.
  • Temma : Le framework simple et efficace — plus facile que les gros frameworks, plus puissant que les microframeworks.
  • PHP The Right Way : Un guide de référence sur les usages modernes de PHP, proposant des bonnes pratiques, des standards de code et des ressources pour écrire du code PHP propre et efficace.
  • PHP The Wrong Way : Un regard satirique sur le développement PHP, qui met en lumière avec humour les mauvaises pratiques et les pièges à éviter.

Références #

  1. The PHP Documentation Group. “History of PHP
  2. Rasmus Lerdorf (Créateur de PHP). “25 years of PHP” (2019)
  3. Rasmus Lerdorf (Créateur de PHP). "[PHP-DEV] Re: Generators in PHP” (2012)
  4. Kevin Yank (Architecte principal chez Culture Amp). “Interview – PHP’s Creator, Rasmus Lerdorf” (2002)
  5. Rasmus Lerdorf (Créateur de PHP). @rasmus tweet (2010)
  6. Avery Pennarun (PDG de Tailscale). “The New Internet” (2024)
  7. Scott Berkun (Auteur de The Myths of Innovation). “Two kinds of people: complexifiers and simplifiers” (2006)
  8. Article Wikipedia. “Wirth's law” (2024)
  9. Marek Kirejczyk (Fondateur de vlayer Labs). “Hype Driven Development” (2016)
  10. Anouk Goossens (Consultant chez The Learning Hub). “Why best practices aren’t the holy grail” (2023)
  11. Austin Knight (Responsable du design chez Square). “The Road to Mediocrity Is Paved with Best Practices” (2015)
  12. Matt Stancliff (Contributeur de Redis). “Panic! at the Job Market” (2024)
  13. Discussion Hacker News. “Why bad scientific code beats code following ‘best practices’” (2016)
  14. Jeff Atwood (Cofondateur de StackOverflow et Discourse). “Why Objects Suck” (2004)
  15. Article Wikipedia. “Object-oriented programming
  16. Brian Will (Ingénieur logiciel sénior chez Unity). “How to program without OOP” (2016)
  17. Eric S. Raymond (Cofondateur de l'Open Source Initiative). “The Art of Unix Programming” (2003)
  18. Jeff Atwood (Cofondateur de StackOverflow et Discourse). “Your Code: OOP or POO?” (2007)
  19. Jeff Atwood (Cofoundateur de StackOverflow et Discourse). “When Object-Oriented Rendering is Too Much Code” (2006)
  20. Rich Hickey (Créateur du langage Clojure). Software Engineering Radio podcast #158 (2010)
  21. Asaf Shelly (Expert en IA et cybersécurité). “Flaws of Object Oriented Modeling” (2008)
  22. Brian Will (Ingénrieur logiciel sénior chez Unity). “Object-Oriented Programming is Embarrassing: 4 Short Examples” (vidéo, 2016)
  23. Elliot Suzdalnitski (PDG de l'Empire School of Business). “Object-Oriented Programming — The Trillion Dollar Disaster” (2019)
  24. Joe Armstrong (Co-créateur du langage Erlang). “Why OO Sucks” (2011)
  25. Rob Pike (Co-créateur du système d'exploitation Plan 9, de l'encodage UTF-8 et du langage Go). Post sur Google+ (2012)
  26. Gina Peter Banyard (Membre de la Core Team PHP − développement et documentation). “PHP RFC: Unify PHP's typing modes (aka remove strict_types declare” (2021)
  27. Cheatmaster30 (Journaliste chez Gaming Reinvented). “Putting devs before users: how frameworks destroyed web performance” (2020)
  28. Mark Zeman (Fondateur de Speedcurve). “Best Practices for Optimizing JavaScript” (2024)
  29. Article sur le blog Easy Laptop Finder. “The relentless pursuit of cutting-edge JavaScript frameworks inadvertently contributed to a less accessible web” (2023)
  30. Eduardo Rodriguez (Ingénieur logiciel full-stack chez they consulting). “Dependency management fatigue, or why I forever ditched React for Go+HTMX+Templ” (2024)
  31. David Haney (Fondateur de CodeSession). “NPM & left-pad: Have We Forgotten How To Program?” (2016)
  32. Adrian Holovaty (Co-créateur du framework Django). “dotJS - A framework author's case against frameworks” (vidéo, 2017)
  33. Alex Russell (Responsable produit associé chez Microsoft). “If Not React, Then What?” (2024)
  34. Gouvernement du Royaume-Uni. “Building a robust frontend using progressive enhancement” (2024)
  35. Andy Bell (Fondateur de Set Studio et Piccalilli). “It’s about time I tried to explain what progressive enhancement actually is” (2024)
  36. Kelly Sutton (Cofondateur de Scholarly Software). “Moving on from React” (2024)
  37. Kelly Sutton (Cofondateur de Scholarly Software). “Moving on from React, a Year Later” (2025)
  38. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “Modern web apps without JavaScript bundling or transpiling” (2021)
  39. David Heinemeier Hansson (Directeur techique de 37signals, créateur de Ruby On Rails). “You can't get faster than No Build” (2023)
  40. Michael Stonebraker (Co-créateur de la base de données PostgreSQL). “Comparison of JOINS: MongoDB vs. PostgreSQL” (2020)
  41. Mridul Verma (Ingénieur logiciel sénior chez Sumo Logic). “Database Performance: MySQL vs MongoDB” (2024)
  42. Laurie Voss (Cofondateur de NPM). “In defence of SQL” (2011)
  43. Laurie Voss (Cofondateur de NPM). “ORM is an anti-pattern” (2011)
  44. Yegor Bugayenko (Directeur de laboratoire chez Huawei). “ORM Is an Offensive Anti-Pattern” (2014)
  45. Eli Bendersky (Chercheur chez Google). “To ORM or not to ORM” (2019)
  46. Jeff Atwood (Cofondateur chez StackOverflow et Discourse). “Object-Relational Mapping is the Vietnam of Computer Science” (2006)
  47. Chris Maffey (Fondateur de PHP Lab). “Why SQL is still really important” (2020)
  48. François Zaninotto (Co-créateur de l'ORM Propel). Tweet from @francoisz (2019)
  49. Alex Martelli (Ingénieur principal chez Google et "Fellow" de la Python Software Foundation). Answer on Stack Overflow (2010)
  50. Mattia Righetti (Ingénieur système chez Cloudflare). “You Probably Don't Need Query Builders” (2025)
  51. Anh-Tho Chuong (PDG de Lago). “Is ORM still an 'anti pattern'?” (2023)
  52. Mattia Righetti (Ingénieur système chez Cloudflare). “Can't Escape Good Old SQL” (2025)
  53. Mike Acton (Directeurr de l'ingénierie chez Hypnos Entertainment). “CppCon 2014: Data-Oriented Design and C++” (vidéo, 2014)
  54. Noel Llopis (Concepteur de jeux indépendant). “Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP)” (2009)
  55. Tan Dang (Rédacteur chez Orient Software). “Revolutionize Your Code: The Magic of Data-oriented Design (DOD) Programming” (2023)
  56. Stoyan Nikolov (Ingénieur IA principal chez Google). “CppCon 2018: OOP Is Dead, Long Live Data-oriented Design” (vidéo, 2018)
  57. Wikipedia. “Web framework
  58. Contributeurs Mozilla. “Server-side web frameworks” (2024)
  59. Blog Expert .NET. “The Problem with Frameworks in Software Development” (2024)
  60. Kirill Bobrov (Ingénieur données sénior chez Spotify). “The Frameworks Dilemma” (2024)
  61. Blog TechAffinity. “The Benefits and Limitations of Software Development Frameworks” (2023)
  62. Sushrut Mishra (Rédacteur technique chez FuelEd). “Why you shouldn’t use Libraries/Frameworks for everything” (2023)
  63. Evaldo Junior (Développeur sénior). “Are micro-frameworks suitable only for small projects?” (2015)
  64. Matthew Weier O’Phinney (Responsable produit sénior chez Zend). “On Microframeworks” (2012)
  65. Chris Dillon (Ingénieur logiciel sénior chez Mitre). “The Database Ruins All Good Ideas” (2021)
  66. Chris Dillon (Ingénieur logiciel sénior chez Mitre). “Microframeworks Are Too Small” (2023)
  67. Jim Coplien (Rédacteur, conférencier et chercheur). “Why Most Unit Testing is Waste” (PDF)
  68. Blog Modus Create. “An Overview of Unit, Integration, and E2E Testing” (2023)
  69. Blog twilio. “Unit, Integration, and End-to-End Testing: What’s the Difference?” (2022)
  70. Andrew Trenk (Ingénieur logiciel chez Google). “Testing on the Toilet: Don’t Overuse Mocks” (2013)
  71. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “TDD is dead. Long live testing” (2014)
  72. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “Testing like the TSA” (2012)
  73. Chris Kiehl (Ingénieur logiciel sénior chez Amazon). “Software development topics I've changed my mind on after 10 years in the industry” (2025)
  74. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “The Majestic Monolith” (2016)
  75. Article Wikipedia. “Fallacies of distributed computing
  76. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “The Majestic Monolith can become The Citadel” (2020)
  77. Martin Fowler (Auteur et conférencier international sur le développement logiciel et les méthodologies agiles). “Microservice Prerequisites” (2014)
  78. David Heinemeier Hansson (Directeur technique de 37signals, créateur de Ruby On Rails). “Even Amazon can't make sense of serverless or microservices” (2023)
  79. OWASP. “OWASP Top Ten
  80. Jai Vijayan (Jounaliste primé chez Computerworld). “SQL Injection Attacks Represent Two-Third of All Web App Attacks” (2019)
  81. Better Stack. “How can I disable TLS 1.0 and 1.1 in apache?” (2023)
  82. Dimitri Nek. “How to Enable TLS 1.3 in Apache and Nginx on Ubuntu and CentOS
  83. Jeff Starr (Développeur, concepteur, auteur et éditeur). “Seven Important Security Headers for Your Website” (2024)
  84. OWASP. “OWASP Secure Headers Project