Revenir
en haut

IOC – Injection de dépendances avec Zend_Application

13/10/2010 6

Introduction

Mais pourquoi je parle de ça ? Je suis bien conscient que la littérature sur le sujet est dense, mais comme je le répéterai très souvent, PHP est en retard, non pas à cause du langage, qui est aujourd’hui abouti, mais à cause de la réputation et des casseroles qu’il traîne depuis sa version 4. Résultat, la communauté autour du langage n’a pas pris l’habitude de s’intéresser à des concepts pourtant adaptés à beaucoup de développements web, et oui, même en script.

Aujourd’hui la POO et les performances sont au rendez-vous, et plus rien ne nous retiens (ok j’exagère un poil) d’appliquer des mécanismes simples et puissants, comme, devinez quoi, l’injection de dépendances.

Les conteneurs légers de dépendances sont très utilisés dans d’autres « mondes », comme JAVA, et permet d’assurer une bonne cohésion des différents composants d’une application. Je peux citer par exemple le Framework Spring ou le conteneur PicoContainer qui sont très connus. Ces conteneurs s’articulent autour du concept d’Inversion de Contrôle (IoC). L’inversion de contrôle est une expression très générique pour définir le concept général qui est utilisé dans d’autres aspects de la programmation et depuis bien longtemps. Ici, j’aborde une forme spécifique d’IOC qui a été baptisée « Injection de dépendances ».

L’injection de dépendances permet de découpler les différents composants de l’application en supprimant la dépendance d’un composant avec l’implémentation d’un autre (ou de plusieurs autres). Pour faire simple, elle permet de manipuler les différents composants d’une application comme des plugins et si vous pouvez voir vos classes de cette manière, alors le mécanisme prend tout son sens.

Mais le concept à aussi d’autres avantages (comment des inconvénients :p), notamment pour faciliter la mise en place de certains tests unitaires.

Il existe bien sûr (comme toujours) d’autres moyens de supprimer les dépendances, comme le « Service Locator » mais ce n’est pas ce qui nous intéresse dans ce billet.

Je voulais faire court, mais comme vous pouvez le constater, c’est loupé :) Je vais commencer par rappeler quelques avantages de l’injection de dépendances, et je proposerai ensuite une implémentation avec le Zend Framework à l’aide du composant Zend_Application.

Un exemple

Pour vous aider à mieux visualiser l’idée, j’illustrerai avec un exemple très simple mais qui permet de voir rapidement les avantages d’un tel mécanisme. C’est un des rares concepts où vous ne serez pas frustrés de constater qu’avec le super exemple « HelloWorld » de la mort c’est bien joli, mais qu’en réalité c’est bien différent, et que vous êtes bien content d’avoir perdu 1h à lire un billet pour les « bisounours ». Non, ici l’application est la même quelque soit la complexité du projet :)

Imaginons un composant ou un service (j’expliquerai dans un autre billet la notion de Service) qui permet de manipuler des utilisateurs inscrits sur un site web. Un des cas d’utilisation sera de lister les utilisateurs inscrits et peut se traduire par (ok, jamais de cette manière, mais admettons !) :

class Service_Impl_User implements Service_User
{
    /**
     * @return ArrayObject
     */

    public function getUserList()
    {
        $users = $this->userRepository->findAll();
        return $users;
    }
}

La méthode getUserList est très simple. Elle nécessite un objet userRepository qui permet de récupérer une liste d’utilisateurs.

Cette méthode est propre et sémantiquement correcte, mais à un moment donné, il faudra bien que j’initialise l’objet userRepository. Naturellement, je fais ça dans le constructeur de mon service :

class Service_Impl_User implements Service_User
{
    private $userRepository;

    public function __construct()
    {
        $this->userRepository = new Repository_UserMySQL();
    }

    // [...]
}

C’est très facile à remarquer, j’utilise un objet userRepository très spécifique qui permet d’accéder à mes données au travers d’un SGBDR de type MySQL. Ceci me convient tant que je stocke les informations de cette manière. Mais voilà, je vous la sors la phrase fatidique, que se passera-t’il si je décide d’utiliser ce service dans un autre projet et que le système de persistance de données n’est pas MySQL, mais une série de fichiers XML; ou encore que les données sont remontées via un service web ?

Nous aurons deux problèmes. Non seulement il faudra modifier le constructeur du service, mais en plus rien ne me dit que l’objet permettant de récupérer les données se comportera de la même manière et implémentera une méthode findAll qui répond à mes besoins. Dans ce cas, je serais sans doute forcé de refactoriser aussi ma méthode getUserList.

Dans un cas réel, ce problème pourrait concerner des dizaines de services et de composants et nous nous retrouverions avec une application incapable de fonctionner dans des environnements différents.

Pour résoudre le deuxième problème et permettre à l’objet userRepository de conserver le même fonctionnement quelque soit le système de persistance de données, je peux décrire son comportement dans une interface, je peux déterminer un contrat. Toutes les implémentations de mon repository devront respecter ce contrat. De cette manière, mon service ignore totalement la manière dont sont organisées les données et n’a pas besoin de le savoir. Il les récupérera toujours sous la même forme au travers du repository.

public interface Repository_User
{
    /**
     * @return ArrayObject
     */

    public function getUserList();
}

Mon repository MySQL devra donc implémenter cette interface et ma méthode getUserList n’aura pas à se soucier de ça.

Pour résoudre l’autre problème et permettre à mon service d’être ré-utilisable dans des applications et des environnements différents, je dois extraire l’initialisation de mon objet userRepository du service. Pour ça je vais utiliser l’injection de dépendances.

Injection de dépendances

Le mécanisme de l’injection de dépendances est simple. Il consiste à créer dynamiquement et à injecter automatiquement les instances des dépendances entre les différents composants de mon application.

Pour ça, nous avons besoin d’un objet particulier, un conteneur, qui, en s’appuyant sur un fichier de configuration, va créer (pas forcément) et stocker les instances des dépendances de chaque composant, puis les injecter dans les propriétés (attributs) des classes utilisées au cours de l’exécution. Il agit comme un assembleur qui nourrit chaque classe avec les dépendances dont elle a besoin.

La configuration est décrite par le développeur et permet de générer une map des classes et leur dépendances. Les dépendances ne sont plus créées statiquement dans le code, mais sont déterminées dynamiquement au moment de l’exécution grâce aux fichiers de configuration. Il devient donc très facile d’utiliser différentes implémentations des dépendances selon les besoins.

Il existe plusieurs formes d’injection de dépendances, dont l’injection par constructeur et l’injection par mutateur (setter).

Injection par constructeur

L’injection par constructeur consiste à faire passer l’instance de la dépendance directement en paramètre du constructeur. Dans notre exemple :

class Service_Impl_User implements Service_User
{
    private $userRepository;

    public function __construct($userRepository)
    {
        $this->userRepository = $userRepository;
    }

    // [...]
}

Cette solution peut poser plusieurs problèmes.

Tout d’abord, il est impossible de décider à quel moment l’instance de userRepository est créée et elle sera en mémoire même si elle n’est pas utilisée dans la suite du processus en cours.

Ensuite, si la classe Service_User possède plusieurs dépendances, le constructeur deviendra vite trop complexe, attendra trop de paramètres, ce qui rendra le code difficile à comprendre.

Pour ma part, j’ai totalement délaissé ce type d’injection et préfère de loin l’injection par mutateur. Mais nous pouvons argumenter en faveur de l’injection par constructeur qui dans certains cas permet de converser le rôle d’initialisation du constructeur (après tout, il est là pour ça).

Injection par mutateur

L’injection par mutateur consiste à définir une méthode de mutation (un setter) qui sera utilisée par le conteneur de dépendances pour injecter l’instance du composant. Un setter doit donc être créé dans notre service :

class Service_Impl_User implements Service_User
{
    // [...]
   
    public function setUserRepository($userRepository)
    {
        $this->userRepository = $userRepository;
        return $this;
    }
}

Je n’ai plus besoin de mon constructeur.

Grâce à ça, j’ai réussi à découpler totalement mon service et mon repository, et je peux transporter mon service d’une application à une autre sans toucher une seule ligne de code à l’intérieur.

Attention, tout ce qui suit repose uniquement sur cette méthode d’injection.

Polymorphisme

Pour moi, la solution est incomplète.

Dans des langages fortement typés comme JAVA, le polymorphisme est indispensable pour mettre en place l’injection de dépendances parce que nous devons être capable d’injecter n’importe quelle implémentation d’une classe par le même mutateur sans changer le code du service.

Pour ça, j’utilise mon interface, et dans mon mutateur (setter) ma classe attend un objet du type de l’interface. Grâce à ça, le conteneur de dépendances peut injecter l’implémentation qu’il veut du moment que la classe implémente cette interface.

En PHP, le polymorphisme a moins de sens parce que le langage est faiblement typé et nous ne sommes pas obligés de spécifier le type d’objet que le setter attend. Mais le concept reste quand même très utile (indispensable ?), d’une part pour conserver la suggestion automatique de nos chers IDE, mais aussi, et j’insiste, pour assurer l’interopérabilité du système et améliorer la compréhension du code.

Les interfaces sont utiles non seulement pour appliquer ce type de polymorphisme mais sont aussi idéales pour documenter les méthodes. Inutile d’alourdir une implémentation avec des docblocks, ceci peut-être fait et abusé dans une interface. Une simple lecture d’une interface permet donc comprendre rapidement le comportement d’une classe.

Je vous conseille vivement de vous habituer à cette pratique qui est appelée « programmation par contrat ». Ça nécessite un peu de code supplémentaire mais ajoute une qualité et une pérennité indiscutables.

Mise en pratique avec Zend_Application

Dans sa version 1.8, le Zend Framework a vu venir un composant majeur, Zend_Application. Ce composant, en plus de permettre de configurer l’environnement de l’application, agit comme un conteneur de dépendances. Mieux encore, sa souplesse permet au développeur de créer son propre conteneur et de le plugger de manière très triviale.

C’est ce que je vous propose ici. Je me suis inspiré du framework Spring et de sa configuration à base de fichiers XML. J’ai donc créé un conteneur de dépendances très simple qui parse un ou plusieurs fichiers XML et construit une map des composants et leurs dépendances.

La seule contrainte imposée par Zend_Application et d’implémenter les méthodes magiques __set, __get et __isset qui lui permettent de manipuler le conteneur de manière universelle. La méthode __set permet d’ajouter une instance d’un composant au conteneur, __get de récupérer une instance, et __isset de s’assurer de l’unicité des instances.

Je les implémente donc dans ma classe que j’ai décidé d’appeler tout simplement Di :

<?php
/**
 * Tight_Di - Contains all dependencies references
 *  
 * @category Tight
 * @package Tight_Di
 * @author Benjamin Dulau
 */

class Tight_Di
{    
    /**
     * Array of instances    
     * @var array
     */

    protected $_instances = array();
   
    /**
     * Array of registered dependencies    
     * @var array
     */

    protected $_components = array();
   
    /**
     * Constructor
     * @return void
     */

    public function __construct()
    {}
   
    /**
     * Returns all components definitions array
     *
     * @return array
     */

    public function getComponents()
    {
        return $this->_components;
    }

    /**
     * Returns component definition
     *
     * @param string $key
     * @return array
     */

    public function getComponent($key) {       
        return $this->_components[$key];       
    }
   
    /**
     * Retrieves component instance.
     * Creates instance if none exists yet
     *
     * @param $key
     * @return mixed
     */

    public function getInstance($key)
    {
        $component = $this->getComponent($key);
        if (false === array_key_exists($key, $this->_instances)) {
            $this->_instances[$key] = new $component['class']();           
        }
       
        return $this->_instances[$key];
    }
   
    /**
     * Sets directly an instance into instances.
     * Used by Zend_Application on resource plugin return.   
     *
     * @param string $key
     * @param mixed $instance
     * @return Tight_Di
     */

    public function __set($key, $instance)
    {        
        $this->_instances[$key] = $instance;        
        return $this;  
    }

    /**
     * Gets the given instance
     * Used by Zend_Application on getInvokArg()
     *
     * @param string $key
     * @return mixed instance
     */

    public function __get($key)
    {      
        return $this->getInstance($key);
    }
   
    /**
     * Tests if an object exists into instances
     * Used by Zend_Application
     *
     * @param string $key
     * @return bool
     */

    public function __isset($key)
    {      
        return array_key_exists($key, $this->_instances);
    }
}

La méthode __set est invoquée par Zend_Application après l’initialisation d’une ressource, que ce soit par un plugin de ressource (Zend_Application_Resource_*), ou par une méthode d’initialisation dans le bootstrap (_initMaRessource()).

Deux paramètres sont envoyés à la méthode, la clé à utiliser pour stocker l’instance, et l’instance de l’objet retourné par le plugin de ressource ou par la méthode d’initialisation. La clé est extraite automatiquement par Zend_Application du nom du plugin de ressource ou de la méthode d’initialisation.

Exemple, dans le bootstrap:

public function _initDoctrine()
{
   // code d'initisalisation[...]
   return $manager;
}

La clé sera doctrine, si la méthode se nommait _initToto, la clé serait toto.

Pour un plugin, c’est le nom de la classe qui compte :

class Zend_Application_Resource_Mailer extends Zend_Application_Resource_ResourceAbstract
{
    // code [...]
}

La clé sera mailer.

Mon but ici est de pouvoir définir mes dépendances dans un fichier de configuration XML. En utilisant toujours notre exemple et en imaginant que j’invoque mon service depuis l’IndexController, voilà à quoi devrait ressembler la configuration :

<?xml version="1.0" encoding="UTF-8"?>
<components>
    <component id="indexController" class="IndexController">
        <property name="userService" ref="userService" />
    </component>

    <component id="userService" class="Service_Impl_User">
        <property name="userRepository" ref="userRepository" />
    </component>    

    <component id="userRepository" class="Repository_Impl_UserMySQL" />
</components>

Très facile à lire :) L’attribut id est utilisé pour identifier un composant, l’attribut class pour indiquer quelle implémentation de la classe doit être instanciée et injectée, les balises property indiquent les propriétés (attributs/membres) de la classe dans lesquelles les instances doivent être injectées (via des setters), et enfin ref indique l’identifiant du composant de référence dans la configuration.

Il est donc très facile de changer les implémentations de classe à injecter.

Je complète donc ma classe Tight_Di pour ajouter la possibilité de parser le fichier XML et de peupler mes composants :

    /**
     * Constructor - calls load method for each file
     *
     * @param array $componentFiles
     * @return void
     */

    public function __construct(array $componentFiles = array())
    {
        foreach($componentFiles as $file) {
            $this->load($file);
        }
    }

    /**
     * Loads file and builds components definitions
     *
     * @param string $componentFile
     * @return bool success
     * @throws Tight_Di_Exception if unable to load file as XML
     */

    public function load($componentFile)
    {      
        if (false === file_exists($componentFile) || false === is_file($componentFile)) {
            Throw new Tight_Di_Exception(Tight_Di_Exception::NO_SUCH_FILE . ': ' . $componentFile);            
        }
       
        $xml = null;
        if (false === ($xml = simplexml_load_file($componentFile))) {
            Throw new Tight_Di_Exception(Tight_Di_Exception::FILE_LOAD_FAILED . ': ' . $componentFile);        
        }; 

        // components
        foreach ($xml->component as $XMLElement) {
            $component = array();
           
            // id
            $id = (string)$XMLElement->attributes()->id;
            $component['class'] = (string)$XMLElement->attributes()->class;
           
            // dependencies
            $component['dependencies'] = array();
            foreach($XMLElement->property as $property) {
                $name = (string)$property->attributes()->name;
                $refComponent = (string)$property->attributes()->ref;
                               
                $component['dependencies'][$name] = $refComponent;                                       
            }
           
            $this->addComponent($id, $component);
        }
       
        return true;
    }

Je ne vais pas m’attarder sur cette partie, il suffit de lire le code pour comprendre.

Nous avons donc notre conteneur, nous savons parser un fichier de configuration XML pour créer une map des composants, et nous savons ajouter et récupérer une instance d’un composant depuis le conteneur. Il reste maintenant à plugger ce conteneur sur Zend_Application et à trouver un moyen d’injecter dynamiquement toutes les dépendances au moment de l’exécution.

Pour plugger le conteneur à Zend_Application, rien de plus simple, dans le fichier « index.php », au moment d’initialiser l’environnement de l’application :

$container = new Tight_Di(array(
    APPLICATION_PATH . '/../application/configs/ioc.xml'
));
   
$application->getBootstrap()->setContainer($container);
$application->bootstrap()
            ->run();

Facile.

Dans une application complexe, rien ne m’empêche d’éclater mes fichiers de configuration pour mon injection de dépendances, exemple :

$container = new Tight_Di(array(
    APPLICATION_PATH . '/../application/configs/ioc/controllers.xml',
    APPLICATION_PATH . '/../application/configs/ioc/services.xml',
    APPLICATION_PATH . '/../application/configs/ioc/daos.xml'
));

Dernière étape, mettre en place l’injection dynamique lors de l’exécution.

C’est une partie un peu délicate parce que l’architecture actuelle du Zend Framework n’est pas assez souple pour permettre d’injecter n’importe quel type de ressource. Par exemple, je ne peux pas injecter le contrôleur frontal qui est un singleton, je ne peux pas injecter de contrôleurs d’actions parce que ça demanderait de refactoriser la méthode de dispatch du contrôleur frontal, ce qui n’est pas forcément une bonne idée :)

La solution que j’ai retenu, et faute de mieux, est d’utiliser un helper d’action qui va intervenir au moment de l’initialisation du contrôleur d’actions et permettre d’injecter récursivement les dépendances à partir de l’instance de ce contrôleur. Cette solution n’est pas sémantiquement correcte car le but d’un helper d’action est d’ajouter des fonctionnalités et de factoriser du code commun à plusieurs contrôleurs d’actions sans avoir à étendre une classe. Alors que l’injection de dépendances devrait intervenir au moment de l’initialisation de l’application. Ce point là pourra être amélioré avec ZF2 qui devrait apporter plus de souplesse pour ce genre de choses (voir un conteneur prêt à l’emploi, aller, faut pousser!).

Le helper d’action :

<?php
/**
 * Provides IOC
 *
 * @uses       Zend_Controller_Action_Helper_Abstract
 * @category   Tight
 * @package    Tight_Controller
 * @author     Benjamin Dulau
 */

class Tight_Controller_Action_Helper_Di extends Zend_Controller_Action_Helper_Abstract
{      
    /**
     * @var bool
     */

    protected $_enabled = true;
   
    /**
     * If not disabled by the action controller:
     * Browses registered components from Tight_Di
     * and injects instances from controller to all dependencies     
     */

    public function init()
    {              
        if (false === $this->_enabled) {
            return;
        }
       
        $bootstrap = $this->_actionController->getInvokeArg('bootstrap');
        $container = $bootstrap->getContainer();       
        $components = $container->getComponents();          
       
        $module = $this->_actionController->getRequest()->getModuleName();     
        $split = explode('-', $this->_actionController->getRequest()->getControllerName());
       
        $controller = '';
        for ($i=0;$i<count($split);$i++) {
            $controller .= ucfirst($split[$i]);
        }
        $currentControllerClass = ucfirst($module) . '_' . $controller . ucfirst($this->_actionController->getRequest()->getControllerKey());
       
        foreach($components as $k => $v) {         
            if ($v['class'] == $currentControllerClass) {
                $container->inject($k, $this->_actionController);
                break;
            }      
        }  
    }      
   
    /**
     * Enables IOC
     * To be called from action controllers
     */

    public function enable()
    {      
        $this->_enabled = true;
    }
   
    /**
     * Disables IOC
     * To be called from action controllers
     */

    public function disable()
    {      
        $this->_enabled = false;
    }
}

Je ne vais pas trop m’attarder sur ce helper d’action non plus (qui peut d’ailleurs être amélioré) parce que le billet est déjà assez long :)

Rapidement, le principe est simple, je récupère mon conteneur grâce à Zend_Application, je détermine le nom de la classe du contrôleur courant et je demande à mon conteneur d’injecter de manière récursive toutes les dépendances de ce contrôleur.

Voilà à quoi doit ressembler la méthode inject de mon conteneur :

    /**
     * Injects recursively dependencies starting by $key
     *  
     * @param $key
     * @param $target
     * @return void
     */

    public function inject($key, $target)
    {      
        $component = $this->getComponent($key);
        foreach($component['dependencies'] as $property => $ref) {
            $setter = 'set' . ucfirst($property);
           
            $instance = $this->getInstance($ref);      
            $target->$setter($instance);
           
            // recursive injection for dependency dependencies
            $this->inject($ref, $instance);
        }
    }

Assez simple également. Je peux déterminer la méthode setter grâce au nom de ma propriété et l’utiliser pour injecter l’instance du composant, et ainsi de suite en cascade pour toutes les dépendances.

Une petite indication, pour pouvoir injecter des ressources initialisées par des plugins de ressource de Zend_Application, il faut impérativement les déclarer dans les fichiers de configuration XML, exemple :

<?xml version="1.0" encoding="UTF-8"?>
<components>
    <!-- Zend_Application_Resource_Mail -->
    <component id="mail" class="Zend_Mail" />

    <component id="indexController" class="IndexController">
        <property name="userService" ref="userService" />
        <property name="mailer" ref="mail" />
    </component>

    <component id="userService" class="Service_Impl_User">
        <property name="userRepository" ref="userRepository" />        
    </component>    

    <component id="userRepository" class="Repository_Impl_UserMySQL" />
</components>

Pour le composant mail, l’attribut class n’est bien évidemment pas utilisé et sert à titre d’information. Ce que je fais généralement c’est que je crée un fichier de configuration resources.xml dédié qui liste toutes les ressources utilisables dans l’application et pouvant éventuellement être injectées.

Je dois par contre bien créer un setter dans mon contrôleur. Ici setMailer parce que d’après la configuration j’injecte la dépendance dans ma propriété mailer.

Pour finir, voici à quoi vont ressembler les codes de mon contrôleur et de mon service.

<?php
class IndexController extends Zend_Controller_Action
{
    /**
     * @var Service_User
     */

    private $userService;

    public function getUserListAction()
    {
        $userList = $this->userService->getUserList();
        $this->view->userList = $userList;
    }

    public function setUserService(Service_User $userService)
    {
        $this->userService = $userService;
        return $this;
    }
}
<?php
class Service_Impl_User implements Service_User
{
    /**
     * @var Repository_User
     */

    private $userRepository;

    /**
     * @return ArrayObject
     */

    public function getUserList()
    {
        $users = $this->userRepository->findAll();
        return $users;
    }

    public function setUserRepository(Repository_User $userRepository)
    {
        $this->userRepository = $userRepository;
        return $this;
    }    
}

Propre, lisible, scalable, auto-complétion possible dans mon IDE, que demander de plus ? :)

Limitations et améliorations

A améliorer

Evidemment, cette implémentation reste assez simpliste et pourrait être largement améliorée. Pour compléter les fonctionnalités du conteneur, nous devrions par exemple ajouter la possibilité d’injecter des paramètres « standard » (String, int, array, etc.). Ceci ne demande à mon avis pas beaucoup de travail et je vous laisse cette partie :)

Concernant le helper d’action, mis à part le fait que cette tâche ne devrait pas lui être confiée, il pourrait être amélioré en ajoutant une introspection de classe à l’aide de la réflection au lieu de déterminer la classe en fonction du nom du contrôleur. Ceci permettrait notamment de régler certains problèmes liés à l’héritage, de gérer donc mieux l’injection dans les super-classes de contrôleurs, mais aussi de vérifier l’existence des propriétés (ce n’est pas très grave car PHP se charge de lacher une erreur fatale dans ce cas là). Je ne l’ai pas fait tout simplement parce qu’avant PHP 5.3 la fonction property_exists ne pouvait vérifier l’existence que d’une propriété « publique ».

Performances

Je vous vois déjà vous affoler pour les performances. Effectivement, ce système veut dire que dès qu’un contrôleur possède des dépendances, dès qu’il est utilisé, toutes les instances des dépendances sont créées et injectées en cascade même si elles ne sont pas utilisées par la suite.

Je vous répondrais que c’est un léger prix à payer et qu’en comparaison du nombre de classes que le Zend Framework doit parcourir et instancier, ça ne représente qu’un grain de sable. C’est un inconvénient que j’ai décidé d’accepter :)

Conclusion

L’injection de dépendances permet d’ajouter une souplesse, une interopérabilité et une pérennité indéniables. Ce n’est pas obligatoire, mais dans certains « gros projets » c’est plus qu’appréciable, j’oserais dire indispensable.

C’est relativement simple à mettre en place, à utiliser, et au prix d’une très légère perte de performances largement acceptable.

J’espère que vous êtes arrivés au bout de ce billet assez long, merci de m’avoir lu et n’hésitez pas à poser des questions dans les commentaires ! :)

6 commentaires :

  1. Anardil :

    Bonjour,

    J’ai découvert ton blog par le biais du forum ZF. J’utilise le Zend Framework dans ton mes projets de manière assez basique pour le moment en mettant toute la logique métier dans mes modèles qui étendent Zend_Db_Table / Zend_Db_Table_Row / Zend_Db_Table_Rowset. Donc mes controllers appellent les méthodes des modèles et ça s’arrête là.

    Depuis peu je suis en train de mettre concevoir un CMS avec le Zend_Framework. Et j’aimerais avoir un logique métier plus propre. Je me suis penché vers Doctrine 1.2, j’aime beaucoup son système de génération des modèles de base ainsi que le systeme de behavior/ Event Listener. J’avais commencé à m’y intéresser de près mais bon apparemment j’ai cru comprendre que la version Doctrine 2 était mieux conçu.

    Pour en revenir à l’injection de dépendance j’ai trouvé sur Github une source implémentant Zend_Framework / Doctrine 2 / Services et utilisant le composant de Symfony DependencyInjection. lien -> https://github.com/loicfrering/losolib

    Voila, j’aimerai savoir ce que tu penses de l’architecture proposé par Loic Frering qui « s’approche » de la tienne.

    Merci.

  2. Bonjour Anardil,

    Je m’étais intéressé au composant de Symfony au moment de mettre en place l’injection de dépendances. J’ai décidé de ne pas l’adopter parce que je ne veux pas lier mon code à un container.

    Avec la solution de Symfony, dès que tu as besoin d’une référence à un objet, tu dois le récupérer via le container :

    $logger = $container->getService('utils.logger');

    Le container va instancier toutes les dépendances nécessaires et les injecter dans le logger avant de retourner l’instance. Bien qu’efficace cette approche ne me convient pas parce que ça crée une forte dépendance avec le container. Comme avec un Registre.

    Je préfère que l’injection de dépendances se fasse « par magie » et que ce soit naturel pour le développeur. C’est à dire qu’il va créer un setter, comme il devrait le faire de toute manière, et n’a rien de plus à faire dans le code.

    Par contre, le container de Symfony a l’avantage des performances. Les dépendances ne sont créées que lorsque le développeur en a besoin et que pour l’objet invoqué. Mais comme je le dis dans l’article, c’est un prix que j’ai accepté.

    Mis à part ça, le composant de Symfony est élégant, utiliser un chemin pour accéder aux dépendances est une bonne idée. Le reproche que je lui ferai est d’avoir une configuration qui n’est pas naturel, le fichier yaml du container est difficile à lire.

    Par contre, il faut être conscient que le composant que je propose devrait être complété. Il faut ajouter le passage de paramètres standards et trouver un moyen de le faire intervenir dans le coeur de l’application (en modifiant le dispatcher). Je le ferai murir avec l’arrivée de ZF2.

    Merci pour ton commentaire.

    A+ benjamin.

    • Anardil :

      Merci pour ta réponse. Effectivement comme tu le dis il faut appeler le container pour pouvoir l’utiliser mais Loic Frering a améliorer le système d’accès au service.

      Si tu regardes bien les sources.

      <?php
      /**
       * @Service
       */

      class Scaffold_PostController extends LoSo_Zend_Controller_Action
      {
          /**
           * @var Scaffold_Service_Doctrine_PostService
           * @Inject
           */

          protected $postService;

          public function setPostService($postService)
          {
              $this->postService = $postService;
              return $this;
          }

      On constate que $this->postService est directement exploitable dans le controller, ce qui est très pratique. Qu’en penses-tu ?

    • Hello,

      Je répond un peu tard, je t’avoue ne pas avoir assez de temps pour me plonger en profondeur dans le code de Loïc.

      J’ai parcouru un peu les sources sur github et de ce que j’ai pu voir, Loïc utilise un driver d’annotations personnalisé (j’ai pas trop regardé mais j’imagine qu’il utilise le moteur d’annotations de Doctrine 2 pour ça), qui, au moment du runtime va récupérer les dépendances dans le container et les injecter par setter.

      C’est très intéressant parce qu’il est possible du coup d’utiliser toutes les fonctionnalités du container de dépendances de symfony en conservant un code propre et totalement découplé.

      C’est un mariage des avantages de ma solution et de celle de symfony.

      Une remarque par contre. Je ne suis pas fan du tout des annotations, et bien que Doctrine 2 et Symfony 2 les mettent en avant je ne pense pas les utiliser. Pour deux raisons, la première c’est que ce n’est pas natif au langage PHP et utiliser les docblocks ne me plait pas du tout. La deuxième c’est que ça devient très difficile de comprendre le code, il faut ouvrir chaque fichier pour regarder les annotations. Tout ça sans parler du « bordel » que ça ajoute dans les objets.

      Mais il ne doit pas être bien compliqué d’adapter et conserver le fonctionnement que propose Loïc avec du XML.

      A+ benjamin.

  3. Anardil :

    Après c’est juste une question de préférence en ce qui concerne les docblocks qui d’ailleurs sont utilisés par le Zend_Framework dans les composants Zend_Soap_Server ou Zend_XmlRpc_Server. De plus les définitions des services sont situés dans un fichier services.yml que l’on peut mettre dans chaque module de l’application. Pour ma part je trouve plus simple l’utilisation des fichiers YAML que XML encore une fois une question de choix.

    Je pense que si vous mettiez en collaboration vos deux implémentations ça pourrait donner quelque chose de pas mal. :)

  4. Pingback: Introduction à Symfony 2 | anonymation – blog – développement et architecture web

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

*

Tags HTML autorisés : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Tag code : [cc lang="langage"][/cc] (ex. [cc lang="php"][/cc])