Un projet PHP sans framework n'est pas un projet sans structure. C'est même l'inverse : là où Symfony ou Laravel imposent une arborescence et une convention de nommage, un projet nu oblige à décider soi-même de tout, et ces décisions se paient très cher si elles sont prises au fil de l'eau. La plupart des sites que nous reprenons souffrent du même mal, un dossier racine où cohabitent les gabarits, les images, un fichier de configuration contenant les identifiants de base de données et trente scripts appelés directement depuis le navigateur. Rien de tout cela n'est nécessaire, et rien de tout cela n'est le prix à payer pour se passer d'un framework : ce sont deux questions distinctes, la première relevant de l'organisation du projet, la seconde de l'outillage. Une arborescence tenable, un point d'entrée unique et un autoload conforme suffisent, et se mettent en place en une heure au démarrage d'un projet. Cette heure-là est la moins chère de tout le projet : les mêmes décisions prises deux ans plus tard, sur un site en production, coûtent plusieurs jours et imposent une phase de recette complète, parce qu'il faut déplacer des fichiers dont on ne sait plus qui les appelle.
Une arborescence qui sépare ce qui est public de ce qui ne l'est pas
La première décision est aussi la plus structurante : seul le dossier réellement exposé au web doit être accessible depuis un navigateur. Tout le reste, le code applicatif, la configuration, les dépendances, les gabarits et les données, vit au-dessus de la racine web. Cette séparation est la base de tout ce que nous décrivons dans notre rubrique création de site Internet, et c'est elle qui rend inutiles la moitié des règles de blocage qu'on trouve habituellement dans un fichier de configuration serveur.
Le dossier public et rien d'autre
Le dossier exposé, généralement nommé public ou web, contient trois choses : le point d'entrée index.php, les ressources statiques réellement destinées au navigateur, et un fichier de configuration serveur minimal. Rien de plus. Aucun script métier, aucun gabarit, aucune bibliothèque. Le bénéfice est immédiat et se mesure : une faille dans une dépendance ne devient exploitable que si le fichier concerné est atteignable par une URL, et il ne l'est plus. Sur un hébergement mutualisé qui impose un dossier racine fixe, on obtient le même résultat en plaçant le code applicatif un niveau au-dessus et en pointant les inclusions vers ce dossier parent. Certains hébergeurs interdisent aussi de remonter d'un cran, l'interpréteur étant confiné au dossier du site. Dans ce cas seulement, le repli acceptable consiste à garder le code dans la racine mais dans des dossiers dont le nom commence par un point, ou protégés par une règle de refus explicite, en sachant que cette protection dépend entièrement de la configuration du serveur et disparaîtra le jour où celle-ci changera.
Où ranger le code, la configuration et le contenu
Au-dessus du dossier public, trois répertoires suffisent à couvrir la quasi-totalité des besoins d'un site de contenu. Un dossier src pour les classes, organisé par domaine fonctionnel et non par type technique : un répertoire par sujet plutôt qu'un répertoire de contrôleurs, un de modèles et un de services. Un dossier config pour les réglages, avec une distinction nette entre ce qui est versionné et ce qui ne l'est jamais. Un dossier content ou data pour ce que le site produit et modifie, seul répertoire qui doit être accessible en écriture par le processus PHP. Cette dernière règle est souvent ignorée, et c'est elle qui transforme une injection banale en prise de contrôle complète. Le raisonnement se tient en une phrase : un attaquant qui parvient à déposer un fichier ne gagne quelque chose que si ce fichier peut être exécuté, et un dossier de données correctement isolé n'exécute rien. Sur les sites que nous auditons, le dossier des téléversements est presque toujours à la fois accessible en écriture et exécutable, ce qui suffit à expliquer la majorité des compromissions constatées.
Les dossiers qu'on ajoute trop tôt
Un projet naissant n'a pas besoin d'un dossier de tests, d'un dossier de migrations, d'un dossier de commandes en ligne et d'un dossier d'événements. Ces répertoires vides donnent l'illusion d'une architecture réfléchie et compliquent la lecture pour rien. La règle que nous appliquons est simple : un dossier se crée le jour où il contient son deuxième fichier. Avant, le fichier reste à sa place naturelle. Cette discipline évite l'effet inverse, celui d'un projet de trois cents lignes réparties dans quarante répertoires, sur lequel personne ne sait plus où ajouter quoi que ce soit. Le même principe vaut pour les couches d'abstraction : une interface écrite pour une seule implémentation ne prépare pas l'avenir, elle ajoute un fichier à lire pour comprendre ce que fait le code. Le jour où une seconde implémentation apparaît réellement, l'extraction de l'interface prend dix minutes.

Le point d'entrée unique, et ce qu'il fait exactement
Tout ce qui arrive au serveur passe par un seul fichier. Ce principe, hérité des frameworks et parfaitement transposable sans eux, remplace la pratique consistant à créer un script par page. Il change la nature du projet : les URL cessent d'être des chemins de fichiers, la sécurité se traite à un seul endroit, et l'ajout d'une fonctionnalité transverse ne demande plus de modifier trente fichiers. C'est ce que fait le langage PHP dans son fonctionnement le plus courant depuis longtemps, mais l'habitude du fichier par page a la vie dure.
Ce que contient index.php
Le point d'entrée est court, une trentaine de lignes au plus, et il fait toujours la même chose dans le même ordre. Il charge l'autoload, lit la configuration, construit le chemin demandé à partir des variables du serveur, confie ce chemin à un routeur, et envoie la réponse. Rien d'autre. La tentation d'y ajouter une vérification par-ci, une redirection par-là, se paie en quelques mois par un fichier de trois cents lignes que personne n'ose plus toucher. Quand une opération doit s'exécuter avant tout le reste, elle prend la forme d'un appel unique à une classe dédiée, pas d'un bloc de code inséré au milieu. Une exception mérite d'être connue : ce qui doit s'exécuter avant toute constante, avant toute inclusion et avant tout envoi d'en-tête doit effectivement figurer dans les premières lignes du fichier, faute de quoi l'appel arrive trop tard. C'est le cas des mécanismes qui décident si la requête a le droit d'aller plus loin.
Le routeur minimal
Un routeur n'a pas besoin d'être un moteur d'expressions régulières compilées. Pour un site de contenu, une correspondance sur le premier segment du chemin suffit à orienter vers un gabarit, une rubrique ou un article, le reste étant traité par recherche dans les données. Trois précautions comptent plus que la sophistication : normaliser le chemin en supprimant les paramètres et en décidant une fois pour toutes du sort de la barre oblique finale, refuser tout chemin contenant une séquence de remontée de répertoire, et prévoir explicitement le cas où rien ne correspond, avec un code de réponse 404 réel et non une page blanche renvoyée en 200. Cette dernière erreur est la plus coûteuse en référencement, parce qu'elle est invisible : le visiteur voit une page vide, le moteur enregistre une page valide, et l'index se remplit de contenus fantômes que rien ne vient signaler.
La redirection de tout le trafic vers ce fichier
Côté serveur, la règle tient en trois lignes : si le chemin demandé correspond à un fichier ou un dossier existant, le servir tel quel ; sinon, passer la main au point d'entrée. C'est l'inverse de la logique historique, où chaque URL correspondait à un fichier. Le détail qui fait échouer la plupart des installations tient à l'ordre des règles : une réécriture placée après une règle terminale ne s'applique jamais, et le symptôme est toujours le même, une partie du site qui répond en 404 sans raison apparente alors que le reste fonctionne. Le fichier de configuration htaccess se lit de haut en bas, sans exception. Deux conditions doivent également précéder la règle finale : ne pas réécrire lorsque le chemin correspond à un fichier réel, et ne pas réécrire lorsqu'il correspond à un dossier réel. Sans elles, les feuilles de style et les images repartent vers le point d'entrée, qui ne sait pas les servir.
L'autoload PSR-4, sans dépendre d'un framework
Charger manuellement chaque fichier de classe par une inclusion est la marque d'un projet qui n'a pas franchi le pas. L'autoload résout ce problème depuis quinze ans, et la norme PSR-4 en fixe la convention : un espace de noms correspond à un dossier, une classe correspond à un fichier portant son nom. Composer génère le mécanisme, mais rien n'oblige à installer la moindre dépendance pour en profiter. Un projet peut parfaitement n'avoir aucun paquet tiers et utiliser Composer uniquement pour son autoload. C'est même une position confortable : aucune dépendance à surveiller, aucune faille héritée, aucune montée de version imposée par un tiers, et malgré tout un chargement de classes conforme à la norme que tout développeur PHP connaît.
Déclarer son espace de noms
La déclaration tient en quatre lignes dans le fichier de configuration de Composer : une section autoload, une entrée PSR-4, le préfixe d'espace de noms choisi, et le dossier correspondant. Une commande de génération plus tard, l'inclusion d'un unique fichier suffit à rendre toutes les classes disponibles. Le préfixe se choisit une fois pour toutes et se change difficilement ensuite, il vaut donc mieux prendre trente secondes pour éviter les noms génériques du type App, qui entrent en collision dès qu'on intègre du code venu d'ailleurs. La convention la plus sûre reprend le nom de l'organisation puis celui du projet, sur deux niveaux, ce qui garantit l'unicité même si le code est un jour publié ou fusionné avec un autre.
L'optimisation de l'autoload en production
Par défaut, l'autoload de Composer cherche les fichiers en suivant la convention, ce qui implique un accès disque par classe non encore chargée. En production, une carte statique associant chaque classe à son chemin supprime cette recherche. Le gain se mesure vraiment sur un site qui charge une centaine de classes par requête, et il se cumule avec le cache d'opcode, qui garde en mémoire la version compilée des fichiers. Cette optimisation se régénère à chaque déploiement, et l'oublier produit une erreur déroutante : une classe pourtant présente sur le disque, introuvable pour l'application. L'ordre des opérations compte lors d'une mise en ligne : on régénère la carte après avoir copié les fichiers, jamais avant, et l'on vide le cache d'opcode dans la foulée, sans quoi le serveur continue de servir la version compilée de l'ancienne carte pendant plusieurs minutes.
Ce que l'autoload ne fait pas
L'autoload charge des classes, pas des fonctions ni des constantes. Les fonctions utilitaires demandent une inclusion explicite, déclarée dans une section dédiée du fichier Composer, qui les charge à chaque requête. C'est une raison suffisante pour en limiter le nombre : chaque fonction globale est un fichier lu à chaque appel de page. Quand une bibliothèque de fonctions dépasse la dizaine d'entrées, il est presque toujours préférable d'en faire une classe aux méthodes statiques, chargée seulement lorsqu'elle sert. Le même raisonnement s'applique aux fichiers de constantes et aux définitions de configuration : tout ce qui est déclaré globalement est lu à chaque requête, y compris sur les pages qui n'en ont aucun usage.
| Emplacement | Ce qu'on y met, et pourquoi |
|---|---|
public/ |
Point d'entrée et ressources statiques. Seul dossier exposé au navigateur. |
src/ |
Classes de l'application, organisées par domaine fonctionnel. |
config/ |
Réglages. Les secrets restent hors du dépôt de code. |
content/ |
Données produites par le site. Seul dossier accessible en écriture. |
vendor/ |
Dépendances installées par Composer. Jamais modifié à la main. |
templates/ |
Gabarits d'affichage, hors racine web. |
Relevé sur la reprise d'un site de contenu de taille moyenne. Sortir le code de la racine web supprime la quasi-totalité des règles de blocage, et surtout le risque qu'une de ces règles cesse d'être lue.
Séparer la configuration du code, et les secrets de la configuration
La dernière décision structurante concerne les réglages. Un projet mélange presque toujours trois choses de nature différente : des constantes techniques qui ne changent jamais, des réglages qui varient d'un environnement à l'autre, et des secrets qui ne doivent jamais quitter le serveur. Les traiter ensemble revient à mettre un mot de passe de base de données dans un dépôt de code, ce qui arrive plus souvent qu'on ne l'imagine et se découvre toujours trop tard.
Trois niveaux de réglages
Les constantes techniques, comme le format de date interne ou la taille maximale d'un téléversement, vivent dans le code et sont versionnées : elles font partie de l'application. Les réglages d'environnement, adresse du site, mode de débogage, chemins absolus, vivent dans un fichier de configuration présent sur chaque serveur mais absent du dépôt, avec un modèle d'exemple versionné à côté. Les secrets, identifiants de base de données et clés d'API, suivent le même chemin que les réglages d'environnement, avec une contrainte supplémentaire : ils ne doivent apparaître dans aucun message d'erreur, ce qui suppose de désactiver l'affichage des erreurs en production et de journaliser dans un fichier hors racine web.
Le fichier de configuration hors racine
Placer le fichier contenant les secrets au-dessus du dossier public élimine tout risque qu'un incident de configuration serveur le rende lisible. Ce scénario n'est pas théorique : une mise à jour d'interpréteur qui cesse d'exécuter les fichiers PHP transforme instantanément chaque fichier de configuration en fichier texte servi en clair. Les sites concernés ne s'en aperçoivent pas, puisque tout continue de fonctionner. Ceux qui ont sorti la configuration de la racine ne sont pas exposés, quel que soit l'état du serveur. La même précaution vaut pour les fichiers de sauvegarde laissés à côté de l'original, ces copies au nom suffixé qui ne sont plus interprétées et se lisent donc en clair. C'est une cause de fuite d'identifiants au moins aussi fréquente que le fichier de configuration lui-même.
Vérifier ce qui est réellement accessible
La séparation ne vaut que si elle est vérifiée. Le contrôle prend deux minutes et consiste à demander depuis un navigateur, sur le site en production, une dizaine de chemins qui ne devraient pas répondre : le fichier de configuration, le dossier des dépendances, un gabarit, le fichier de Composer, le dossier de contenu, un journal d'erreurs. Toute réponse autre qu'une erreur 403 ou 404 signale un problème à corriger le jour même. Ce contrôle mérite d'être refait après chaque déploiement et après chaque intervention de l'hébergeur, car une configuration serveur se réinitialise plus souvent qu'on ne le croit. Il s'automatise en quelques lignes, sous la forme d'une liste de chemins et du code de réponse attendu pour chacun, exécutée après chaque mise en ligne. Une alerte sur cette liste vaut mieux qu'un audit annuel, puisque le moment dangereux n'est jamais le jour de l'audit mais celui d'un changement discret dans la configuration du serveur. Cette liste se limite utilement à cinq ou six adresses, au delà de quoi elle cesse d'être relue attentivement.
💬 0 commentaires
✍️ Laisser un commentaire
Créez un compte gratuit ou connectez-vous pour commenter.