Skip to Content
SymfonyPartie 2 — PHP moderne pour dev JS/TS2.6 — Composer, PSR-4, namespaces

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 avec use (l’équivalent de l’import JS, mais pour des symboles, pas des fichiers).
  • PSR-4 est la convention qui mappe un préfixe de namespace à un dossier : App\ → src/. Donc App\Entity\Bookingsrc/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 namespace et des use corrects, y compris les alias.
  • Comprendre le mapping PSR-4 namespace → fichier et savoir déduire l’un de l’autre.
  • Lire un composer.json et manier require/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 si import retrouvait 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\Mailer

Pour 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 tiers

2.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;, écrire new DateTimeImmutable() sans \ ni use fait chercher App\Domain\DateTimeImmutable (qui n’existe pas) → erreur « class not found ». Réflexe : soit préfixer par \ (new \DateTimeImmutable()), soit ajouter use DateTimeImmutable; en haut. Les IDE ajoutent le use automatiquement.

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\Bookingsrc/Entity/Booking.php
App\Service\Mailersrc/Service/Mailer.php
App\Controller\Api\BookingControllersrc/Controller/Api/BookingController.php
App\Tests\Service\MailerTesttests/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éclarez namespace 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 : requiredependencies, require-devdevDependencies, scriptsscripts de package.json. La séparation prod/dev est identique. Le psr-4 n’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 :

ContrainteAutoriseInterdit
^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.x7.5, 8.0
>=8.28.2 et au-delà
*n’importe quoirien (à 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 install respecte le composer.lock (versions figées, reproductible) ; composer update recalcule les versions selon les contraintes et réécrit le lock. Ne lancez jamais composer update sur un serveur de prod : on update en dev (en testant), on committe le lock, et on install en prod. Exactement la même discipline que npm ci vs npm 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 ?

Dans src/Repository/SlotRepository.php.

4. Différence entre 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 ?

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.