Bien sûr, tout développeur apprend rapidement que les paramètres de connexion à une base de données ne s’écrivent jamais en dur. Mais il peut exister une multitude d’autres adhérences entre un programme et son contexte d’exécution, dont la résolution est parfois moins intuitive. Segmenter convenablement une application en unités isolées afin d’en permettre le test, produire une base de code unifiée qui fonctionne indifféremment dans le nuage ou en local, avec ou sans journalisation, etc., sont autant de bonnes raisons, parmi d’autres, de s’abstenir d’instancier explicitement les composants au sein d’un programme. Apprenons à les injecter de façon paramétrique en PHP.
1. Un peu de théorie
Le lecteur ayant déjà connaissance des définitions présentées dans cette première section, peut passer directement à la mise en œuvre en PHP.
La programmation orientée objet vise à structurer le code source des applications en briques emboîtables et réutilisables, dénommées classes. L’un des bénéfices de ce paradigme est la hiérarchisation arborescente qu’il est possible d’établir entre les classes, et qui modélise les interactions des objets au cours du cycle de vie de l’application.
Des analogies avec le monde réel sont souvent utilisées pour vulgariser les concepts de la programmation orientée objet. Ainsi, la voiture, en tant que classe composite, comprend un moteur qui transmet un mouvement à des roues. Celles-ci sont remplaçables, et leurs caractéristiques techniques (qualité de la gomme, gonflage optimal, etc.) sont sans incidence sur le principe de fonctionnement du moteur, pourvu qu’elles respectent un certain standard (le diamètre, les fixations, etc.) appelé « interface » en programmation objet.
La programmation orientée objet permet ainsi de construire des applications dans lesquelles, théoriquement, chaque brique peut être conçue, modifiée et testée indépendamment des autres.
Plus avant dans l’abstraction, l’utilisateur-conducteur n’est pas concerné directement par les caractéristiques des roues ni même par les propriétés du moteur, pourvu que toute voiture lui offre une interface standardisée : pédales, volant, tachymètre, etc.
Mais l’analogie s’arrête souvent là, car les architectures applicatives reposent généralement sur des concepts qui vont au-delà du découpage en objets du monde réel. On aime les concevoir en couches ayant chacune des responsabilités isolées des autres.
On peut trouver notamment, selon les types d’applications, une couche responsable de gérer les interactions avec l’utilisateur ; une autre pour assurer la logique transactionnelle et les règles métier ; puis une autre pour négocier le dialogue avec les dispositifs de persistance et de stockage des données, etc.
On parle alors de contrôleurs, de vues, de services, de modèle relationnel et autres.
Chaque couche est constituée de plusieurs classes, et un objet d’une couche donnée ne doit dépendre que d’objets de la même couche ou des couches immédiatement inférieures. Pour reprendre l’analogie automobile, le conducteur a besoin d’une pédale de frein, et préfère éviter de devoir toucher directement les plaquettes pour ralentir.
En outre, les interactions entre classes de couches différentes sont représentées par des interfaces, à l’image de notre conducteur qui sait manipuler n’importe quel modèle de véhicule en vertu de l’interface « volant-pédales ». Ainsi, qu’il s’agisse d’une voiture commerciale, familiale, ou même d’un simulateur d’arcade, sa prise en main pour contrôler l’engin reste la même.
1.1 Patrons et anti-patrons
Les patrons de conception (design patterns) sont des solutions éprouvées et standardisées, en termes de construction de code, pour répondre à des problématiques précises et récurrentes. Les buts essentiels de la programmation orientée objet étant la lisibilité et la réutilisation (de code, mais aussi d’efforts de conception), il est fortement recommandé de suivre les bonnes pratiques des design patterns, qui sont amplement documentées [1].
À l’inverse, on appelle anti-patron une forme conceptuelle qui, en dépit de son éventuelle élégance ou de sa subtilité, briserait ce principe de réutilisation (même si elle apporte localement une solution réelle au problème posé).
1.2 Injection
L’injection de dépendances est l’un de ces patrons fameux, et qui sert deux buts. D’une part, il s’agit de conférer à un dispositif tiers la responsabilité de faire le lien entre des composants a priori indépendants dans leur nature, mais qui reposent l’un sur l’autre pour fonctionner au sein d’un système. De l’autre, elle offre la possibilité (souvent appelée Inversion of Control) de brancher des variantes spécifiques des différents composants au moment de l’assemblage du système, en fonction de certains paramètres extérieurs.
Considérons par exemple une application web qui a besoin d’envoyer un message à l’utilisateur. Différentes options peuvent être envisagées selon les capacités de l’infrastructure d’hébergement (quotas, passerelle), le choix de l’utilisateur (e-mail, SMS, notifications mobiles), ou même l’environnement (test, production, etc.).
Il ne serait pas efficace que le producteur du message (un composant au sein de l’application) connaisse trop de détails sur l’implémentation du composant d’acheminement du message (le consommateur). Le code qui construit l’e-mail ne doit pas savoir si l’envoi sera effectué de façon synchrone ou en file d’attente, par protocole chiffré ou non, ou s’il ne devra qu’être simulé dans le cadre d’un scénario de test.
Toutes ces possibilités de fonctionnement peuvent prendre, dans le monde objet, la forme de classes différentes partageant une interface commune, et il faudrait brancher l’une ou l’autre lors de l’exécution du programme, selon le comportement souhaité. C’est le rôle de l’injection, que nous allons approfondir.
1.3 Singletons
De nombreux services au sein de l’application ne doivent exister qu’en un seul exemplaire (par exemple l’objet qui représente la connexion à une base de données, ou à un serveur de messagerie). Si plusieurs composants différents requièrent une telle instance, un mécanisme doit s’assurer qu’ils travailleront tous avec la même, plutôt qu’avec un individu nouvellement créé chaque fois.
Le patron de conception du singleton pourrait venir à l’esprit pour résoudre ce besoin. Mais nous allons voir que c’est une fausse bonne idée.
Définition : le singleton
Il s’agit d’une classe dont le constructeur est privé, c’est-à-dire qu’aucune autre classe ne peut en créer une instance (new). La classe expose une méthode statique pour obtenir (ou créer, lors de la première invocation) l’instance unique, conservée comme attribut de classe. Différentes variantes peuvent se rencontrer selon les langages, ou pour la prise en charge du parallélisme.
Le problème avec le singleton est que toute classe utilisatrice doit obligatoirement l’invoquer par son nom précis, figé (typiquement TheSingletonClass::getInstance()).
Imaginons un programme qui utilise un composant gérant les sessions des utilisateurs. Ce programme veut pouvoir stocker les sessions, soit en base de données, soit simplement sous forme de fichiers locaux (pour le développement notamment). Ces deux saveurs du gestionnaire de sessions seront représentées par des classes (que nous nommerons respectivement DbSessionMgr et FileSessionMgr) qui implémentent toutes deux l’interface SessionManager.
On aimerait utiliser tantôt une saveur, tantôt l’autre (et potentiellement encore d’autres !) selon l’environnement (station de travail du développeur, plateforme d’intégration continue, serveur de production, etc.), or l’utilisation simple du singleton ne le permet pas : le code aurait à faire un appel figé à DbSessionMgr::getInstance() (ou à un des autres représentants de la même interface) et la flexibilité serait rompue.
Pour rappel, afin de réduire l’adhérence du code, nous souhaitons nous interdire de le rendre « conscient » de son contexte d’exécution. Aussi nous devons proscrire les maltraitances telles que :
if (context == ‘dev’) { . . . } else . . .
(Le contexte auquel il est fait référence dans ces pages est généralement matérialisé par un fichier de propriétés, déposé à un endroit connu de l’application, et qui est typiquement différent d’un environnement à l’autre.)
Tout au plus, ce type de branchements peut s’envisager pendant une phase de chargement et d’initialisation de l’application, mais pas pendant l’exécution de croisière.
1.4 Service Locator
On serait tenté d’imaginer un singleton intelligent qui encapsulerait les choix environnementaux, par exemple via un appel générique :
$environmentSpecificSessionManager = SessionManager::getInstance();
Celui-ci se contenterait de répondre le réel objet correspondant à l’environnement en cours d’usage (l’une des implémentations concrètes de l’interfaceSessionManager) et il serait, seul, autorisé à tester les conditions sur le contexte comme vu plus haut.
Mieux (en apparence) : on pourrait envisager une classe centrale qui assurerait ce comportement évoqué ci-dessus, mais pour toutes les dépendances à la fois : c’est le pattern du Service Locator. À l’initialisation de l’application, il « décide » de l’assemblage des briques en fonction du contexte, puis le programme dans son ensemble peut rester agnostique.
ServiceLocator::getInstance()->getMailManager();
ServiceLocator::getInstance()->getSessionManager();
Cependant, cette conception est très largement regardée comme un anti-pattern, car elle implique que la classe centrale (ici ServiceLocator) soit mentionnée un peu partout dans le code. On la dénomme parfois « god class ».
Il faut lui préférer l’injection externe, aussi associée parfois à l’appellation Inversion of Control (IoC). Il existe de nombreux outils dans presque tous les langages pour la mettre en œuvre (citons le célébrissime Spring en Java). Nous allons maintenant étudier un bon moyen de le faire en PHP.
2. Le paquet PHP-DI
PHP-DI (Dependency Injection) est un formidable projet [2] sous licence MIT, initialement écrit par le français Matthieu Napoli. Il s’agit d’un conteneur de dépendances intelligent, qui réalise l’injection d’instances et de paramètres de configuration, à la demande et de façon lisible et flexible.
2.1 Installation
Classiquement, pour ajouter le paquet PHP-DI au projet, exécutons la commande :
$ composer require php-di/php-di
La bibliothèque se présente sous la forme de fonctions helpers, qui servent à décrire le comportement souhaité pour l’injecteur. Le principe est de laisser l’application donner le contrôle le plus tôt possible à cet injecteur, qui endossera la responsabilité de gérer les instances de tous types de composants dont elle pourra avoir besoin au cours de son cycle de vie.
2.2 Principe de fonctionnement
Le travail réalisé par l’injecteur peut se représenter de la façon suivante :
1- L’application crée un conteneur de composants (dans le cas d’une application web PHP, en cycles requête/réponse, le conteneur est à créer de préférence au niveau du script répartiteur frontal, vers lequel toutes les requêtes sont acheminées).
2- Le conteneur s’initialise et prend connaissance des directives qui caractérisent son comportement (fichier de configuration).
3- L’application demande au conteneur de lui fournir chaque composant qu’elle souhaite utiliser, au moment où elle en a besoin. Si une instance du composant demandé ne s’y trouve pas encore, le conteneur la crée (assemblée en cascade avec toutes les pièces qui constituent le composant parmi les couches inférieures, pièces elles-mêmes puisées/créées automatiquement depuis le conteneur).
2.3 Mise en œuvre
Ainsi, en théorie, grâce à une construction en couches, plus aucune partie du code ne doit extraire « manuellement » un composant depuis le conteneur (au revoir lepattern du Service Locator), et encore moins les instancier explicitement. Plutôt, tout composant utile à l’application ne doit provenir que du conteneur, et ne doit être constitué que de pièces que le conteneur sait construire automatiquement.
Voyons un premier exemple avec le code suivant, que nous allons par la suite transformer au fil de la mise en œuvre de PHP-DI :
class Application {
private $container;
public function __construct() {
$builder = new DI\ContainerBuilder();
$this->container = $builder->build();
}
public function run() {
$sessMgr = $this->container->get(DbSessionMgr::class);
. . .
}
}
Nous voyons dans cet extrait que l’objet Application commence par créer un conteneur, qu’il est le seul à détenir (propriété privée). Puis, lorsque l’application désire exploiter un service de haut niveau (tel que notre gestionnaire de sessions suggéré plus haut), elle n’a qu’à le demander au conteneur. Si le conteneur ne possède pas déjà une instance de DbSessionMgr, l’instruction ->get aura pour conséquence d’en construire une, et de la stocker dans le conteneur pour réutilisation. Notons l’usage de la pseudo-constante ::class : disponible depuis PHP 5.5, elle fournit le nom complet du type, y compris son namespace, sous forme de chaîne de caractères.
L’objet Application est ici, normalement, le seul composant qui aura à manipuler directement le $container car, une fois qu’une instance est gérée par PHP-DI, toutes ses dépendances y sont également automatiquement prises en charge.
Illustrons ceci :
class DbSessionMgr
implements SessionManager {
private $dbConnection;
public function __construct(PDO $db) {
$this->dbConnection = $db;
}
// ...
}
La magie se situe au niveau du constructeur de la classe DbSessionMgr : dès lors que l’application a provoqué la création d’une instance DbSessionMgr via PHP-DI, ce dernier a examiné le constructeur de ladite classe, et a compris qu’elle avait besoin d’une instance de PDO. Et d’où cette instance peut-elle provenir ? Mais, du conteneur ! Par conséquent, si elle n’y existe pas déjà, elle sera elle-même créée dans la foulée, et ainsi de suite de façon récursive.
Définition : Réflexion
La réflexion (Reflection) est la capacité d’un programme à examiner, et potentiellement modifier, sa propre structure (les caractéristiques de ses classes et fonctions) pendant son exécution. La plupart des langages modernes sont réflexifs, c’est-à-dire qu’ils offrent des instructions ou bibliothèques pour exploiter la réflexion.
Très utilisé dans le test unitaire, le concept est également central dans le domaine de l’inversion de contrôle. Ainsi par exemple, le code source peut, pendant l’exécution, examiner une classe donnée et interroger la liste et les types des paramètres attendus par son constructeur.
Nous verrons dans la suite comment instruire PHP-DI pour qu’il sache construire ladite instance PDO avec les bons paramètres de connexion.
Néanmoins, à ce stade, le code de la classe Application ci-dessus fait encore usage explicite du qualificateur DbSessionMgr::class. Or nous ne voulons pas que notre code soit responsable du choix de cette saveur plutôt que de l’autre, la FileSessionMgr (qui se manipule par ailleurs de façon identique par l’application, puisque toutes deux implémentent l’interfaceSessionManager.)
Qu’à cela ne tienne : PHP-DI va nous aider ici aussi. Il suffit de demander au conteneur, non pas une instance explicite d’une implémentation ou de l’autre, mais bien de leur interface !
$sessMgr=$this->container->get(SessionManager::class);
Mais alors, pour que le conteneur puisse savoir quelle classe concrète instancier sur demande de cette interface, il faut le lui avoir spécifié au préalable, lors de l’étape de configuration du conteneur (au moyen du pattern du Builder). Le code de la classe Application devient :
class Application {
private $container;
public function __construct() {
$builder = new DI\ContainerBuilder();
// Configurer le conteneur : fournir
// les mappings entre les interfaces et
// les implémentations
$builder->addDefinitions([
SessionManager::class=> DI\object(DbSessionMgr::class)
]);
$this->container = $builder->build();
}
...
En ayant indiqué à PHP-DI le choix d’implémentation pour l’interface SessionManager, on permet au conteneur de servir automatiquement l’instance concrète à toute partie consommatrice dans le code (aussi bien par appel explicite ->get()que par injection automatique en cascade).
2.4 Préparation des mappings
Comme nous pouvons le voir, la méthode ->addDefinitions( [...] ) du builder accepte un tableau dont les clés sont des noms d’interfaces, et les valeurs appellent des fonctions helpers. Dans cet exemple, nous utilisons le helperobject, qui indique le nom de la classe concrète à utiliser.
PHP-DI offre plusieurs possibilités de mapping, via différents helpers, afin de :
- produire un singleton ;
- créer une nouvelle instance chaque fois, non stockée dans le conteneur ;
- réaliser un simple alias vers une autre définition ;
- utiliser une variable d’environnement ;
- fournir une factory pour créer un objet qui nécessite des initialisations post-construction ;
- exécuter tout callable PHP (comme les fonctions anonymes et classes invocables) ;
- et encore d’autres formes de paramétrages. La documentation du projet fournit tous les détails sur ces techniques.
En réalité, les noms des clés ne sont pas obligatoirement des noms d’interfaces : le conteneur de PHP-DI implémente Interop\Container\ContainerInterface et, à ce titre, on peut y stocker tout et n’importe quoi. Ainsi que nous le verrons plus loin, nous utiliserons ce même conteneur pour stocker des dépendances scalaires, de configuration.
Toutefois, s’il est nécessaire d’expliciter l’implémentation de chaque interface, les déclarations telles que celle-ci :
MyConcreteClass::class => DI\object(MyConcreteClass::class)
… sont inutiles, car c’est le comportement par défaut.
En d’autres termes, si PHP-DI doit produire une dépendance qui est une simple classe instanciable par un constructeur par défaut, alors c’est ce qu’il fera, sous la forme d’un singleton. C’était bien ce que faisait notre première mouture du code Application, lorsque nous tirions du conteneur :
$sessMgr=$this->container->get(DbSessionMgr::class);
Et dès lors que nous avons introduit l’interface SessionManager, il a fallu instruire PHP-DI quant à l’implémentation à utiliser.
Notons au passage que les fonctions anonymes activent, elles aussi, la résolution en cascade : si les paramètres sont correctement qualifiés par type-hint, PHP-DI les injecte automatiquement. Nous trouvons cet exemple sur la page du projet :
[
'LoggerInterface' => DI\object('MyLogger'),
'Foo'=>function(LoggerInterface$logger){
return new Foo($logger);
},
]
Quoi qu’il en soit, il est aisé de charger un tableau particulier de mappings plutôt qu’un autre en fonction du contexte à activer. Ceci est un aspect très important, puisqu’il permet d’externaliser complètement la configuration du conteneur d’inversion de contrôle. Certaines parties du tableau PHP peuvent être importées (par un include) et ainsi couvrir autant d’environnements différents que souhaité.
Par exemple, le code suivant est possible :
// Configurer le conteneur : importer un tableau externe
$builder->addDefinitions(
include(getenv(‘DEPENDENCY_CONFIG_FILE’))
);
Ici la variable d’environnement DEPENDENCY_CONFIG_FILE contient le chemin d’un script PHP retournant le tableau des définitions :
return [
SessionManager::class => DI\object(DbSessionMgr::class),
// d’autres définitions ici...
];
On renseignera bien sûr dans cette variable d’environnement, un chemin de fichier différent sur la station du développeur et sur le serveur de production ; et dans ces fichiers, des directives et valeurs différentes sur les différents environnements.
En outre, pour optimiser les performances de la résolution de dépendances (qu’il n’y a pas lieu de répéter à chaque requête, une fois l’environnement fixé), la bibliothèque offre des possibilités de mise en cache, que nous n’explorerons pas dans cet article.
2.5 Stratégies d’injection
Pour permettre au conteneur de construire les différentes pièces à la demande, il existe plusieurs stratégies d’injection (non mutuellement exclusives). Étudions-en quelques-unes.
2.5.1 Instanciation réflexive
La plus répandue, car la moins intrusive, est la stratégie d’injection par arguments de constructeur. Si une classe A définit son constructeur ainsi :
public __construct (B $b, C $c,...)
Alors PHP-DI analyse les arguments par réflexion, puis construit (ou tire) chaque paramètre en fonction de son type (et selon les définitions de mapping qui le caractérisent).
On obtiendra ainsi classiquement du code tel que :
class ProductManager {
private $productRepository;
public __construct(ProductRepoInterface $pr) {
$this->productRepository = $pr;
}
}
Cette stratégie présente l’avantage de rendre extrêmement limpide l’identification des dépendances d’une classe, par simple lecture de la signature de son constructeur. En outre, aucune modification du code de la classe n’est nécessaire pour le faire fonctionner avec PHP-DI.
L’inconvénient principal est que tous les composants doivent être présents lors de la construction de l’instance. Si la classe peut s’utiliser selon plusieurs scénarios, chacun ayant des besoins différents (par exemple, une méthode a besoin de la dépendance $a et une autre a besoin de $b), alors il est inutile d’injecter toutes les dépendances dès la construction.
Tout au moins, pour les dépendances coûteuses à construire (comme les connexions à des ressources réseau, etc.) il existe une possibilité de réaliser une injection lazy, c’est-à-dire ne pas tenter d’instancier la dépendance si elle n’existe pas déjà, mais plutôt d’injecter un représentant creux à sa place (un proxy). Le véritable objet ne sera effectivement initialisé qu’à la première utilisation. Cette option requiert l’ajout d’une autre bibliothèque (ocramius/proxy-manager) en plus du paquet PHP-DI et, naturellement, augmente un peu la complexité générale du programme et de la déclaration du mapping du conteneur.
[
SessionManager::class=> DI\object(DbSessionMgr::class)->lazy()
]
Par ailleurs, à mesure que le code augmente et que la classe nécessite plus de dépendances, la signature du constructeur peut vite devenir interminable. Mais attention, ceci est généralement le signal que votre classe est mal conçue et que les rôles qu’elle joue devraient peut-être s’étaler sur plusieurs structures.
Quoi qu’il en soit, dans la majorité des cas, c’est bien cette stratégie d’injection qu’il faut privilégier.
2.5.2 Annotations
Il est possible (mais pas toujours conseillé) d’indiquer les dépendances à injecter automatiquement, sous la forme de propriétés annotées. Il faut pour cela ajouter au projet la bibliothèque doctrine/annotations, et configurer le conteneur comme ceci :
$containerBuilder->useAnnotations(true);
En effet, l’option n’est pas activée par défaut.
Une fois ceci fait, on ajoute à chaque propriété concernée l’annotation @Inject, comme dans cet exemple :
class MailingListService {
/**
* @var MailServerInterface
* @Inject
*/
private $mailServer;
...
}
Cette approche semblera très familière aux habitués du Java et du framework Spring en particulier ; mais dans le cas du PHP elle n’est pas idéale, pour les raisons suivantes :
- L’étude des annotations est assez coûteuse pour l’interpréteur : la réflexion permet au moteur d’extraire les commentaires qui précèdent un bloc de code, mais l’analyse de ce que le commentaire contient est intégralement la responsabilité userland (c’est le rôle de la bibliothèque doctrine/annotations susmentionnée).
- En cas d’utilisation de l’opcache [3], les commentaires risquent d’être ignorés, en fonction de la configuration locale de l’interpréteur. Si le code de l’application compte sur les annotations pour fonctionner, cela pourrait causer des problèmes inattendus sur certains environnements.
- La classe qui reçoit des propriétés injectées ne peut fonctionner que dans le cadre du framework PHP-DI (qui interprète l’annotation @Inject).
Aussi, si cette technique permet d’éviter la très longue définition du constructeur d’une classe à laquelle de nombreuses dépendances doivent être branchées, elle est à éviter, au profit de la méthode que nous allons détailler plus bas.
2.5.3 Passation de conteneur
Nous présentons ici une solution simple pour offrir le chargement paresseux tout en résolvant l’inconvénient des constructeurs à rallonge. Il s’agit de fournir au constructeur une unique dépendance, qui est le conteneur lui-même.
En effet, PHP-DI est précâblé pour s’auto-injecter en lieu et place de l’interface Interop\Container\ContainerInterface.
class MailingListService {
protected $container;
publicfunction __construct(ContainerInterface$container){
$this->container = $container;
}
...
Le bénéfice de capturer une référence vers le conteneur, comme attribut de l’instance, est qu’il devient possible d’en puiser manuellement les autres dépendances, à la demande :
protected $userRepository = null;
protected function getUserRepository() {
return $this->userRepository ?:
$this->userRepository = $this->container->get(UserRepositoryInterface::class);
}
Par la suite, le code de la classe n’aura qu’à appeler sa fonction getUserRepository() pour faire référence à la dépendance concernée, qui sera alors automatiquement résolue si et quand nécessaire.
L’inconvénient de cette astuce est qu’il devient moins facile d’identifier à l’œil nu les dépendances possibles d’une classe donnée. Il faut donc veiller à la documenter convenablement, ou tout au moins à regrouper les getter de ces dépendances. Par ailleurs, la classe est, là aussi, écrite en s’appuyant explicitement sur l’existence d’un conteneur. Le fait d’utiliser son interface permet de ne pas augmenter inutilement l’adhérence, mais la méthode n’est pas aussi élégante quel’instanciation réflexive, car elle avoisine dangereusement l’anti-patron du Service Locator.
2.6 Injection de configuration
Nous savons désormais injecter des instances de classes dans d’autres classes, et ceci en cascade, afin de supprimer l’adhérence entre les divers constituants de l’application. Il nous reste maintenant à clarifier par quel moyen on branche les valeurs scalaires de façon configurable.
En effet, il ne suffit pas de pouvoir aiguiller les différentes saveurs d’implémentation en fonction de l’environnement : il faut également préciser, par exemple, les coordonnées de connexion aux bases de données, et d’autres paramètres textuels ou numériques.
Nous avons rencontré plus haut un exemple d’usage d’un PDO injecté automatiquement au constructeur de l’objet DbSessionMgr. Voyons comment paramétrer cette instance dans les définitions de notre builder. Tout d’abord, il est important de comprendre qu’il est possible (et même conseillé) d’effectuer plusieurs appels successifs à $containerBuilder->addDefinitions(...) avant l’instruction $containerBuilder->build(). En effet, les définitions s’accumulent, et l’on peut ainsi les découper en autant de fichiers que souhaité. Par exemple :
$containerBuilder->addDefinitions( include ‘database-def.php’ );
$containerBuilder->addDefinitions( include ‘mail-def.php’ );
Et ainsi de suite, où chaque fichier inclus ici retourne un tableau de définitions.
Ainsi, nous allons pouvoir injecter des valeurs scalaires de cette façon :
return [
‘pdo.dsn’ => ‘mysql:host=dbhost’,
‘pdo.user’ => ‘bob’,
‘pdo.password’ => ‘secret’
];
Ce que nous utiliserons pour construire le PDO :
return [
PDO::class => DI\object()->constructor(
DI\get(‘pdo.dsn’), DI\get(‘pdo.user’), DI\get(‘pdo.password’)
)
];
Nous avons utilisé ici la forme courte de DI\object(), c’est-à-dire sans argument, car il ne s’agit pas d’un mapping interface => implémentation, et donc PHP-DI considère que le nom de la clé (PDO::class, attention sans les guillemets !) est également directement le nom de la classe à instancier. Mais, plus important, nous avons indiqué une série d’arguments que PHP-DI devra passer au constructeur de ladite classe au moment où il devra résoudre la dépendance. Et pour résoudre la valeur de ces arguments, nous utilisons le helperDI\get qui permet de tirer la valeur de la clé spécifiée au moment de la résolution plutôt qu’au moment de la définition.
Finalement, la définition du mapping PDO reste collective (et peut être stockée en source-control par exemple), tandis que le petit fichier de définition des paramètres de connexions doit être privé et modifié selon l’environnement. Mieux : il peut lui aussi faire partie du projet (car sa structure est importante), mais les valeurs scalaires peuvent provenir de variables d’environnement par exemple.
Définition : Les annotations
Les annotations sont des directives paramétrables dont le nom commence par un @. Elles servent à apporter facilement des fonctionnalités complémentaires à du code, tout en maintenant ce dernier léger et lisible. Les annotations se placent devant les déclarations de classes, méthodes, propriétés ou autres structures. Dans un langage tel que Java, elles sont compilées au même titre que le code source, et le complète pendant l’étape de compilation en ajoutant du comportement au programme. En PHP toutefois, les annotations prennent en réalité la forme de commentaires DocBlocks, et ne sont donc interprétées qu’au moyen de l’analyse réflexive des blocs de commentaires, lorsque c’est possible.
Conclusion
En confiant le contrôle à un framework d’injection de dépendances, les implémentations de toutes les pièces peuvent devenir interchangeables sur simple changement déclaratif des définitions de l’injecteur. Ainsi, en jouant sur le bloc de définitions (ici le tableau importé par le builder) on pourra distinguer, par exemple, un environnement de production en autoscaling avec cache distribué Redis, puis un environnement de préproduction sur machine dédiée avec cache local Memcache, et enfin un simple poste de travail où la gestion du cache serait tout bonnement débrayée.
Le code source gagne également en lisibilité, puisqu’il s’allège de toute la tuyauterie visant à brancher les objets les uns sur les autres, et qui n’a pas de valeur ajoutée fonctionnelle.
Nous n’avons pu aborder qu’une partie de ce que permet cette puissante bibliothèque PHP-DI. Elle ne rivalise certes pas avec les solutions qui se rencontrent dans l’écosystème Java, en raison tout simplement des limites du langage PHP dans le domaine de la métaprogrammation. Mais nous avons là de quoi changer radicalement l’approche des projets PHP et la disposition du code source. Les applications en deviennent plus élégantes, professionnelles, agréables à écrire et faciles à déployer.
Références
[1]Patrons de conception : https://fr.wikipedia.org/wiki/Patron_de_conception
[2]Site du projet PHP-DI : http://php-di.org/
[3]OpCache et commentaires : http://php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments