Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Namespaces, Composer et autoloading

PHP n’a pas de systĂšme de modules, mais trois mĂ©canismes plus modestes, require, les espaces de noms et un crochet d’autoloading, que Composer assemble en un gestionnaire de paquets comparable Ă  npm, pip, Maven ou Cargo. Vous n’écrirez plus jamais de require pour l’une de vos propres classes, et il reste utile de savoir ce que Composer fait Ă  votre place.

Les trois primitives

require 'file.php'; lit et exĂ©cute un fichier, une fois par appel. C’est ainsi que le code se partageait en 2005, et c’est encore ainsi que tout s’amorce aujourd’hui, avec un unique require de vendor/autoload.php en tĂȘte de votre point d’entrĂ©e.

Un espace de noms est un prĂ©fixe sur un nom de classe. namespace App\Billing; en tĂȘte d’un fichier fait de chaque classe qui y est dĂ©clarĂ©e une App\Billing\Something, et n’apporte rien d’autre, aucune hiĂ©rarchie, aucune visibilitĂ©, aucun lien avec un dossier. App\Billing n’est pas « dans » App, ce sont simplement deux chaĂźnes qui partagent un prĂ©fixe.

Le crochet d’autoloading est la piĂšce qui rend les deux premiĂšres utiles. Quand PHP rencontre une classe qu’il n’a jamais vue, il appelle une fonction que vous avez enregistrĂ©e, en lui passant le nom de la classe, et cette fonction est censĂ©e faire un require du bon fichier, aprĂšs quoi PHP rĂ©essaie.

<?php
declare(strict_types=1);

spl_autoload_register(function (string $class): void {
    $file = __DIR__ . '/src/' . str_replace('\\', '/', $class) . '.php';
    if (is_file($file)) {
        require $file;
    }
});

$invoice = new App\Billing\Invoice(); // loads src/App/Billing/Invoice.php

Le mĂ©canisme se rĂ©sume Ă  cette closure, et Composer en Ă©crit une meilleure version, avec un cache, qu’il vous remet.

Un petit Ă©lĂ©phant lit un bout de papier oĂč est Ă©crit App\Billing\Invoice, puis suit une ligne pointillĂ©e le long d'un classeur dont les tiroirs sont Ă©tiquetĂ©s App, Billing, Invoice.php, et ouvre le dernier

PSR-4 : du nom au chemin

La rùgle que suit la closure ci-dessus porte un nom, PSR-4, et Composer l’applique à partir d’un bloc de composer.json :

{
    "name": "acme/shop",
    "type": "project",
    "require": {
        "php": "^8.4",
        "ext-intl": "*"
    },
    "require-dev": {
        "phpunit/phpunit": "^12.0"
    },
    "autoload": {
        "psr-4": { "App\\": "src/" }
    },
    "autoload-dev": {
        "psr-4": { "App\\Tests\\": "tests/" }
    }
}

App\Billing\Invoice vit dans src/Billing/Invoice.php : une classe par fichier, un nom de fichier Ă©gal au nom de la classe, et des dossiers qui reprennent les segments de l’espace de noms aprĂšs le prĂ©fixe. L’arborescence qui en rĂ©sulte est la mĂȘme sur tous les projets PHP modernes que vous ouvrirez :

shop/
├── composer.json
├── composer.lock
├── public/
│   └── index.php        # require __DIR__ . '/../vendor/autoload.php';
├── src/
│   └── Billing/
│       └── Invoice.php  # namespace App\Billing;
├── tests/
│   └── Billing/
│       └── InvoiceTest.php
└── vendor/              # generated, not committed

Ajoutez une classe sous src/, et elle est trouvĂ©e Ă  la requĂȘte suivante, sans commande Ă  lancer, puisque PSR-4 rĂ©sout par chemin au moment de l’appel. La stratĂ©gie plus ancienne classmap parcourt les dossiers pour en faire un tableau gĂ©nĂ©rĂ©, et rĂ©clame un composer dump-autoload aprĂšs chaque nouveau fichier ; vous la rencontrerez dans les projets anciens.

Composer, dans le vocabulaire que vous connaissez

composer.json joue le rÎle de votre package.json, composer.lock celui du fichier de verrouillage, vendor/ celui de node_modules, généré et ignoré par Git, et Packagist celui du registre public.

composer init                          # interactive composer.json
composer require monolog/monolog       # add and install, updates the lock
composer require --dev phpstan/phpstan # development-only dependency
composer install                       # reproduce exactly what the lock says
composer update                        # resolve anew, rewrite the lock
composer update monolog/monolog        # ... for one package only
composer show                          # what is installed, with versions
composer outdated                      # what has a newer release
composer audit                         # known vulnerabilities in the lock
composer dump-autoload -o              # regenerate the autoloader, optimised

install obĂ©it Ă  composer.lock, tandis qu’update le réécrit. En CI et en production vous lancez install, et vous obtenez exactement les versions que votre collĂšgue a testĂ©es. Committez composer.lock pour une application ; pour une bibliothĂšque, la plupart des auteurs ne le font pas, afin qu’elle soit testĂ©e avec ce que ses utilisateurs rĂ©solvent, et c’est le mĂȘme dĂ©bat que dans tous les autres Ă©cosystĂšmes.

Les contraintes de version suivent semver, et ^8.4 signifie « 8.4 ou toute 8.x ultĂ©rieure ». L’entrĂ©e php de require est une contrainte elle aussi ; avec config.platform.php vous figez la version contre laquelle Composer rĂ©sout, pour qu’un dĂ©veloppeur sous PHP 8.5 ne tire pas un paquet que votre production en 8.4 ne peut pas exĂ©cuter.

Les scripts vivent sous une clĂ© scripts et se lancent avec composer run nom ou comme crochets de cycle de vie (post-install-cmd), Ă  la maniĂšre des scripts npm. Les extensions ne sont pas des paquets : ext-intl dans require vĂ©rifie seulement que l’extension est prĂ©sente, et l’installer est le travail de votre gestionnaire de paquets systĂšme, de PECL ou de PIE. Les outils que vous installeriez globalement ailleurs (linters, analyseurs) vont dans require-dev, pour que chaque dĂ©veloppeur et la CI utilisent la mĂȘme version ; composer global require existe et il vaut mieux le laisser de cĂŽtĂ©. Un monorepo dĂ©clare ses paquets internes comme dĂ©pĂŽts path, et Composer crĂ©e des liens symboliques.

Deux panneaux. À gauche, Ă©tiquetĂ© install : un Ă©lĂ©phant lit un registre cadenassĂ© et empile des boĂźtes exactement comme listĂ©. À droite, Ă©tiquetĂ© update : le mĂȘme Ă©lĂ©phant consulte un tableau d'affichage couvert d'annonces de versions, choisit de nouvelles boĂźtes, puis réécrit le registre

Vivre avec les espaces de noms

Dans un fichier, une instruction use importe un nom pour que vous puissiez l’écrire court. Les alias rĂ©solvent les collisions, et les fonctions comme les constantes peuvent aussi ĂȘtre importĂ©es :

<?php
declare(strict_types=1);

namespace App\Billing;

use App\Customer\Customer;
use DateTimeImmutable as Date;
use function App\Support\money;
use const App\Support\CURRENCY;

final class Invoice
{
    public function __construct(
        public readonly Customer $customer,
        public readonly Date $issuedOn,
    ) {
    }

    public function total(): string
    {
        return money(1999, CURRENCY);
    }
}

echo Invoice::class, PHP_EOL; // App\Billing\Invoice

Invoice::class donne le nom pleinement qualifiĂ© sous forme de chaĂźne, ce qui est ce que vous passez Ă  un conteneur, Ă  un mock ou aux vĂ©rifications instanceof qui prennent une chaĂźne. Un antislash initial, comme dans \DateTimeImmutable, nomme une classe de l’espace de noms global sans passer par use.

Les fonctions se rabattent sur l’espace de noms global, alors que les classes ne le font pas. Appeler strlen() dans App\Billing cherche d’abord App\Billing\strlen, puis strlen, tandis qu’appeler new DateTimeImmutable() sans use ni antislash initial Ă©choue. Vous verrez \strlen() dans certaines bibliothĂšques, parce que l’antislash saute la recherche et laisse OPcache inliner quelques fonctions intĂ©grĂ©es ; c’est une micro-optimisation, pas une convention que vous ayez Ă  adopter.

Les standards Ă  connaĂźtre

Le PHP-FIG est le groupe oĂč les auteurs de frameworks et de bibliothĂšques s’accordent sur des interfaces, publiĂ©es sous forme de PSR. Une PSR est un contrat plutĂŽt qu’une bibliothĂšque : les implĂ©mentations viennent de nombreux Ă©diteurs, et vous pouvez en remplacer une par une autre parce que votre code ne voit que l’interface. Voici celles que vous croiserez dĂšs la premiĂšre semaine :

  • PSR-4, l’autoloading, ci-dessus.
  • PER Coding Style, successeur de PSR-12 : placement des accolades, indentation, nommage. PHP-CS-Fixer ou PHP_CodeSniffer le font respecter.
  • PSR-3, LoggerInterface. Toutes les bibliothĂšques journalisent Ă  travers elle ; vous branchez le logger de votre choix.
  • PSR-7, PSR-15 et PSR-17 : objets requĂȘte et rĂ©ponse HTTP, middlewares, et leurs fabriques. Les $_GET et header() du langage sont traitĂ©s dans Une requĂȘte web, sans framework ; PSR-7 est le modĂšle objet que les bibliothĂšques partagent par-dessus.
  • PSR-11, ContainerInterface, pour qu’une bibliothĂšque puisse demander un service Ă  n’importe quel conteneur.
  • PSR-14, la distribution d’évĂ©nements, PSR-6 et PSR-16, le cache.

Une bibliothĂšque qui type son constructeur contre Psr\Log\LoggerInterface fonctionne dans tous les frameworks citĂ©s dans ce livre. Cette interopĂ©rabilitĂ© explique pourquoi l’écosystĂšme compte moins de forks et de réécritures que sa taille ne le laisserait penser.

Le piĂšge

Ne modifiez jamais un fichier de vendor/. Le prochain composer install sur n’importe quelle machine efface la modification, et personne ne saura pourquoi la production diffĂšre de votre portable. Forkez le paquet, ou surchargez la classe par votre propre entrĂ©e d’autoloading, ou envoyez le correctif en amont.

Le second piĂšge est plus discret : vous ajoutez une classe, PHP dit qu’elle n’existe pas, et vous passez vingt minutes Ă  chercher une faute de frappe qui n’existe pas. Comparez l’espace de noms au dossier : App\Billing\Invoice doit ĂȘtre src/Billing/Invoice.php, lettre pour lettre, casse comprise sous Linux. Si le projet utilise classmap plutĂŽt que psr-4, lancez composer dump-autoload et passez Ă  autre chose.

Composer n’est pas une Ă©tape de build, puisqu’il n’y a rien Ă  compiler, Ă  empaqueter ni Ă  transpiler : aprĂšs composer install, le code s’exĂ©cute tel quel.

Une fois les classes trouvées et les paquets installés, il reste la bibliothÚque standard sur laquelle vous vous appuierez chaque jour, et Chaßnes, nombres, dates et JSON en fait le tour.