Introduction à l'injection de dépendances en PHP

Formation

En Ligne

Prix sur demande

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Description

  • Typologie

    Formation

  • Méthodologie

    En ligne

Grâce à cette formation vous pourrez acquérir les connaissances nécessaires qui vous permettrons d’ajouter des compétences à votre profil et obtenir de solides aptitude qui vous offriront de nombreuses opportunités professionnelles.

Questions / Réponses

Ajoutez votre question

Nos conseillers et autres utilisateurs pourront vous répondre

À qui souhaitez-vous addresser votre question?

Saisissez vos coordonnées pour recevoir une réponse

Nous ne publierons que votre nom et votre question

Les Avis

Les matières

  • Dépendances

Le programme

Introduction du cours

Bonjour à tous,

Ce tutoriel vise à vous faire découvrir un modèle de gestion du cycle de vie des objets parfois méconnu des développeurs, particulièrement en PHP. Après une présentation du concept d'injection de dépendances, deux bibliothèques écrites en PHP seront présentées afin de permettre une utilisation facile dans vos scripts.

Le pré-requis de ce tutoriel est une bonne connaissance de PHP et une maîtrise de la programmation orientée objet (introduite dans le tutoriel officiel PHP et approfondie dans ce tutoriel). Une curiosité et une ouverture d'esprit sont aussi nécessaires pour aller au bout du tutoriel et comprendre l'intérêt du patron de conception que je vais vous présenter.

Notez que si les codes sont tous écrits en PHP, la théorie est facilement transposable dans d'autres langages. Les codes sont compréhensibles pour toute personne maîtrisant la POO dans n'importe quel langage (Java, C++, etc.).

Qu'est-ce que l'injection de dépendances ?

Avant toute chose, il faut savoir que l'injection de dépendances s'inscrit dans un contexte de programmation orientée objet (POO pour les intimes) et que par conséquent le concept n'est que de peu d'utilité dans un langage ne permettant pas la POO ou bien si vous n'exploitez pas ses possibilités.

Présentation du problème

Comme tout design pattern qui se respecte, l'injection de dépendances (en anglais dependency injection, abrégé DI) est une réponse à un problème. Afin de le présenter, je vais prendre un exemple d'utilisation concret.

L'idée de départ est d'offrir une persistance des données entre deux pages chargées par un même utilisateur ; on a pour cela deux solutions dans toute application web : utiliser des cookies ou les sessions. Dans un contexte objet, vous serez amenés à écrire des classes afin de gérer cette persistance. La classe la plus naturelle est une classe de stockage fournissant un code objet pour manipuler les sessions PHP.

<?php class SessionStorage { function __construct($cookieName = 'PHP_SESS_ID') { session_name($cookieName); session_start(); } function set($key, $value) { $_SESSION[$key] = $value; } function get($key) { return $_SESSION[$key]; } // ... }

Cependant il n'est pas très confortable de manipuler cet objet directement et vous préférerez utiliser un objet plus haut niveau représentant un utilisateur.

<?php class User { protected $storage; function __construct() { $this->storage = new SessionStorage(); } function setAttribute($name, $value) { $this->storage->set($name, $value); } function getAttribute($name) { return $this->storage->get($name); } // ... }

Cela fonctionne bien mais manque cruellement de flexibilité. Par exemple, si vous voulez changer le nom du cookie contenant l'id de la session, comment faites-vous ?

Solution 1 : coder la valeur en dur

La première solution qui viendra est de coder la valeur en dur dans le constructeur de la classe User.

<?php class User { function __construct() { $this->storage = new SessionStorage('SESSION_ID'); } // ... }

On voit tout de suite que si on veut changer cette valeur à l'avenir, cela nécessite de changer le code. Vu que la classe SessionStorage accepte la valeur en paramètre dans son constructeur, c'est donc qu'elle est capable de s'adapter quel que soit le nom du cookie. Il est donc dommage que la classe User ne puisse pas être configurée plus facilement.

Solution 2 : utiliser une constante

L'avantage par rapport à la situation précédente est que la constante est définie en dehors de la classe et donc qu'il est toujours possible de centraliser la configuration dans un seul fichier définissant toutes les constantes de configuration.

<?php class User { function __construct() { $this->storage = new SessionStorage(STORAGE_SESSION_NAME); } // ... } define('STORAGE_SESSION_NAME', 'SESSION_ID');

Cependant la solution n'est toujours pas idéale vu que la classe dépend maintenant d'une constante pour fonctionner correctement. Même bien documentée, cette utilisation est rapidement source d'erreurs.

Solution 3 : ajouter un argument au constructeur

De même que la classe de gestion de session a un argument à son constructeur, on peut faire de même pour l'utilisateur.

<?php class User { function __construct($sessionName) { $this->storage = new SessionStorage($sessionName); } // ... } $user = new User('SESSION_ID');

Une fois de plus ce n'est pas optimal puisqu'on encombre le constructeur avec des options n'ayant rien à voir avec la classe elle-même.

Une solution

Vous aurez aussi peut-être remarqué un autre problème : comment changer facilement le stockage des sessions ? Même si utiliser les sessions natives de PHP est souvent la meilleure solution, surtout pour de petits sites, il y a des cas où d'autres solutions doivent être envisagées (stockage des données persistantes en base de données par exemple). Dans ce dernier cas, une classe DatabaseStorage sera écrite, mais il faudra alors une fois de plus changer le code du constructeur de User... Pas terrible.

Une solution (il y en a sans doute d'autres, mais c'est celle qui est exploitée dans le design pattern que je souhaite vous présenter) est de passer l'instance de la classe de stockage en argument au constructeur de la classe de l'utilisateur (on dit qu'on injecte une classe dans une autre).

<?php class User { function __construct($storage) { $this->storage = $storage; } // ... } $storage = new SessionStorage('SESSION_ID'); $user = new User($storage);

Si vous découvrez ce patron de conception, il y a des chances que vous soyez déçus comme je l'ai été la première fois que j'ai vu l'explication. Cependant malgré le fait que l'idée de base soit triviale (vous y aviez peut-être pensé avant que je ne dévoile la solution), sa mise en œuvre devient vite très complexe, surtout dans un environnement orienté objet dense. La création d'un objet devient alors rapidement un calvaire lorsque vous devez auparavant créer cinq autres classes, elles-mêmes dépendant d'autres classes, etc. De plus, vous aurez remarqué que vous n'y gagnez pas nécessairement en facilité de configuration si vous exploitez ce modèle tel quel. C'est tout l'objet des parties qui vont suivre.

Utilisation d'un conteneur de services

En réalité, la vraie force de l'injection de dépendances vient de l'écosystème qui l'accompagne. Chaque langage fournit des bibliothèques permettant de l'utiliser très facilement. Dans un premier temps, nous allons voir comment utiliser concrètement ce que nous avons vu dans la partie précédente avec un simple code PHP (sans bibliothèque).

Notre premier conteneur

Cela passe par la notion de conteneur (en anglais depdendency injection container, abrégé DIC). C'est un objet qui sait comment construire d'autres objets. Ces objets sont ensuite généralement appelés des services (le dénomination n'est pas spécifique au DIC). En reprenant l'exemple de la section précédente, une telle classe s'écrit très simplement.

<?php class Container { public function getUser() { return new User($this->getUserStorage()); } public function getUserStorage() { return new SessionStorage('SESSION_ID'); } } $container = new Container();

Ainsi à partir de votre objet $container, vous pouvez construire à n'importe quel moment une instance de la classe User. La configuration est centralisée au sein de cette classe, et vous pouvez aisément modifier un nom de classe ou un paramètre en modifiant cette classe.

La seule difficulté à laquelle vous pourrez être confrontés est de garder votre instance accessible partout. Pour cela plusieurs remèdes sont possibles. Le cas idéal est une bonne conception objet qui permet d'injecter le conteneur (eh oui !) dans d'autres objets (par exemple des contrôleurs). Si cela n'est pas possible (par exemple si vous utilisez beaucoup de fonctions ou méthodes statiques) une autre solution est d'implémenter un singleton sur votre conteneur.

Partager les services

Le code actuel comporte un petit désagrément : à chaque récupération de service, un nouveau service est créé. Le comportement peut être pratique dans certains cas, mais de façon générale vous préférez éviter de créer un nouvel utilisateur à chaque fois que vous voulez accéder à une valeur de session. Heureusement on corrige très facilement ce problème en n'instanciant qu'une seule fois chaque service. Plusieurs solutions sont possibles, en voici une.

<?php class Container { public function getUser() { static $instance; if (!isset($instance)) { $instance = new User($this->getUserStorage()); } return $instance; } public function getUserStorage() { static $instance; if (!isset($instance)) { $instance = new SessionStorage('SESSION_ID'); } return $instance; } }

Ainsi, le premier appel de $container->getUser() construira un utilisateur (et donc l'objet gérant le stockage des données persistantes). Les appels suivants retourneront toujours le même utilisateur, ce qui est bien le comportement souhaité. Attention à toujours n'instancier qu'un seul conteneur ! Le conteneur ne pouvant s'instancier lui-même pour des raisons évidentes, c'est au développeur d'y faire attention.

Paramétrer les services

Le paramétrage de nos services nécessite toujours de modifier le code de la classe. C'est pourquoi les DIC sont généralement paramétrables facilement. Le plus simple est de gérer une liste de paramètres qui seront accessibles dans les méthodes de construction. Ces paramètres sont typiquement passés au conteneur lors de son instanciation.

<?php class Container { protected $parameters; public function __construct(array $parameters) { $this->parameters = $parameters; } public function getUserStorage() { static $instance; if (!isset($instance)) { $instance = new SessionStorage($this->parameters['user.storage.cookie_name']); } return $instance; } //... }

Une bonne pratique (utilisée dans Symfony) pour éviter les collisions de paramètres est d'utiliser une notation à point(s) afin de repérer facilement à quel service se rattache chaque paramètre. Pour faire encore plus fort (et c'est là que le DIC révèle tout son potentiel !), il est possible de paramétrer de même le nom de la classe :

<?php class Container { protected $parameters; public function getUserStorage() { static $instance; if (!isset($instance)) { $className = $this->parameters['user.storage.class']; $instance = new $className($this->parameters['user.storage.cookie_name']); } return $instance; } //... }

C'est particulièrement utile dans le cas présenté, afin de passer facilement d'un stockage en session à un stockage en base de données par exemple. Deux remarques :

  • il est plus confortable de gérer des valeurs par défaut pour les paramètres : dans la majorité des cas, la classe SessionStorage sera utilisée et ne pas devoir systématiquement la nommer explicitement est un plus ;

  • l'utilisation des interfaces montre alors tout son intérêt quand on utilise un DIC. Cela permet de ne pas se soucier des implémentations mais juste des méthodes disponibles dans tout code utilisant des objets extraits d'un conteneur.

On pourrait évidemment appliquer le même traitement à la méthode Container::getUser(). Même si dans le cas d'un utilisateur, spécifier la classe pourrait à première vue présenter moins d'intérêt, cela reste très utile dans un contexte d'héritage. Symfony utilise par exemple abusivement ce principe afin de permettre de personnaliser chaque classe utilisée dans le framework. Il suffit alors de créer une classe personnalisée héritant de la classe de base, de changer un paramètre (tout est paramétré) et le tour est joué !

Visibilité des services

Certains services n'ont pas besoin d'être accessibles depuis n'importe quel morceau de code. Par exemple ici, notre service user.storage est uniquement destiné à être injecté dans notre service d'utilisateur et n'a pas vocation à être manipulé seul. Lui mettre une visibilité non publique est alors une bonne idée afin de prévenir une maladresse d'un développeur.

Notez que cela ne l'empêche pas d'être réutilisé pour la construction d'autres classes. Avec notre conteneur actuel, il suffit de remplacer le mot clé public par protected ou private.

Récapitulatif

Voici le code complet à la fin de cette partie :

<?php class Container { protected $parameters; public function __construct(array $parameters) { $this->parameters = $parameters; } public function getUser() { static $instance; if (!isset($instance)) { $className = $this->parameters['user.class']; $instance = new $className($this->getUserStorage()); } return $instance; } protected function getUserStorage() { static $instance; if (!isset($instance)) { $className = $this->parameters['user.storage.class']; $instance = new $className($this->parameters['user.storage.cookie_name']); } return $instance; } } Pimple, un conteneur minimaliste

Le code que nous avons écrit précédemment marche correctement mais vous aurez constaté qu'il est très répétitif et qu'au delà de quelques classes (une dizaine) le maintenir peut rapidement devenir cauchemardesque. Nous allons donc découvrir rapidement quelques bibliothèques PHP permettant de gérer ses dépendances facilement. Si vous n'appréciez ni le framework symfony/Symfony ni son créateur Fabien Potencier, je vous conseille de passer immédiatement votre chemin : toutes les bibliothèques qui suivent proviennent soit de l'un, soit de l'autre !

À l'origine de Pimple : Twittee

Pour l'anecdote, Pimple ressemble quelque peu à Twittee. Twittee est un défi (délire ?) consistant à faire tenir un DIC dans un tweet (140 caractères). Je vous laisse admirer le résultat (PHP 5.3 ou plus requis) :

<?php class Container { protected $s = array(); function __set($k, $c) { $this->s[$k] = $c; } function __get($k) { return $this->s[$k]($this); } }

Vous aurez remarqué que ce code ne fait au final pas grand chose de plus qu'un simple tableau en PHP. Mais strictement parlant, c'est bien un DIC ! Son utilisation exploite ensuite les fonctions anonymes de PHP, comme suit.

<?php $c = new Container(); $c->user_storage_name = 'SESSION_ID'; $c->user_storage = function($c) { return new SessionStorage($c->user_storage_name); }; $c->user = function($c) { return new User($c->user_storage); };

Si vous voulez en savoir plus, Twittee a même son site web (tout aussi minimaliste que le script).

PS : si certains ont encore des doutes, non Twittee n'a pas vocation à être utilisé réellement !

Introduction à Pimple

La parenthèse culturelle étant faite, laissez-moi vous présenter Pimple. Bien qu'il comporte heureusement quelques fonctionnalités en plus par rapport à Twittee, sa conception a aussi été guidée par la simplicité. Contrairement à son petit frère, il est utilisable en pratique (le micro-framework Silex le fait). Pour télécharger Pimple, une adresse : son site officiel. Vous pouvez aussi visiter directement son dépôt GitHub.

Appeler le centre

Avez-vous besoin d'un coach de formation?

Il vous aidera à comparer différents cours et à trouver la solution la plus abordable.

Introduction à l'injection de dépendances en PHP

Prix sur demande