T: / Articles techniques / PHP
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 :
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 :
Une réponse technique aux 2 premiers points est le concept de namespaces.
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 :
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.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.
Un namespace, c’est une façon de dire : « Cette classe appartient à un groupe précis ».
Ici, on a 2 groupes :
Game\Domain\Character\PersonnageAdmin\Domain\Character\PersonnageMême si les classes sont dans des fichiers séparés, cela permet de :
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
{
// ...
}
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 :
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;
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é :
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).
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 :
require_onceEt surtout : c’est exactement ce qu’on veut éviter dans un projet moderne !
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 :
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 :
Heureusement, la solution professionnelle pour gérer son arborescence et ses dépendances existe : Composer.
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 :
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
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 :
Et c’est exactement là que la POO devient intéressante, quand ton code reste lisible et organisé même quand ton projet grossit !
Other content to discover