Chapitre 2.6 — Composer, PSR-4, namespaces & autoload
Partie 2 : PHP moderne — Chapitre 6/8 Version de référence : PHP 8.4 / Composer 2 pour Symfony 7.4.
⏱️ TL;DR
- En PHP moderne, on n’importe pas des fichiers : on référence des classes par leur namespace, et l’autoload (Composer) trouve le fichier tout seul.
- Un namespace organise les noms (
App\Entity\Booking). On l’importe en haut du fichier avecuse(l’équivalent de l’importJS, mais pour des symboles, pas des fichiers).- PSR-4 est la convention qui mappe un préfixe de namespace à un dossier :
App\ → src/. DoncApp\Entity\Booking⇒src/Entity/Booking.php. Un fichier = une classe, chemin = namespace.- Composer gère les dépendances (
composer.json/composer.lock) et génère l’autoloader (vendor/autoload.php) chargé une fois par le front controller.- Contraintes de version :
^7.4(compatibles 7.x ≥ 7.4),~7.4.0(7.4.x),*(n’importe quoi — à éviter).- Pour un dev Next.js :
namespace+use≈ modules ES +import, mais résolus par nom de classe via une convention de nommage stricte, pas par chemin relatif.
🎯 Objectifs
- Écrire des
namespaceet desusecorrects, y compris les alias. - Comprendre le mapping PSR-4 namespace → fichier et savoir déduire l’un de l’autre.
- Lire un
composer.jsonet manierrequire/install/update/dump-autoload. - Interpréter les contraintes de version (
^,~,*).
1. Le déclic : pas d’import de fichiers
En JavaScript, vous importez un fichier (ou un module) par son chemin : import { Booking } from './entities/booking'. En PHP moderne, ce n’est pas le modèle. Vous ne dites jamais « charge tel fichier ». Vous référencez une classe par son nom complet (son namespace), et un mécanisme d’autoload se charge de trouver — et d’inclure — le bon fichier automatiquement, à la première utilisation.
<?php
declare(strict_types=1);
namespace App\Controller; // ← ce fichier définit des symboles dans ce namespace
use App\Entity\Booking; // ← j'« importe » la classe Booking (pas son fichier)
use App\Repository\BookingRepository;
final class BookingController
{
public function show(BookingRepository $repo): void
{
$booking = $repo->find(42); // Booking est résolue et son fichier chargé à la volée
}
}💡 Pour un dev Next.js :
use App\Entity\Booking;joue le rôle d’import { Booking }. Mais vous n’écrivez jamais le chemin du fichier : la correspondance nom → fichier est déterministe (PSR-4, §3). C’est comme siimportretrouvait le module uniquement à partir du nom exporté, grâce à une convention de nommage imposée.
2. Les namespaces
Un namespace est un préfixe qui évite les collisions de noms (deux classes Client peuvent coexister dans App\Billing\Client et App\Http\Client).
namespace App\Service; // déclaré une seule fois, tout en haut, après declare()
class Mailer { /* … */ } // nom complet : App\Service\MailerPour utiliser une classe d’un autre namespace, on l’importe avec use :
use App\Service\Mailer; // import simple
use App\Service\Mailer as AppMailer; // import avec alias (évite une collision)
use Symfony\Component\Mailer\MailerInterface; // classe d'un package tiers2.1 Le \ initial (namespace global)
Les classes natives de PHP (DateTimeImmutable, Exception, ArrayObject…) vivent dans le namespace global. Depuis un namespace, on les référence avec un \ initial, ou on les importe :
namespace App\Domain;
$date = new \DateTimeImmutable(); // \ = « depuis la racine, namespace global »
// ou, en important en haut du fichier :
use DateTimeImmutable;
$date = new DateTimeImmutable();⚠️ Piège : dans un fichier avec
namespace App\Domain;, écrirenew DateTimeImmutable()sans\niusefait chercherApp\Domain\DateTimeImmutable(qui n’existe pas) → erreur « class not found ». Réflexe : soit préfixer par\(new \DateTimeImmutable()), soit ajouteruse DateTimeImmutable;en haut. Les IDE ajoutent leuseautomatiquement.
3. PSR-4 : la convention qui relie namespace et fichier
PSR-4 est le standard d’autoloading. Il définit une règle simple : un préfixe de namespace correspond à un dossier de base, et le reste du namespace se traduit littéralement en chemin de dossiers, un fichier .php par classe.
Dans un projet Symfony, le mapping est déclaré dans composer.json :
// composer.json (extrait)
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
}Conséquence directe, à mémoriser :
| Classe (namespace complet) | Fichier |
|---|---|
App\Entity\Booking | src/Entity/Booking.php |
App\Service\Mailer | src/Service/Mailer.php |
App\Controller\Api\BookingController | src/Controller/Api/BookingController.php |
App\Tests\Service\MailerTest | tests/Service/MailerTest.php |
Le préfixe App\ est remplacé par src/, chaque niveau de namespace devient un dossier, et le nom de la classe devient le nom du fichier (à la casse près : c’est sensible à la casse). Si vous respectez cette règle, l’autoloader trouve toujours vos classes — vous n’écrivez jamais de require/include.
⚠️ Piège : si vous créez
src/Entity/booking.php(minuscule) ou déclareznamespace App\Entities;(pluriel) sans que le dossier corresponde, l’autoload échoue avec « Class not found ». Le trio namespace ↔ dossier ↔ nom de fichier doit être cohérent au caractère près. Le MakerBundle et l’IDE génèrent tout ça correctement — mais quand vous créez un fichier à la main, vérifiez la correspondance.
4. L’autoloader en pratique
Composer génère un autoloader dans vendor/autoload.php. Le front controller le charge une fois (souvenez-vous du chapitre 1.5, via vendor/autoload_runtime.php), et à partir de là, toute classe référencée est chargée à la demande. Vous n’avez rien à faire.
Quand vous ajoutez une classe, tout marche immédiatement (l’autoloader PSR-4 calcule le chemin dynamiquement). Après un déploiement, on optimise l’autoloader (une table de correspondance figée, plus rapide) :
composer dump-autoload --optimize # génère une classmap optimisée (prod)5. Anatomie d’un composer.json
{
"name": "acme/booklab",
"type": "project",
"require": {
"php": ">=8.2",
"symfony/framework-bundle": "7.4.*",
"doctrine/orm": "^3.0"
},
"require-dev": {
"symfony/maker-bundle": "^1.0",
"phpunit/phpunit": "^11.0"
},
"autoload": { "psr-4": { "App\\": "src/" } },
"autoload-dev": { "psr-4": { "App\\Tests\\": "tests/" } },
"scripts": {
"test": "phpunit"
}
}require: dépendances de production.require-dev: dépendances de développement (tests, générateurs) — absentes en prod (composer install --no-dev).autoload/autoload-dev: les mappings PSR-4.scripts: des raccourcis (composer test).
💡 Pour un dev Next.js :
require↔dependencies,require-dev↔devDependencies,scripts↔scriptsdepackage.json. La séparation prod/dev est identique. Lepsr-4n’a pas d’équivalent direct — c’est la config d’autoload que Node fait implicitement par résolution de chemins.
6. Contraintes de version (SemVer)
Composer utilise le versioning sémantique avec des opérateurs :
| Contrainte | Autorise | Interdit |
|---|---|---|
^7.4 | ≥ 7.4.0 et < 8.0.0 (mises à jour mineures/patch) | 8.x |
~7.4.2 | ≥ 7.4.2 et < 7.5.0 (patch seulement) | 7.5.x |
7.4.* | n’importe quel 7.4.x | 7.5, 8.0 |
>=8.2 | 8.2 et au-delà | — |
* | n’importe quoi | rien (à proscrire) |
^ (caret) est le plus courant : il autorise tout ce qui ne casse pas selon SemVer. Pour Symfony, on épingle souvent 7.4.* afin de rester sur la lignée LTS choisie.
⚠️ Piège :
composer installrespecte lecomposer.lock(versions figées, reproductible) ;composer updaterecalcule les versions selon les contraintes et réécrit le lock. Ne lancez jamaiscomposer updatesur un serveur de prod : onupdateen dev (en testant), on committe le lock, et oninstallen prod. Exactement la même discipline quenpm civsnpm update.
✏️ Exercices
Exercice 1. Donnez le chemin de fichier attendu pour les classes suivantes (mapping App\ → src/) : (a) App\Security\Voter\BookingVoter ; (b) App\Dto\CreateBookingInput.
✅ Solution
(a) src/Security/Voter/BookingVoter.php. (b) src/Dto/CreateBookingInput.php. Chaque segment de namespace après App\ devient un dossier, et le nom de la classe devient le fichier .php (casse identique).
Exercice 2. Ce code lève « Class DateTimeImmutable not found » dans un fichier commençant par namespace App\Domain;. Pourquoi, et deux façons de corriger ?
namespace App\Domain;
final class Clock { public function now() { return new DateTimeImmutable(); } }✅ Solution
DateTimeImmutable est dans le namespace global, mais sans \ ni use, PHP la cherche dans App\Domain\DateTimeImmutable. Deux corrections : (1) préfixer par \ → new \DateTimeImmutable() ; (2) importer en haut du fichier → use DateTimeImmutable; puis new DateTimeImmutable(). (Au passage, typez : public function now(): \DateTimeImmutable.)
Exercice 3. Un collègue lance composer update sur le serveur de prod « pour être sûr d’avoir les dépendances ». Quel est le risque et quelle est la bonne pratique ?
✅ Solution
composer update recalcule les versions selon les contraintes et peut installer des versions différentes de celles testées (patchs/minors non validés), avec un risque de régression en prod. Bonne pratique : composer update uniquement en dev (puis tests), committer le composer.lock, et en prod lancer composer install --no-dev --optimize-autoloader qui respecte le lock (reproductible). Analogue à npm ci vs npm update.
🧠 Quiz de révision
1. En PHP moderne, importe-t-on des fichiers ou des classes ?
Des classes (via use Namespace\Classe;). L’autoload trouve et inclut le fichier automatiquement ; on n’écrit jamais de require/include pour son propre code.
2. Que fait PSR-4 ?
Il mappe un préfixe de namespace à un dossier (App\ → src/), de sorte que le namespace d’une classe détermine le chemin de son fichier (un fichier = une classe).
3. Où se trouve App\Repository\SlotRepository ?
App\Repository\SlotRepository ?Dans src/Repository/SlotRepository.php.
4. Différence entre composer install et composer update ?
composer install et composer update ?install respecte le composer.lock (versions figées, reproductible) ; update recalcule les versions selon les contraintes de composer.json et réécrit le lock.
5. Que signifie la contrainte ^7.4 ?
^7.4 ?Toutes les versions ≥ 7.4.0 et < 8.0.0 (mises à jour mineures et correctives compatibles selon SemVer), mais pas 8.x.
Prochain chapitre : 2.7 — Erreurs, générateurs, Fibers — exceptions et hiérarchie Throwable, générateurs (yield), et pourquoi PHP n’a pas d’async/await.