Apprendre la POO en PHP #3 – Namespaces, autoloading et organisation du code

Comment organiser proprement ses classes en PHP. Introduction à Composer.

On continue la découverte de la programmation orientée objet en PHP avec l’organisation du code, des fichiers et l’appel aux différentes classes qui constituent notre site ou application.

Au programme :

Introduction et rappels

Rappel d’une bonne pratique vue dans les bases de la POO #1 :

1 classe = 1 fichier

Si j’ai une classe qui s’appelle Personnage, alors elle est rangée dans un fichier Personnage.php. Et ce fichier ne contient que cette classe, rien d’autre.

Mais pour un site ou une application web, le nombre de classes nécessaires à son bon fonctionnement peut vite devenir très important. Il va donc falloir trouver un moyen de :

  • Ranger correctement les fichiers des classes et définir une arborescence claire
  • Comment gérer le fait qu’on peut avoir 2 classes qui s’appellent pareil ?
  • Trouver des techniques pour charger les classes efficacement (et non une par une à chaque fois)

Une réponse technique aux 2 premiers points est le concept de namespaces.

Les namespaces en PHP

Au début, en créant 1 fichier par classe, on peut se retrouver avec cette organisation de fichier :

src/
├── Personnage.php
├── Guerrier.php
├── Magicien.php
└── Inventaire.php

Ça fonctionne, c’est clair, et pour un petit projet ce n’est pas un problème. Mais quand le projet grandit, deux choses arrivent très vite :

  • Trop de classes au même endroit. Le dossier src/ contient trop de fichiers. Il devient un peu « fourre-tout ». Alors on commence à créer des dossiers pour ranger les classes de façon plus logique.
  • Les collisions de noms. Deux classes qui ont le même nom mais qui ne représente pas la même chose.

Exemple avec cette nouvelle arborescence (on a créé des dossiers et sous dossiers) :

src/
├── Game/
│   └── Domain/
│       ├── Character/
│       │   ├── Personnage.php   --- ici
│       │   ├── Guerrier.php
│       │   ├── Magicien.php
│       │   └── Archer.php
│       ├── Inventory/
│       │   ├── Inventaire.php
│       │   ├── Potion.php
│       │   └── Arme.php
│       └── Combat/
│           ├── Attaque.php
│           └── Degats.php
└── Admin/
    └── Domain/
        ├── Character/
        │   └── Personnage.php   --- ici
        ├── User/
        │   ├── User.php
        │   └── Role.php
        └── Dashboard/
            ├── Dashboard.php
            └── Stats.php

On a donc une collision sur le nom de classe Personnage.

C’est dans ce cas de figure que les namespaces deviennent indispensables.

Définition des namespaces et mise en place

Un namespace, c’est une façon de dire : « Cette classe appartient à un groupe précis ».

Ici, on a 2 groupes :

  • Game\Domain\Character\Personnage
  • Admin\Domain\Character\Personnage

Même si les classes sont dans des fichiers séparés, cela permet de :

  • structurer le projet de manière propre
  • éviter les collisions de noms
  • rendre le code plus clair
  • préparer l’autoloading (qui repose sur ce principe)

Et voici comment déclarer un namespace dans un fichier PHP :

<?php
declare(strict_types=1);

namespace Game\Domain\Character;

class Personnage
{
    // ...
}

On a donc déclaré que la classe Personnage vit dans le groupe Game\Domain\Character. Le nom complet de la classe devient : Game\Domain\Character\Personnage. Ce nom complet est très important car c’est ce que PHP utilise réellement pour identifier la classe (et la différencier d’une autre qui aurait le même nom).

On appelle ça un FQCN (Fully Qualified Class Name)

Et pour la seconde classe :

<?php
declare(strict_types=1);

namespace Admin\Domain\Character; // Base du namespace différente

class Personnage
{
    // ...
}

Utilisation des classes, use en PHP

Une fois les namespaces déclarés, on peut appeler les classes de 2 façons.

Avec ce nom complet :

$hero = new \Game\Domain\Character\Personnage();

Le \ au début signifie qu’on part de la racine du namespace. Cette méthode fonctionne mais peut être un peu lourde à l’écriture et à la lecture.

Avec un use :

// Début d'un fichier
use Game\Domain\Character\Personnage;

// Quelque part dans le code
$hero = new Personnage();

On précise en fait au début du fichier quelles vont être la ou les classes utilisées dans ce fichier. Et dans le fichier, plus besoin de préciser la totalité du namespace à chaque appel de la classe.

Un exemple avec le Personnage qui gère un Inventaire :

<?php
declare(strict_types=1);

namespace Game\Domain\Character;

use Game\Domain\Inventory\Inventaire;

class Personnage
{
    private Inventaire $inventaire;

    public function __construct()
    {
        $this->inventaire = new Inventaire();
    }
}

Ici, on a un code :

  • lisible
  • bien typé
  • structuré
  • et surtout : PHP sait exactement quelle classe on utilise.

Important : si tu dois faire appel à une classe qui est dans le même namespace, tu n’as pas besoin d’utiliser un use. PHP commencera par chercher la classe « inconnue » dans le namespace courant. Et forcément, si tu oublies le use d’une classe utilisée (d’un autre namespace), PHP te renverra une erreur (mais ton éditeur de code le fera également).

Important : si malgré tout, dans un fichier, tu dois faire appel à 2 classes qui s’appellent pareil, il faudra alors créer un alias pour une d’entre elles. Exemple :

<?php
declare(strict_types=1);

namespace Admin\Domain\User;

use Admin\Domain\Character\Personnage;
use Game\Domain\Character\Personnage as GamePersonnage;

// Et dans le code, on pourra faire : 
$personnage = new Personnage;
$gamePersonnage = new GamePersonnage;

Organisation des fichiers

Comment faire maintenant pour organiser les fichiers quand on a 10, 20, 50, 100 classes (ou plus) dans un projet sans se perdre ?

Organiser les fichiers et les dossiers par « sujets »

Au lieu de tout laisser dans un même dossier, on va ranger par responsabilité :

  • Character : tout ce qui concerne les personnages
  • Inventory : tout ce qui concerne l’inventaire et les objets
  • Combat : tout ce qui est lié aux interactions et mécaniques des combats
  • Game : tout ce qui est lié au déroulement du jeu
  • Admin : tout ce qui est lié à l’administration des différents éléments
  • etc.

L’idée n’est pas d’avoir une arborescence compliquée, c’est juste d’avoir un rangement logique.

Les namespaces reprennent les chemins de l’arborescence

Si le fichier est src/Game/Domain/Character/Personnage.php alors son namespace doit être :

namespace Game\Domain\Character;

C’est primordial de respecter cette règle de nommage, qui permettra de réaliser l’autoloading efficacement (cf. section suivante).

Require et autoloading en PHP

Maintenant qu’on a tout éclaté dans plusieurs dizaines de dossiers et fichiers, il reste le problème majeur de charger toutes ces classes.

Quand on débute en PHP et qu’on a quelques fichiers, on utilise souvent la fonction require :

// Game
require_once __DIR__ . '/../src/Game/Domain/Character/Personnage.php';
require_once __DIR__ . '/../src/Game/Domain/Character/Guerrier.php';
require_once __DIR__ . '/../src/Game/Domain/Character/Magicien.php';
require_once __DIR__ . '/../src/Game/Domain/Inventory/Inventaire.php';
// ...

// Admin
require_once __DIR__ . '/../src/Admin/Domain/Character/Personnage.php';
// ...

Ca fonctionne très bien, mais à gérer sur le long terme, avec des dizaines ou centaines de fichiers, ça devient vite un enfer :

  • tu dois ajouter une ligne à chaque nouvelle classe
  • si tu bouges un fichier, il faut mettre à jour tous tes require_once
  • ça devient long, fragile, et ça casse vite

Et surtout : c’est exactement ce qu’on veut éviter dans un projet moderne !

Autoloading

L’autoloading se déclenche lorsque PHP rencontre une classe qu’il ne connait pas encore. Il va alors rechercher dans l’arborescence le fichier correspondant. Mais il faut lui expliquer comment rechercher ce fichier.

L’idée est donc de déclarer une règle du genre : « si on demande Game\Domain\Character\Personnage, alors le fichier est là », « si on demande Game\Domain\Inventory\Inventaire, alors le fichier est là », de façon automatisée.

PHP permet de mettre en oeuvre ce mécanisme grâce à la fonction spl_autoload_register :

spl_autoload_register(function ($class) {
    $prefixes = [
        'Game\\'  => __DIR__ . '/src/Game/',
        'Admin\\' => __DIR__ . '/src/Admin/',
    ];

    foreach ($prefixes as $prefix => $baseDir) {
        if (strncmp($prefix, $class, strlen($prefix)) !== 0) {
            continue;
        }

        $relativeClass = substr($class, strlen($prefix));
        $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

        if (file_exists($file)) {
            require $file;
        }
    }
});

Un peu d’explications :

  • C’est cette fonction qui est appelée lorsque PHP rencontre une nouvelle classe. Avec le nom complet de la classe en paramètre ($className)
  • On utilise ce nom complet pour créer le chemin vers le fichier ($fullPath)
  • S’il y a un fichier alors on l’intègre avec require.

Si la classe est Game\Domain\Character\Personnage, on va donc require src/Game/Domain/Character/Personnage.php.

Ce genre d’autoload peut être suffisant pour apprendre ou pour un (petit) projet perso, mais en pratique, on rencontre vite des limites :

  • A chaque projet, si on a changé un peu son mode d’organisation, il faut revoir cette méthode
  • Je dois gérer manuellement chaque dossier racine de mon projet (Game et Admin ici)
  • Je ne peux pas intégrer de librairies externes facilement

Heureusement, la solution professionnelle pour gérer son arborescence et ses dépendances existe : Composer.

Composer et PSR-4

Composer est le gestionnaire de dépendances PHP.

Mais même si on n’installe aucune dépendance dans son projet, Composer reste est un outil très efficace pour gérer ses fichiers et son arborescence, car il se base sur le standard PSR-4.

Ce standard encadre ce qu’on a vu jusqu’à présent dans cet article :

  • Un namespace correspond à un dossier
  • Une classe correspond à un fichier

Le fichier composer.json

Pour utiliser composer, on commence par créer un fichier composer.json. Ce fichier peut être créé en ligne de commande, mais aussi manuellement. Voici à quoi peut ressembler un fichier composer.json basique dans le cadre de notre exemple :

{
  "autoload": {
    "psr-4": {
      "Game\\": "src/Game/",
      "Admin\\": "src/Admin/"
    }
  }
}

Dans cet article, les namespaces Game\ et Admin\ correspondent directement à des dossiers physiques dans src/. Le mapping PSR-4 associe donc chaque préfixe de namespace à son dossier racine (src/Game/, src/Admin/). Cette approche facilite la lecture de l’arborescence et la compréhension des dépendances entre contextes métier.

Donc : Game\Domain\Character\Personnage sera cherchée dans src/Game/Domain/Character/Personnage.php. Exactement ce qu’on a mis en place.

Une fois le fichier composer.json écrit et notre arborescence en place, il faut lancer la commande suivante :

composer dump-autoload

Composer va alors générer un dossier vendor/ contenant notamment un fichier autoload.php.

Il suffit alors d’inclure ce fichier une seule fois (souvent dans index.php) :

require_once __DIR__ . '/../vendor/autoload.php';

use Game\Domain\Character\Guerrier;

$hero = new Guerrier();

Et le tour est joué ! Pas besoin d’inclure les classes une par une, pas besoin de créer soi même la logique d’import, Composer s’occupe de tout automatiquement !

En respectant PSR-4 dans l’organisation de ses fichiers, le chargement des classes devient un non sujet et on peut alors réfléchir efficacement à l’architecture de notre projet.

Important : si tu déplaces une classe, si tu mets à jour le nom d’un dossier (et donc d’un namespace), des erreurs d’import peuvent survenir. Il suffit alors de relancer la commande suivante pour tout remettre en place :

composer dump-autoload

Conclusion et conseils d’architecture

Avec les namespaces et Composer on a désormais toutes les clés pour créer un projet complexe comprenant plusieurs (dizaines de) classes. La réflexion ne porte plus sur le nommage des fichiers ou sur comment appeler ses classes mais bien sur son arborescence et donc l’architecture de son projet.

Quelques conseils pour une bonne architecture :

  • Organiser par responsabilité (et pas par type de fichier) : Évite les dossiers « Classes » ou « Utils ». Range plutôt par sujet : Character, Inventory, Combat, Admin, etc.
  • Garder des namespaces courts et explicites : Si ton namespace devient une phrase à rallonge, c’est souvent que l’arborescence est trop compliquée. Un bon namespace doit servir de repère rapide.
  • Éviter le dossier « fourre-tout » : Les dossiers Utils/ ou Helpers/ finissent souvent par contenir “un peu de tout”. Si tu ne sais pas où ranger une classe, c’est souvent qu’elle n’a pas une responsabilité claire (ou qu’il manque un module dédié).
  • Avoir un point d’entrée propre : En général, public/index.php lance l’application et src/ contient le code métier. Le point d’entrée doit rester léger : charger l’autoload et déléguer le reste.
  • Respecter PSR-4 dès le début : Même sur un petit projet, ça te fait gagner du temps. Et surtout, tu évites la galère de devoir tout renommer et déplacer quand le projet grossit.

Et c’est exactement là que la POO devient intéressante, quand ton code reste lisible et organisé même quand ton projet grossit !


Qui a codé ce superbe contenu ?

Keep learning

Other content to discover