Ce que fait vraiment vendor/autoload.php : de require_once à PSR-4
Ouvrez le public/index.php d'un projet Symfony ou Laravel : tout commence par une ligne qui charge vendor/autoload.php (ou autoload_runtime.php, qui l'inclut). Ensuite, plus un seul require pour charger une classe. Des milliers de classes, la vôtre et celles de toutes les dépendances, sont disponibles « comme par magie ».
Cette ligne est souvent la première chose qu'on copie sans la comprendre. Et c'est dommage, parce que le mécanisme tient en une dizaine de lignes de PHP. Dans un parcours pour débutants que je viens d'écrire, on le refait à la main, étape par étape, avant de laisser Composer prendre le relais. Voici le chemin, en accéléré.
Le point de départ : un require_once par fichier
Un petit site PHP commence souvent dans un seul fichier. Puis on range : une classe par fichier, dans src/. Et index.php se remplit de lignes comme celles-ci :
require_once __DIR__ . '/../src/Publication.php'; require_once __DIR__ . '/../src/Rubrique.php'; require_once __DIR__ . '/../src/Auteur.php'; require_once __DIR__ . '/../src/Article.php'; require_once __DIR__ . '/../src/Annonce.php'; require_once __DIR__ . '/../src/Bulletin.php';
Deux problèmes apparaissent très vite.
D'abord, l'ordre compte. Si Article implémente l'interface Publication, le fichier de Publication doit être chargé avant. Changez l'ordre « pour ranger », et PHP répond :
Fatal error: Uncaught Error: Interface "Publication" not found
Le message ne dit pas que le fichier manque. Il dit que PHP ne connaît pas encore ce nom au moment où il en a besoin. C'est une nuance que beaucoup de débutants mettent du temps à saisir, et c'est exactement le problème que l'autoload va régler.
Ensuite, chaque oubli casse tout. Une nouvelle classe, et il faut penser à ajouter sa ligne, dans le bon fichier, au bon endroit.
Apprendre la règle à PHP
Pourtant, ces fichiers suivent tous la même règle : la classe Article est dans src/Article.php. Et une règle, on peut l'apprendre à PHP, avec spl_autoload_register() :
spl_autoload_register(function (string $classe): void { require __DIR__ . '/../src/' . $classe . '.php'; });
Quand PHP rencontre un nom de classe qu'il ne connaît pas, il appelle, une à une, les fonctions enregistrées de cette façon avant d'abandonner. La fonction reçoit le nom ('Article'), charge le fichier, et l'exécution reprend comme si de rien n'était.
Les deux problèmes disparaissent d'un coup :
- une classe n'est chargée que si on s'en sert, et au moment où on s'en sert : l'ordre ne compte plus ;
- une nouvelle classe dans
src/est disponible sans rien ajouter nulle part.
Les espaces de noms entrent en jeu
Un vrai projet mélange son code et celui des autres. Si une bibliothèque contient elle aussi une classe Article, PHP refuse : il ne peut pas déclarer deux fois le même nom. On range donc ses classes sous un espace de noms, comme un nom de famille :
<?php namespace Gazette; class Article { // … }
La classe s'appelle maintenant Gazette\Article, et c'est ce nom complet que l'autoloader reçoit. Il suffit que l'autoloader retire le préfixe :
spl_autoload_register(function (string $classe): void { $prefixe = 'Gazette\\'; if (str_starts_with($classe, $prefixe)) { require __DIR__ . '/../src/' . substr($classe, strlen($prefixe)) . '.php'; } });
Le if n'est pas décoratif : plusieurs autoloaders peuvent cohabiter, et chacun ne doit répondre que pour les noms qui le concernent. Un autoloader qui tente de charger n'importe quoi empêche les suivants de faire leur travail.
Pour aller plus loin, il faudrait aussi transformer les antislashs restants en séparateurs de dossier (Gazette\Meteo\Bulletin vers src/Meteo/Bulletin.php), et ne pas planter si le fichier n'existe pas. Rien de compliqué, mais c'est précisément le moment où l'on se rend compte que tout le monde a écrit le même code.
Cette règle a un nom : PSR-4
Une correspondance « préfixe d'espace de noms vers dossier, puis le reste du nom vers le chemin du fichier », c'est PSR-4, une recommandation commune aux projets PHP. Composer sait l'appliquer : il suffit de la déclarer dans composer.json.
{ "autoload": { "psr-4": { "Gazette\\": "src/" } } }
Puis :
composer dump-autoload
Composer lit composer.json et réécrit vendor/autoload.php et le dossier vendor/composer/. On remplace alors tout l'autoloader maison par une seule ligne :
require __DIR__ . '/../vendor/autoload.php';
C'est la ligne du début. Elle fait la même chose que nos dix lignes, et un peu plus.
Ce que Composer fait de plus
Quelques différences méritent d'être connues, parce qu'elles expliquent des comportements qui surprennent en production.
Il charge aussi les dépendances. Chaque paquet installé déclare son propre autoload dans son composer.json. Composer agrège toutes ces règles dans vendor/composer/ (regardez autoload_psr4.php, c'est un simple tableau). Le jour où vous lancez composer require nikic/fast-route, la fonction FastRoute\simpleDispatcher() et les classes du paquet sont utilisables sans rien ajouter d'autre.
Il vérifie que le fichier existe. Si le fichier attendu n'existe pas, l'autoloader de Composer ne fait rien, et PHP finit par lever « Class not found ». C'est le comportement attendu d'un autoloader poli : il laisse sa chance aux suivants.
Il ne relit pas composer.json à chaque requête. Les règles sont figées dans vendor/composer/ au moment du dump-autoload (ou d'un install, d'un require). Changer la section autoload sans relancer la commande ne change rien. C'est une source classique de « ça marche chez moi, pas sur le serveur » : on a modifié composer.json et relancé la commande en local, mais pas sur le serveur.
Il sait aller plus vite en production. Avec composer dump-autoload --optimize (ou -o), Composer parcourt src/ et vendor/ une fois pour toutes et écrit une classmap : un tableau qui associe directement chaque classe à son fichier. Plus besoin de calculer le chemin ni de tester son existence, du moins pour les classes connues au moment de la commande (les autres retombent sur la règle PSR-4). L'option --classmap-authoritative va plus loin : une classe absente de la classmap est considérée comme inexistante, sans même regarder le disque. Composer recommande --optimize partout en production ; --classmap-authoritative s'y ajoute souvent (Symfony le conseille au déploiement), avec une contrepartie : une classe ajoutée, ou générée pendant l'exécution, sans relancer la commande reste introuvable.
Les trois derniers points (fichier absent, règles figées, classmap) sont à l'origine d'une bonne partie des « Class not found » qui n'apparaissent qu'après un déploiement. Quand on a écrit l'autoloader soi-même une fois, le diagnostic devient évident.
Pourquoi le refaire à la main
Personne n'écrira son propre autoloader en production, et c'est tant mieux. Mais un développeur qui ne l'a jamais fait voit vendor/autoload.php comme une boîte noire, et les frameworks comme une couche de magie de plus.
C'est l'idée du parcours Apprendre PHP 8 que je viens d'ouvrir sur apprendre.forelse.fr. On y construit la gazette d'un petit village, de la première page qui affiche « Bonjour » jusqu'à un vrai site avec une page par article, un formulaire et une recherche, sans framework :
- les bases du langage, puis les objets en PHP 8 ;
- les
require_once, l'autoloader maison, les espaces de noms, puis Composer et PSR-4, comme dans cet article ; - un routeur écrit à la main avec des fonctions anonymes, puis remplacé par FastRoute installé avec
composer require; - des gabarits, des formulaires, la validation et la protection contre les failles XSS.
Tout se passe dans le navigateur, sans rien installer : PHP y tourne en WebAssembly, avec une vraie console pour lancer composer. À la fin, vendor/autoload.php n'est plus une boîte noire, et un framework ressemble moins à de la magie : il fait, en mieux, ce que vous avez déjà écrit une fois.
Le premier chapitre est libre d'accès avec un compte.