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. 

2 commentaires:

Anonyme a dit…

Ton article est un peu ancien mais je trouve bien que certaine personne se penche sur le sujet Service/DAO/Modèle en PHP et surtout avec Zend Framework.

Le seul reproche est que le composant Zend_Db_Table est basé sur la pattern Active Record donc effectivement le modèle est directement lié avec la persistance des données.

J'ai trouvé une source sur Github qui inclut les notions de Service/DAO et Modèle avec Doctrine 2 qui est basé sur le pattern DataMapper et l'injection de dépendance le tout couplé avec Zend_Framework.

https://github.com/loicfrering/losolib

J'aimerai savoir ce que tu en penses.

BGBlog a dit…

Merci pour le lien, je n'ai malheuresement pas trop le temps (je ne travaille plus en PHP en ce moment) de regarder dans le détail l'exemple que tu as trouvé.

Je pense que ce qui est bien dans cette source c'est:
- qu'elle utilise Doctrine qui semble vraiment se rapprocher d'Hibernate en Java, l'utilisation d'un modèle de données annoté est géniale, les requêtes DQL aussi.
- qu'elle utilise l'injection de dépendances (http://components.symfony-project.org/dependency-injection/).

Tu as des retours sur l'utilisation de Doctrine 2 et Symphony DI ?

En tous cas, même sans les avoir assayé ça m'a l'air très propre et plus rapide que l'utilisation que le framework Zend seul...