• Ce blog — désormais archivé — est en lecture seule.

Implémentation sans prétention : injection de dépendances à la sauce Spring en PHP

En Java, ceux qui ont déjà fait un peu de web avec doivent connaître le framework Spring, un conteneur léger, MVC multi-couches, … L’une de ses particularités est de se configurer avec des fichiers XML.

En PHP nous n’avons pas ce genre de fonctionnalité. Il serait bon de pouvoir décrire les dépendances entre ses objets dans un fichier et d’appeler uniquement le premier objet dont nous nous servons. Ensuite, le reste serait fait en interne, sans que nous soyons obligés de créer les objets à la main. C’est le propos de cet article.

 

Pour une fois je vais partir de la fin, de ce que l’on obtient au final :

<?php

namespace core\engine;

use core\model\orm\wOrm;
use core\dependence\wService;
use core\dependence\wDependenceBuilder;

/**
* class wEngine
* When you start it, it sets all the services and calls the dispatcher.
*
* @author William DURAND <william.durand1@gmail.com>
*/

class wEngine extends wDependenceBuilder
{
  /**
  * @param string $appName Application name
  */

  public function start($appName)
  {
     $this->parseDependencies('conf/dependencies.xml');

    /* Just gives the datasource to the ORM */
    wOrm::setDataSource($this->getService('db'));

    /* Let's work */
    $this->getService('dispatcher')->dispatch($appName);
  }
}

?>

 

Cette classe représente plus ou moins un bootstrap. Ce qui nous intéresse ici ce sont les trois lignes à l’intérieur de la méthode start().

La première va parser le fichier de dépendances. Il se présente ainsi :

<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
  <service id="db" class="core\model\db\wDb">
    <service id="pdo" class="core\model\pdo\wPdo">
      <parameter>mysql:host=localhost;dbname=test</parameter>
      <parameter>root</parameter>
      <parameter>root</parameter>
    </service>
  </service>
  <service id="dispatcher" class="core\dispatcher\wDispatcher">
    <service id="request" class="core\view\request\wRequest" />
    <service id="response" class="core\view\response\wResponse" />
  </service>
</dependencies>

 

On ne parle plus d’objets mais de services, ce qui permet de rendre un peu plus générique notre implémentation. On se moque du nom de la classe d’une connexion à la base de données, ce que l’on veut ce sont ses services.

Parser ce fichier de description c’est appeler la méthode parseDependencies() :

/**
* @param string $filename
*/

public function parseDependencies($filename)
{
  $xml = simplexml_load_file($filename);

  foreach($xml->service as $service)
  {
    $this->makeDependency($service);
  }
}

 

Elle va parcourir chaque service pour créer les dépendances :

/**
* @param SimpleXMLElement $serviceNode
* @return A service
*/

private function makeDependency($serviceNode)
{
  $dependencies = array();

  foreach($serviceNode->service as $service)
  {
    array_push($dependencies, $this->makeDependency($service));
  }

  if(isset($serviceNode->parameter))
  {
    foreach($serviceNode->parameter as $param)
    {
      array_push($dependencies, (string) $param);
    }
  }

  $service = (string) $serviceNode['id'];
  $class = (string) $serviceNode['class'];

  $this->setService(new wService($service, $class, $dependencies));

  return $this->getService($service);
}

 

Pour définir un service, on appelle la méthode setService() :

/**
* @param $service
* @param $object
*/

public function setService($service, $object = null)
{
  if($service instanceof wService)
  {
    parent::setService($service->getName(), $this->makeObject($service));
  }
  else
  {
    parent::setService($service, $object);
  }
}

 

La méthode makeObject() va créer (par réflection) un l’objet correspondant au service s’il n’existe pas déjà dans ce que nous appelerons un conteneur de dépendances :

/**
* @param Service $service
* @return object
*/

private function makeObject($service)
{
  $class = $service->getClass();
  $dependencies = $service->getDependencies();

  if(count($dependencies) == 0)
  {
    return new $class;
  }

  $arg = array();
  foreach($dependencies as $dependence)
  {
    if($dependence instanceof wService)
    {
      if(!$this->hasService($dependence->getName()))
      {
        $this->setService($dependence->getName(), $this->makeObject($dependence));
      }

      $args[] = $this->getService($dependence->getName());
    }
    else
    {
      $args[] = $dependence;
    }
  }

  $rc = new \ReflectionClass($class);

  return $rc->newInstanceArgs($args);
}

 

Ces méthodes font parties de la classe wDependenceBuilder. J’ai introduit quelques lignes au-dessus la notion de conteneur de dépendances. C’est la classe wDependenceContainer qui joue ce rôle et wDependenceBuilder en hérite.

Ce conteneur possède un tableau PHP qui va contenir les objets correspondants aux services ainsi que trois méthodes : accesseur en lecture, en modification et test de présence :

<?php

namespace core\dependence;

/**
* @abstract wDependenceContainer
*
* @author William DURAND <william.durand1@gmail.com>
*/

abstract class wDependenceContainer
{
  /**
  * Container
  * @var array
  */

  protected $_container = array();

  /**
  * @param $serviceName A service name
  * @return object The service object identified by $serviceName
  */

  public function getService($serviceName)
  {
    return $this->_container[$serviceName];
  }

  /**
  * @param string $serviceName A service name
  * @param object $serviceObject A service object
  */

  public function setService($serviceName, $serviceObject)
  {
    $this->_container[$serviceName] = $serviceObject;
  }

  /**
  * @param string $serviceName
  * @return boolean
  */

  public function hasService($serviceName)
  {
    return in_array($serviceName, $this->_container);
  }
}

?>

 

Pour terminer, un service est décrit comme suit : un nom, une classe et des dépendances.

<?php

namespace core\dependence;

/**
* wService
*
* @author William DURAND <william.durand1@gmail.com>
*/

class wService
{
  /**
  * @var string
  */

  private $_name;
  /**
  * @var string
  */

  private $_class;
  /**
  * @var array
  */

  private $_dependencies;

  /**
  *
  * @param string $name
  * @param string $class
  * @param array $dependencies
  */

  public function __construct($name, $class, $dependencies = array())
  {
    $this->_name = $name;
    $this->_class = $class;
    $this->_dependencies = $dependencies;
  }

  /**
  * @return string
  */

  public function getName()
  {
    return $this->_name;
  }

  /**
  * @return string
  */

  public function getClass()
  {
    return $this->_class;
  }

  /**
  * @return array
  */

  public function getDependencies()
  {
  return $this->_dependencies;
  }
}

?>

 

Pour revenir à la première classe donnée, une fois la méthode parseDependencies() est faite, il suffit d’appeler nos services par leurs noms.

$this->getService('dispatcher')->dispatch($appName);

Ici on appelle le dispatcher de l’application.

 

On peut très bien décrire nos services en PHP uniquement. Par exemple :

$this->setService(new wService('db', 'core\model\db\wDb',
  array(
    new wService('pdo', 'core\model\pdo\wPdo',
      array('mysql:host=localhost;dbname=test', 'root', 'root')))));
$this->setService(new wService('dispatcher', 'core\dispatcher\wDispatcher',
  array(
    new wService('request', 'core\view\request\wRequest'),
    new wService('response', 'core\view\response\wResponse')
  )));

C’est l’équivalent du fichier XML précédent.

Ainsi une optimisation possible est d’ajouter un cache PHP après avoir parsé le fichier XML car cette opération reste coûteuse.

 

Voilà, un passage rapide sur l’injection de dépendances.

Merci à Fabien Potencier pour ses articles sur le sujet.

  • Print
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • FriendFeed
  • LinkedIn
  • MySpace
  • Netvibes
  • PDF
  • Ping.fm
  • RSS
  • Technorati
  • viadeo FR
  • Wikio
  • Yahoo! Buzz

Related Posts

Cet article a été publié dans Ancien blog avec les mots-clefs : , , , , , . Bookmarker le permalien. Les commentaires et les trackbacks sont fermés.