mercredi 23 décembre 2009

Transactions automatiques avec Zend

Dans ce post j'explique comment rendre un service Zend transactionnel sans alourdir votre code, c'est à dire sans avoir à démarrer / commiter vos transactions manuellement.

J'entends par service une classe issue du modèle Service / DAO / modèle de données, ce modèle a été détaillé dans un précédent post : Concevoir une application Zend selon le modèle Service / DAO / Modèle de données. Dans ce post je montrais un exemple de service très simple, je propose de repartir de la classe Service_User qui correspond à un service de gestion d'utilisateurs dans une application Zend / PHP.

Voici cette classe:

// File 'application/services/Service.php'

/**
 * Service used to manage Model_Users.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Service_User implements Service_Service {

  /**
   * DAO used to manage the persistence for Model_Users entities.
   *
   * @var Dao_User
   */
   private $userDao;

  /**
   * Constructs a new Service_User.
   *
   * @return Service_User
   */
  public function __construct() {
    $this->userDao = new Dao_User();
  }

  /**
   * Saves a Model_User in the system.
   *
   * @return Model_User $user the user to save in the system.
   */
  public function save(Model_Account $user) {
    $this->userDao->saveOrUpdate($user);
  }

  /**
   * Lists all the Model_Users registered in the system.
   *
   * @return Array An array of Model_Users which represents 
   *               all the registered Model_Users.
   */
  public function listAll() {
    return $this->userDao->listAll();
  }
}

Cette classe de service comporte 3 fonctions, vous pouvez remarquer qu'aucune de ces fonctions n'utilise de code relatif aux transactions.

L'astuce que je propose pour rendre transactionnelles ces 3 fonctions est de créer un service de wrapping Service_TransactionalService qui se chargera de démarrer / commiter / rollbacker automatiquement les transactions.

Voici cette classe de wrapping.

/**
 * Service which wraps an other one to add transaction around 
 * each method of the wrapped service.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@hotmail.com)
 */
class Service_TransactionalService implements Service_Service {

  /**
   * The service to make transactional.
   *
   * @var Service
   */
  private $service;

  /**
   * The Zend Db Adapter.
   *
   * @var Zend_Db_Adapter
   */
  private $db;

  /**
   * Constructs a new Service_TransactionalService by wrapping 
   * an other one.
   *
   * @param $service the service to wrap.
   * @return Service_TransactionalService
   */
  function __construct(Service_Service $service) {
    $this->service = $service;
    $this->db = Zend_Registry::get('db');
  }

  /**
   * Wraps a service call around a transaction.
   *
   * @param String $name name of the function to call.
   * @param Array $arguments arguments to pass to the function.
   * @return Object return of the called function.
   */
  function __call($name, $arguments) {

    // -- Starts a transaction
    $this->db->beginTransaction();

    try {
   
      $ret = call_user_func_array(
                 array($this->service, $name), 
                 $arguments);
  
      // -- Commit the transaction
      $this->db->commit();
 
      return $ret;

    } catch (Exception $e) {
   
      // -- Rollback the transaction
      $this->db->rollBack();
      echo $e->getMessage();
    }
  }
}

Toute la magie de cette classe repose dans l'utilisation de la méthode PHP __call(), cette méthode est appelée à chaque fois que l'on appelle une méthode inexistante sur un objet.

Dans notre cas nous pouvons appeler 3 méthodes sur un objet de type Service_TransactionnalService, par exemple, pour un appel à listAll() :

$transactionnalService = new Service_TransactionnalService(
    new Service_UserService());

$transactionnalService->listAll();

L'appel à listAll() sur $transactionnalService équivaut à avoir le code suivant dans la méthode listAll() de la classe de service Service_User.

/**
  * Lists all the Model_Users registered in the system.
  *
  * @return Array An array of Model_Users which represents 
  *               all the registered Model_Users.
  */
public function listAll() {

  // -- Starts a transaction
  $this->db->beginTransaction();

  try {
   
    $ret = $this->userDao->listAll();

    // -- Commit the transaction
    $this->db->commit();
 
    return $ret;

  } catch (Exception $e) {
   
    // -- Rollback the transaction
    $this->db->rollBack();
    echo $e->getMessage();
  }
}

Voilà, nous avons réussi à wrapper tous les appels de méthodes de nos services dans des transactions. Dans un de mes prochains post j'expliquerai comment accéder à vos services transactionnels de manière simple au sein de votre application afin d'éviter d'avoir des instanciations à la classe Service_TransactionnalService partout ou vous avez besoin d'un service transactionnel.

mardi 22 décembre 2009

Architecture Service / DAO / Modèle de données avec Zend

Les architectures des applications Java EE sur lesquelles j'ai travaillé étaient la plupart du temps basées sur une couche de services métiers transactionnels (Spring ou EJB3), une couche de DAO (Spring / Hibernate ou EJB3 / JPA) et une couche modèle de données basée sur des POJOs (Plain Old Java Objects).

L'objectif de ce post est de montrer comment utiliser une architecture de type Service / DAO / modèle de données avec le framwork Zend.

La documentation du framework Zend ne pousse pas vraiment le lecteur à mettre en place de telles architectures mais nous allons voir qu'il est tout à fait possible de le faire de façon simple.

Je n'expliquerai pas ici comment créer la hiérarchie de répertoires d'un projet Zend, veuillez vous référer à la documentation du framework Zend (http://framework.zend.com/manual/en/) ou au Quickstart (http://framework.zend.com/docs/quickstart) pour plus d'informations à ce sujet.


En Java la répartition des classes de services, de DAOs et du modèle de données se fait en créant des packages différents pour chacune des couches. Les classes sont ensuite suffixées, par exemple une interface correspondant à un service utilisateur pourrait s'appeler UserService, un DAO UserDao et une entité Hibernate User.

PHP et le framework Zend n'offrent pas de notion de packages, mais, en appliquant quelques modifications à la structure des répertoires d'un projet Zend et à l'Autoloader nous pouvons arriver à avoir quelque chose de similaire.


Structure des répertoires

Je propose de placer les classes de services dans un répertoire 'services', les classes de DAO dans un répertoire 'daos' et les classes du modèle de données dans un répertoire 'models'.

Vous devez disposer des répertoires suivants dans votre application Zend:

  • Un répertoire 'application/services'
  • Un répertoire 'application/daos'
  • Un répertoire 'application/models'

Surcharge de l'Autoloader Zend

Une fois les répertoires créés nous allons devoir surcharger l'Autoloader Zend de façon à pouvoir charger automatiquement les classes de services, de DAOs et du modèle de données.

De base l'Autoloder Zend permet de gérer les répertoires 'applications/services' et 'application/models' mais pas le répertoire 'application/daos'.

Pour permettre la gestion de cette structure de répertoires la solution consiste à surcharger la classe Zend_Application_Module_Autoloader de la manière suivante.

// File 'application/Bootstrap.php'

/**
 * Custom Zend AutoLoader used to allow use of a 'daos' 
 * directory which contains 'Dao_' prefixed classes.
 * 
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class CustomAutoLoader 
 extends Zend_Application_Module_Autoloader {

  /**
   * Initialize default resource types for module resource 
   * classes
   *
   * @return void
   */
  public function initDefaultResourceTypes() {
    $basePath = $this->getBasePath();
    $this->addResourceTypes(array(
      'dbtable' => array(
        'namespace' => 'Model_DbTable',
        'path' => 'models/DbTable',
      ),
      'form' => array(
        'namespace' => 'Form',
        'path' => 'forms',
      ),
      'model' => array(
        'namespace' => 'Model',
        'path' => 'models',
      ),
      'plugin' => array(
        'namespace' => 'Plugin',
        'path' => 'plugins',
      ),
      'dao' => array(
        'namespace' => 'Dao',
        'path' => 'daos'
      ),
      'service' => array(
        'namespace' => 'Service',
        'path' => 'services',
      ),
      'viewhelper' => array(
        'namespace' => 'View_Helper',
        'path' => 'views/helpers',
      ),
      'viewfilter' => array(
        'namespace' => 'View_Filter',
        'path' => 'views/filters',
      ),
    ));
    $this->setDefaultResourceType('model');
  }
}

Cet Autoloder permet de nommer les classes de la manière suivante:
  • Les classes de services sont préfixées par 'Service_', par exemple 'Service_User'. Les classes de services sont placées dans le répertoire 'application/services' du projet Zend.
  • Les classes de DAOs sont préfixées par 'Dao_', par exemple 'Dao_User'. Les classes de DAOs sont placées dans le répertoire 'application/daos' du projet Zend.
  • Les classes du modèle de données sont préfixées par 'Model_', par exemple 'Model_User'. Les classes du modèle de données sont placées dans le répertoire 'application/models' du projet Zend.

L'Autoloader est ensuite utilisé de manière classique dans votre classe de Bootstrap.

// File 'application/Bootstrap.php'

/**
 * Zend Bootstrap class.
 * 
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { 
 
  /**
   * Autoload initialization function.
   */
  protected function _initAutoload() {
    $autoloader = new CustomAutoloader(array(
      'namespace' => '',
      'basePath' => dirname(__FILE__),
    ));

    return $autoloader;
  }
}


Exemple de classe de service
Les classes de services doivent être placées dans le répertoire 'application/services' et disposer de la convention de nommage suivante:
  • Le nom du fichier PHP doit porter le nom du service sans le préfixe 'Service_', par exemple la classe 'Service_User' sera dans le fichier 'application/services/User.php'.
  • Le nom de la classe est toujours préfixée par 'Service_', par exemple 'Service_User'.

Voici un exemple de service utilisateur.

// File 'application/services/User.php'

/**
 * Service used to manage Model_Users.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Service_User implements Service_Service {

  /**
   * DAO used to manage the persistence for Model_Users entities.
   *
   * @var Dao_User
   */
   private $userDao;

  /**
   * Constructs a new Service_User.
   *
   * @return Service_User
   */
  public function __construct() {
    $this->userDao = new Dao_User();
  }

  /**
   * Saves a Model_User in the system.
   *
   * @return Model_User $user the user to save in the system.
   */
  public function save(Model_Account $user) {
    $this->userDao->saveOrUpdate($user);
  }

  /**
   * Lists all the Model_Users registered in the system.
   *
   * @return Array An array of Model_Users which represents 
   *               all the registered Model_Users.
   */
  public function listAll() {
    return $this->userDao->listAll();
  }
}


Exemple de classe de DAO
Les classes de DAO doivent être placées dans le répertoire 'application/daos' et disposer de la convention de nommage suivante:
  • Le nom du fichier PHP doit porter le nom du DAO sans le préfixe 'Dao_', par exemple la classe 'Dao_User' sera dans le fichier 'application/daos/User.php'
  • Le nom de la classe est toujours préfixée par 'Dao_', par exemple 'Dao_User'

Voici un exemple de DAO utilisateur.

// File 'application/daos/User.php'

/**
 * DAO used to manage Model_User business entities.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Dao_User extends Dao_AbstractDao {

  /**
   * Constructs a new Dao_User.
   *
   * @return Dao_User
   */
  public function __construct() {
    parent::__construct(new Model_DbTable_Account());
  }

  /**
   * Saves a new Model_User in database or updates an existing 
   * one.
   *
   * @param Model_User $user the user to save or update.
   */
  public function saveOrUpdate(Model_User $user) { 
    ...
  }

  /**
   * Finds a Model_User using a technical identifier.
   * 
   * @param Integer $id The technical identifier used to find 
   *                    the Model_User.
   * 
   * @return Model_User The found Model_User or null 
   *                    if no Model_User having an identifier
   *                    equals to $id has been found in the 
   *                    system.
   */
  public function find($id) {
    ...
  }

  /**
   * Lists all the Model_Users registered in the system.
   * 
   * @return Array An array of Model_User representing all the 
   *               Model_User registered in the system.
   */
  public function listAll() {
    ...
  }
}

Exemple de classe du modèle de données
Les classes du modèle de données doivent être placées dans le répertoire 'application/models' et disposer de la convention de nommage suivante:
  • Le nom du fichier PHP doit porter le nom de l'entité sans le préfixe 'Model_', par exemple la classe 'Model_User' sera dans le fichier 'application/models/User.php'
  • Le nom de la classe est toujours préfixée par 'Model_', par exemple 'Model_User'

Voici un exemple d'entité utilisateur.

// File 'application/models/User.php'

/**
 * Business entity for a user.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Model_User extends Model_AbstractEntity {

  /**
   * The user name.
   *
   * @var String
   */
  private $name;
}

La classe Zend Db_Table associée pourrait être la suivante.

// File 'application/models/DbTable/User.php'

/**
 * Zend Db Table used to map the Model_User class 
 * to the users table.
 *
 * @author Baptiste GAILLARD (baptiste.gaillard@gmail.com)
 */
class Model_DbTable_User extends Model_DbTable_AbstractTable {

  /**
   * The table name.
   *
   * @var String
   */
   protected $_name = 'users';
} 

Nous avons vu dans cet article que la conception d'une application Zend basée sur une architecture logicielle Service / DAO / Modèle de données est très simple à mettre en place.

Cette architecture a évidemment de nombreux avantages, notamment une séparation claire des responsabilités en 3 couches bien définies.

Enfin, n'hésitez pas à apporter votre avis, remarques ou critique à propos de cet article. Je n'ai trouvé que peu d'informations sur internet abordant cette architecture Service / DAO / Modèle de données en PHP. Je pense que quelques échanges sur le sujet et un partage des expériences seraient les bienvenus.