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.

Aucun commentaire: