Archive pour la catégorie ‘zend_Framework’

Un messager pour collecter les messages à l’utilisateur.

Vendredi 16 mai 2008

Si vous avez lu l’article « De la granularité des actions » vous avez pu approcher les avantages et les inconvénients d’utiliser des actions atomiques. Un des problèmes induit par cette approche et la gestion des messages à l’utilisateur. Ce n’est pas propre à la méthode, c’est en fait dû à l’utilisation des redirections. Ce qui suit concerne donc toute application qui utilise les redirections. La problématique est relativement simple : si dans une action A j’ai besoin d’afficher un message à l’utilisateur et que cette action fait une redirection je me retrouve avec un conflit. Si j’affiche le message je ne peux pas rediriger la page. Et si je redirige, je ne peux pas afficher le message.

Contourner le problème

Une façon simple de faire, est d’abandonner la redirection et d’afficher le message avec un lien sur la page à atteindre. Cette approche est pour le moins simpliste et facile à mettre en œuvre. Elle implique tout de même l’intervention de l’utilisateur. De plus les messages étant affichés dans une page à part, la lisibilité n’est pas toujours au rendez-vous. Imaginez que votre application vérifie un formulaire complexe et vous affiche une liste de trente messages. Et ensuite vous cliquez sur le lien pour voir le formulaire. Pas évident de se souvenir de tous les messages pour corriger le formulaire. Il en va de même si les redirections se font en cascade.

Garder les messages sous le coude

Une autre approche consiste à se poser la question : quand afficher les messages ? En effet le problème vient des redirections et il peut y en avoir plusieurs. Mais quoi qu’il arrive il va bien y avoir un affichage, car sinon mon application part dans une boucle folle. Donc si je garde mon message, je pourrai l’afficher dès qu’une action ne fait plus de redirection. Donc lorsque j’ai un message à afficher je le garde sous le coude dans la session et lorsque j’arrive sur une action qui affiche, je récupère mon message, le donne à la vue et le supprime de la session. Si je reprends mon processus décrit dans l’article « De la granularité des actions » j’ai :

  • Afficher la liste

  • Rechercher les données de l’enregistrement à éditer
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données

Dans cet enchaînement mon problème va se rencontrer dans les actions « rechercher les données » « vérifier les données » et « traiter les données ». Dans ces actions je dois donc ajouter dans la session les messages lorsqu’il y en a.

         $messenger = new Zend_Session_Namespace('messenger');
         $messenger = 'invalid datas';

Si la recherche est infructueuse, je reviens sur l’affichage de la liste. Je dois donc dans cette action récupérer les messages dans la session, les donner à la vue et les effacer. De même si les données de formulaire sont invalides, ou si le traitement échoue, je reviens sur l’affichage du formulaire. Dans celui-ci je vais aussi devoir récupérer les messages dans la session, les donner à la vue et les effacer. C’est le traitement que je donnais dans l’exemple de cet article.

      $messenger = new Zend_Session_Namespace('messenger');
      $this->view->messages = $messenger;
      unset($messenger);

Généraliser le messager

On voit aisément que tout cela va être très répétitif. Il est alors intéressant de généraliser ce comportement. Pour toutes les actions, nous avons la déclaration du namespace dans la session. Si nous plaçons cette déclaration dans la méthode init de tous nos contrôleurs, toutes nos actions bénéficieront de cette déclaration. Toutes les actions qui génèrent un affichage donnent les messages à la vue et les supprime de la session. Comment déterminer automatiquement si une action fait ou ne fait pas de redirect ? Pour cela nous allons fouiller un peu le fonctionnement du contrôleur. Ouvrez la classe Zend_Controller_Action dont tous vos contrôleurs dérivent. Vous y trouverez les méthodes suivantes

  • Init

  • dispatch
  • render
  • _redirect

Init est appelé après le constructeur. C’est la bonne méthode pour placer des initialisations communes à toutes les actions. Comme la déclaration du namespace. Dispatch est le cœur du contrôleur c’est là que l’action courante est réellement traitée. Render est appelé par une action pour faire un rendu explicite d’une page. Et _redirect est invoqué pour une redirection.
Il y a donc deux façons d’invoquer un rendu : soit explicitement dans l’action et c’est render qui est utilisé. Soit l’action se termine sans redirect et sans render, le rendu est automatique et c’est dans le dispatch que cela se passe.

Une classe Messager

Lorsque l’on collecte des messages, cela implique de gérer une collection de messages et non une simple chaine. De plus on peut avoir divers type de messages. Des infos, des alertes, des erreurs, etc. Un messager est donc un agent qui collecte des messages les enregistre en session et les restitue pour la vue. Il est aussi nécessaire de purger les messages de temps en temps.
Nous allons définir une classe pour cela.
Nous allons donc avoir un membre pour référencer le namespace.et un tableau contenant la liste des messages.
Nous avons besoin des méthodes addOk, addNotice, addWarning, addError, getAll, clear, write, et nous ajouterons pour nous faciliter l’écriture add
Write place le tableau des messages dans le namespace, add permet d’ajouter un message en passant son type en paramètre.
Celle classe est relativement simple et peut être donnée directement ici.

_messenger = new Zend_Session_Namespace('Fast_Controller_Messenger');
        if (isset($this->_messenger->messages)) $this->_messages = &$this->_messenger->messages;
    }

    /**
    * met en session un objet standard contenant tous les membres
    * de l'objet Messenger.
    *
    * @return null
    */
    public function write()
    {
        $this->_messenger->messages = $this->_messages;
    }

    /**
    * vide tous les messages de l'objet Messenger.
    *
    * @return null
    */
    public function clear()
    {
        $this->_messenger->messages = null;
        $this->_messenger->messages = $this->_messages = array();
    }

    /**
    * Ajoute un message à la collection de messages.
    *
    * @param string $msg Texte du message
    * @param string $type Type du message (Messenger::NORMAL, Messenger::NOTICE, Messenger::WARNING, Messenger::ERROR, Messenger::OK)
    */
    public function add($msg, $type=self::NORMAL)
    {
        $obj = new StdClass();
        $obj->label = $msg;
        $obj->type = $type;
        $this->_messages[] = $obj;
    }

    /**
    * Ajoute un message de simple information.
    *
    * Alias de add($msg)
    *
    * @param string $msg Texte du message
    */
    public function addNormal($msg)
    {
        $this->add($msg, self::NORMAL);
    }

    /**
    * Ajoute un message indiquant que les opérations se sont
    * déroulées avec succès.
    *
    * Alias de add($msg, Messenger::OK)
    *
    * @param string $msg Texte du message
    */
    public function addOk($msg)
    {
        $this->add($msg, self::OK);
    }

    /**
    * Ajoute un message indiquant un avertissement, sans conséquence
    * sur le déroulement des opérations.
    *
    * Alias de add($msg, Messenger::NOTICE)
    *
    * @param string $msg Texte du message
    */
    public function addNotice($msg)
    {
        $this->add($msg, self::NOTICE);
    }

    /**
    * Ajoute un message indiquant un avertissement fort, la suite
    * des opérations pouvant être compromise.
    *
    * Alias de add($msg, Messenger::WARNING)
    *
    * @param string $msg Texte du message
    */
    public function addWarning($msg)
    {
        $this->add($msg, self::WARNING);
    }

    /**
    * Ajoute un message indiquant une erreur, la suite
    * des opérations pouvant être gravement compromise, voire arrêtée.
    *
    * Alias de add($msg, Messenger::ERROR)
    *
    * @param string $msg Texte du message
    */
    public function addError($msg)
    {
        $this->add($msg, self::ERROR);
    }

    /**
    * Retourne le tableau de messages.
    *
    * @return array
    */
    public function getAll()
    {
        return $this->_messages;
    }
}

Automatisation

Nous avons donc un messager. Il ne reste qu’à l’invoquer. Nous avons vu que tous les contrôleurs dérivent de Zend_Controller_Action. Nous ne pouvons pas modifier cette classe. Et il serait lourd de devoir redéfinir ces méthodes dans tous les Contrôleurs de l’application. Nous allons donc dériver Zend_Controller_Action en Fast_Controller_Action et faire dériver nos contrôleurs de cette classe.


Nous avons ainsi la possibilité de redéfinir les méthodes qui nous intéressent pour tous les contrôleurs.
La méthode init instancie le messager pour le rendre disponible dans toutes les actions

    public function init()
    {
        parent::init();
        $this->_messenger = new Fast_Controller_Messenger();
    }

Dans la méthode redirect on écrit les messages dans la session

    protected function _redirect($url, array $options = array())
    {
        // stocke les messages en session pour affichage ultérieur
        $this->_messenger->write();
        parent::_redirect() ;
    }

La méthode postDispatch est appelée par la méthode disptach après le traitement. En fin de processus. C’est donc un bon endroit pour donner les messages à la vue et vider le messager

    function postDispatch()
    {
        if (!$this->_redirected) {
            $this->view->messages = $this->_messenger->getAll();
            $this->_messenger->clear();
        }
        parent::postDispatch();
    }

La méthode render pour les appels explicites

    public function render($action = null, $name = null, $noController = false)
    {
        // on ajoute à la vue le tableau de messages à diffuser
        $this->view->messages = $this->_messenger->getAll();
        // on passe la main au renderer pour afficher la vue
        parent::render($action, $name, $noController);
    }

Utilisation

Nous avons donc maintenant un messager automatique dans nos contrôleurs. Voyons comment l’utiliser. Pour cela reprenons l’exemple de l’article sur la granularité des actions. Dans cet exemple nous avions des bouts de code pour placer les messages en session. Ils sont à remplacer par les méthodes add du messager. Nous avions aussi du code pour afficher les messages qui est devenu inutile.

view->list = $this->model->getObjectList();
   }

   /**
    * Prépare un enregistrement pour l'afficher dans le formulaire
    * redirige vers showForm
    * @see showForm
    */
   public function addAction() {
      $context = new Zend_Session_Namespace('context');

      $context->returnPath = '/FormController/showList';
      $context->saveMethod = 'add';
      // Demander au model un nouvel enregistrement avec les valeurs par défaut
      $context->formData = $this->model->newObject();
      $this->_redirect('/FormController/showForm');
   }

   /**
    * Recherche l'enregistrement pour l'afficher dans le formulaire
    * redirige vers showForm
    * @see showForm
    */
   public function editAction() {
      $context = new Zend_Session_Namespace('context');

      $context->returnPath = '/FormController/showList';
      $context->saveMethod = 'update';

      $id = $this->_request->get('id');
      $context->formData = $this->model->getItemById($id);
      if (!$context->formData) {
         // revenir au point de retour
         $this->_messenger->addError('no object for: '.$id);
         $redirect = $context->returnPath;
      } else {
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }

   /**
    * Affiche le formulaire d'édition d'un élément.
    *
    * @return null
    */
   public function showFormAction(){
      $context = new Zend_Session_Namespace('context');

      $this->view->cancelAction = $context->returnPath;
      $this->view->saveAction = '/FormController/checkForm/';

      $this->view->form = clone($context->formData);
   }

   /**
    * Récupère les données du formulaire les filtres et les vérifie
    * redirige vers save si le formulaire est valide showFrom sinon
    * @see save
    */
   public function checkFormAction() {
      $context = new Zend_Session_Namespace('context');
      if ($context->formData = $this->_request->get('form'))
      // vérification
      $ok = true;
      if ($ok) {
         $redirect = '/FormController/save';
      } else {
         $this->_messenger->addError('invalid datas');
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }

   /**
    * enregistre l'élément dans la collection
    * redirige vers l'action qui précédé l'action add ou edit en cas de succès
    * vers showForm en cas d'échec
    * @see add
    * @see edit
    * @see showForm
    */
   public function saveAction($perform = true) {
      $context = new Zend_Session_Namespace('context');

      $data = $context->formData;
      $method = $context->saveMethod;

      $ok = $this->model->saveObject($data, $method);
      if ($ok) {
         $this->_messenger->addOk('data saved');
         $redirect = $context->returnPath;
      } else {
         $this->_messenger->addError('error data could not be saved');
         $redirect = '/FormController/showForm';
      }
      $this->_redirect($redirect);
   }
}

On le voit ici, nous sommes totalement déchargés de la gestion des messages. Dans l’écriture d’un contrôleur nous ne faisons que nous concentrer sur le traitement et indiquer les messages à afficher. Ils le seront au moment approprié, sans que l’on ait à s’en préoccuper.
Le monde parfait n’existant pas cette façon de faire à un très léger défaut qu’il faut tout de même connaitre. Le messager est systématiquement vidé après chaque affichage. Ce qui signifie que si l’utilisateur use de la touche F5 sur son navigateur il appelle de nouveau l’action courante qui à déjà vidé les messages, les messages ne seront donc pas réaffichés.

Auto-Jointure

Mardi 12 février 2008

Dans mon article « Ajouter un champ calculé dans une table. » Je parlais d’une méthode pour ajouter des champs dans les objets row issue d’une table qui ne les contient pas base.
La version 1.5 de ZF a introduit une nouvelle classe qui facilite ce genre de manipulation.

L’Auto Jointure simple

Cette petite nouveauté introduit de façon claire les select qui sous-tend la classe Zend_Db_Table. Pour voir comment cette introduction change la donne je me suis posé le problème de l’auto jointure. J’ai une table principale et une où plusieurs table de références.
Le but de l’autojointure et de remonter avec les éléments de la table les valeurs de références. Par exemple les éléments de ma table principale sont typés, il contient un id sur la table de référence des types. Je veux lorsque je lis un enregistrement pouvoir remonter le label de sont type sans avoir systématiquement à faire une deuxième requête.
Les données complémentaires étant des données de référence leur valeur ne change pas (pas lorsqu’on manipule un élément de la table principale) je ne poserais donc pas ici le problème de l’écriture en base.

Zend_Db_Table_Select

Voyons comment fonction les classes de Zend_Db lorsque nous lisons dans la table. L’appel des méthodes fetch, fetchCols, fetchRow, fetchAll, et find font tous appels à _fetch. Dans les versions antérieures à la 1.5 la méthode _fetch recevais les paramètres de la requête. Where et autres order ou limit. La méthode _fetch fabriquait donc un select avec $this->db->select() et y appliquait les clauses adéquates.
Avec la version 1.5 les fonctions suscitées vont-elles-même créer l’objet select et lui ajouter les clauses nécessaires. Pour éviter que chaque fonction réinvente la roue elles ne vont pas créer un simple select mais un Zend_Db_Table_Select dont la base est simplement
Select * From tablename. Pour cela une nouvelle méthode à été ajouté à Zend_Db_Table : select il est donc possible de demander à la table un select préconfiguré pour le manipuler à sa sauce. Nous allons donc nous arranger pour que ce select ne soit pas un simple Zend_Db_Table_Select mais pour qu’il ajoute automatiquement une ou plusieurs jointures à la table.

Fast_Db_Table

Une classe table acceptant l’autojointure. Notre but est de définir une classe comme Zend_Db_Table_Abstract que nous pourrons dériver pour la mapper sur les éléments de la base. On va donc introduire un membre qui listera les jointures à effectuer dans les requêtes. Et tant que nous y sommes nous allons ajouter des restrictions automatique (clauses where ajouté systématiquement)

 /**
 * Definition de base d'une table Fast
 * elle étend la classe Zend_Table et lui adjoint un classe spécifique pour les enregistrement
 * ainsi que les méthode courantes d'accès au données
 *
 * @see  Zend_Db_Table
 * @see  Fast_Exception_Db
 * @see  Fast_Db_Row
 * @author Jean-Yves Terrien
 */
Class Fast_Db_Table extends Zend_Db_Table_Abstract {

   const FAST_RESTRICT = 'fast_restrict';
   const FAST_AUTOJOIN = 'fast_autojoin';
   /**
   * Classname for select , Zend_Db_Table_Select,...
   *
   * @var string
   */
   protected $_selectClass = 'Fast_Db_Table_Select';
   /**
   * Restriction for query
   *
   * @var string
   */
   protected $_restrict = null;
   /**
   * Auto Joined table for query
   *
   * @var string
   */
   protected $_autojoin = NULL;
   /**
    * Returns an instance of a Zend_Db_Table_Select object.
    *
    * @return Zend_Db_Table_Select
    */
   public function select()
   {
      if ('Zend_Db_Table_Select' == $this->_selectClass) {
         $select = parent::select();
      } else {
         Zend_Loader::loadClass($this->_selectClass);
         $select = new $this->_selectClass($this);
      }
      return $select;
   }
    /**
     * Returns table information.
     *
     * @return array
     */
    public function info()
    {
        $info = parent::info();
        $info[self::FAST_RESTRICT] = $this->_restrict;
        $info[self::FAST_AUTOJOIN] = $this->_autojoin;
        return $info;
    }
}

Voilà la base de notre classe on va lui indiquer la classe Select à utiliser (il faudra qu’elle dérive de Zend_Db_Table_Select) cela permettra de la surcharger. On redéfinit la méthode Select() pour tenir compte de notre classe Select et on redéfini la méthode info pour que la classes Select connaisse les jointures à faire.
La définition de la requête étant faite dans la classe Select c’est tout pour la classe Table.

Fast_Db_Table_Select

Un select pour table à auto jointure.
Il est un peu étonnant de voir que la classe Zend_Db_Table_Select ne crée pas la partie from de la table par défaut. Elle ne le fait qu’au moment de la transformation en chaine. Du coup si on tente de faire le join avant on obtient une exception. Il faut donc respecter cette mécanique pour nous insérer au mieux dans ZF

 class Fast_Db_Table_Select extends Zend_Db_Table_Select
{
   private $_autojoined = false;
   private $_useRestrict = true;
   /**
   * Performs a validation on the select query before passing back to the parent class.
   * Ensures that only columns from the primary Zend_Db_Table are returned in the result.
   *
   * @return string This object as a SELECT string.
   */
   public function __toString()
   {
      if (!$this->_autojoined) {
         $this->_autojoined = true;

         $fields  = $this->getPart(Zend_Db_Table_Select::COLUMNS);
         $primary = $this->_info[Zend_Db_Table_Abstract::NAME];
         $schema  = $this->_info[Zend_Db_Table_Abstract::SCHEMA];

         // If no fields are specified we assume all fields from primary table
         if (!count($fields)) {
            $this->from($primary, '*', $schema);
            $fields = $this->getPart(Zend_Db_Table_Select::COLUMNS);
         }

         if ($this->_useRestrict) {
            if (is_string($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
               $restricts[] = $this->_info[Fast_Db_Table::FAST_RESTRICT];
            } elseif (is_array($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
               $restricts = $this->_info[Fast_Db_Table::FAST_RESTRICT];
            } else {
               $restricts = array();
            }
            foreach ($restricts as $restrict) {
               $this->where($restrict);
            }
         }
         if (is_array($this->_info[Fast_Db_Table::FAST_AUTOJOIN])) {
            $this->setIntegrityCheck(false);
            foreach ($this->_info[Fast_Db_Table::FAST_AUTOJOIN] as $join) {
               if (is_array($join)) {
                  $this->join($join['table'], $join['on'], $join['fields']);
               }
            }
         }
      }
      return parent::__toString();
   }
   /**
      @function setRestrict()
   	@param boolean $restrict Use restriction for this select
      @return Fast_Db_Table_Select Description
   */
   function setRestrict($restrict) {
      $this->_useRestrict = $restrict;
      return $this;
   } // end function setRestrict
}

Le premier membre private $_autojoined indique que la jointure à déjà été faite. Vu que nous l’ajoutons lors de la transformation en chaine il ne faudrait pas que le soit plusieurs fois. Un simple echo sur l’objet appelle __toString()
Le second private $_useRestrict = true indique que nous devons ou pas utiliser les clauses restrictives
La méthode setRestrict permet de changer ce mode.
La méthode __toString est semblable à celle de sa classe parente elle ne fait que parcourir le tableau d’auto jointure pour ajouter les clauses join.

Une utilisation

La classe user. Nous allons remonter le champ label du profil en même temps que l’utilisateur.

 Class Adm_Model_User_Table extends Fast_Db_Table {
   /**
   * The table name.
   *
   * @var array
   */
   protected $_name = 'user';

   /**
   * Classname for row
   *
   * @var string
   */
   protected $_rowClass = 'Adm_Model_User_Row';
   /**
   * Auto Joined table for query
   *
   * @var string
   */
   protected $_autojoin = array(
      array('table' => 'profile',
            'on' =>  'profile.prf_id = user.prf_id',
            'fields' => array('prf_label')
      )
   );
   /**
   * Restriction for query
   *
   * @var string
   */
   protected $_restrict = array('profile.prf_valid = 1');
}

Lorsque nous utiliserons cette classe la requête générée par défaut sera

Select
   user.*,
   profile.prf_label
FROM user
INNER JOIN profile ON (profile.prf_id = user.prf_id)
WHERE profile.prf_valid = 1

Conclusion

Zend_Db_Table_Select apporte une ouverture nouvelle pour adapter au mieux à ses besoins le mapping objet de ZF. Les classes ci-dessus ne sont que des premières versions. Elles sont grandement améliorables. Par exemple pour gérer les jointures plus complexes comme left rigth mais aussi avec des clauses de recoupement etc.

Ajouter un champ calculé dans une table.

Vendredi 1 février 2008

Il arrive parfois qu’il soit intéressant de véhiculer un champ dans un objet de mapping qui n’est pas conservé en base. C’est le cas entre autre des champs calculés. Je veux par exemple un objet facture. Lorsque je manipule ma facture un élément important est le total de la facture. Mais une facture en elle-même est composée de champs qui lui sont propres et de lignes de facturations. Chaque ligne véhicule une partie du total de la facture. Lorsque je manipule la facture je n’ai pas nécessairement besoin de ses lignes de facturations. Par exemple lorsque je vérifie que le montant payé et bien le bon. Inutile de remonter toutes les lignes seul le total m’intéresse. Je peux alors décider de garder en base le total. Il me faudra alors veiller à ce que ce total soit tenu à jour en adéquation avec mes lignes de facturations. Je peux aussi décider de ne pas le garder en base. Mais alors il me faudra le calculer et donc faire deux accès à la base pour obtenir ma facture (sans ses lignes mais avec) son total

Ajouter un champ à un objet de mapping

Une solution consiste à calculer ce champ lors de la lecture en base et de le garder dans un coin. Si je le mets directement dans mon objet de mapping je vais me heurter à quelques difficultés. Par exemple si j’enregistre cet objet l’ORM de Zend va au mieux supprimer le champ, au pire lever une exception et il aura raison. Il ne sait qu’en faire. Le but de cet article est de voir les points à lever pour arriver à cette solution.

la table facture

La toute première étape consiste à crée un classe pour la table facture

Class Model_Facture_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'facture';
   protected $_rowClass = ' Model_Facture_Row';
   public function __construct($config = array())
   {
      parent::__construct($config);
	$this->_cols[] = 'fac_total';
   }
}

Et d’y ajouter une colonne. Ainsi lorsque je sortirais ou entrerait une facture dans ma table le champ fac_total ne sera pas inconnu.
Mais il va falloir aller un peu plus loin si on ne veut pas se retrouver avec des Exception de partout. La première étape passe par le calcul de ce champ. Donc lorsqu’on lit un enregistrement dans la base. ZF est ainsi fait que quelque soit la façon dont vous interrogez votre table il passe toujours par la même méthode. Sauf évidement si vous écrivez vous-même une requête. La méthode qui définit la requête à effectuer sur la base pour lire un ou plusieurs enregistrements s’appelle _fetch. Il nous faut donc la modifier pour obtenir le résultat que nous cherchons. Ainsi toute lecture prendra en compte notre modification. Pour bien comprendre ce que fait cette méthode il suffit de se pencher sur le fonctionnement d’une Zend_Db_Table. De façon générale une Zend_Db_Table c’est

SELECT * FROM tableName ;

Les autres méthodes de recherche ne font qu’ajouter des clauses WHERE ORDER ETC. le but de la méthode _fetch est de construire cette requête.
Moi, je voudrais à la place

SELECT
   facture .*,
   SUM(lig_prix*lig_qte) AS fac_total
FROM facture
INNER JOIN lignes USING (fac_id)
GROUP BY fac_id;

modifier la méthode _fetch

Tout d’abord voyons comment est faite la méthode de Zend

    /**
     * Support method for fetching rows.
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @param  string|array $order  OPTIONAL An SQL ORDER clause.
     * @param  int          $count  OPTIONAL An SQL LIMIT count.
     * @param  int          $offset OPTIONAL An SQL LIMIT offset.
     * @return array The row results, in FETCH_ASSOC mode.
     */
    protected function _fetch($where = null,
                              $order = null,
                              $count = null,
                              $offset = null)
    {
        // selection tool
        $select = $this->_db->select();

        // the FROM clause
        $select->from($this->_name, $this->_cols, $this->_schema);

        // the WHERE clause
        $where = (array) $where;
        foreach ($where as $key => $val) {
            // is $key an int?
            if (is_int($key)) {
                // $val is the full condition
                $select->where($val);
            } else {
                // $key is the condition with placeholder,
                // and $val is quoted into the condition
                $select->where($key, $val);
            }
        }

        // the ORDER clause
        if (!is_array($order)) {
            $order = array($order);
        }
        foreach ($order as $val) {
            $select->order($val);
        }

        // the LIMIT clause
        $select->limit($count, $offset);
        // return the results
        $stmt = $this->_db->query($select);
        $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
        return $data;
    }

Cette méthode est, un peu, longue mais au final par très complexe. On voit vite que notre SELECT * est à la ligne

$select->from($this->_name, $this->_cols, $this->_schema);

Et que c’est là qu’il faut intervenir. En effet le reste n’est que l’ajout de clause diverse.
Remplaçons donc

$cols = $this->_cols;
unset($cols[array_search('fac_total',$cols)]);
$select->from($this->_name, $cols, $this->_schema)
->join('lignes',’ligne.fac_id = facture.fac_id’, array('fac_total' =>Zend_Db_Exp('SUM(lig_prix * lig_qte)')))
->group('fac_id');

Nous avons maintenant une table qui lit des factures avec leur total.

L’écriture

Tant que nous ne faisons que lire dans la table avec cet objet, nous n’auront pas de problème.
Mais si nous tentons un update ou un insert nous allons avoir un problème. En effet nous allons essayer de mettre à jour dans la base un champ qui n’y est pas. Il nous faut donc retirer ce champ de l’objet. Avant l’enregistrement.

   public function insert(array $data) {
      unset($data[‘fac_total’]);
      parent::insert($data);
   }

Cela n’est en fait pas bien compliqué. La méthode insert va en accord avec la liste des colonnes de la table tenter d’ajouter l’enregistrement avec tous les champs présents dans $this->_cols si un champ de data n’est pas dans la liste, il sera supprimé, mais fac_total y est puisque nous l’avons ajouté. Mais ce champ n’est pas dans la table le moteur SQL va donc rejeter la requête. Il suffit donc de supprimer ce champ des données.
La méthode update à un comportement équivalent mais légèrement différent. En ce sens que update va tenter de mettre à jour même les champs qui ne son pas présent dans $data. Si je retire simplement le champ fac_total la méthode update tentera de le mettre à null dans la base. Il faut donc retirer le champ des données mais aussi de la liste des colonnes. Et le restituer ensuite car sinon notre objet table sera incohérent.

    public function update(array $data, $where)
    {
      unset($this->_cols[array_search(‘fac_total’,$this->_cols)]);
      unset($data[‘fac_total’]);
      $res = parent::update($data, $where);
      $this->_cols[] = ‘fac_total’;
      return $res;
    }

Il est ainsi possible d’ajouter de nombreux champs dans un objet table qui ne serons pas stockés en base. Il est assez simple de généraliser cette méthode. En se basant sur la description des relations dans les Zend_Db_Table on peut imaginer une classe abstraite qui contiendrait un tableau des autoJoinnedTable et qui implémenterait ce principe.

Un exemple généralisé

Pour ma part je l’utilise dans un tout autre contexte. J’utilise des tables hiérarchiques en POO il est simple et efficace d’utiliser une référence sur l’objet parent pour constituer une hiérarchie. Mais cette approche n’est pas performante dans une base de données. Une représentation intervallaire l’est bien mieux. Je vous conseille de lire les articles sur le sujet.
Dans le cas qui m’intéresse, j’ais donc des tables qui on un id numérique unique comme clef et des données. Le principe de la représentation intervallaire consiste à ajouter une borne droite et une borne gauche, j’ai aussi très souvent besoin de niveau hiérarchique relatif. Par exemple tous les nœuds au rand N+1, N+2 et N+3 d’un nœud donné. J’ajoute donc dans ma table un champ level.
Mais du côté PHP il est plus simple d’utiliser une relation père fils que la notion d’intervalle. Ma classe va donc masquer la représentation intervallaire à PHP. Elle va gérer elle-même les transactions nécessaires pour maintenir à jours les bornes des éléments de la table.

Zend_Loader::loadClass('Zend_Db_Table');

Class Fast_Db_Hierarchical extends Zend _Db_Table {

   /**
   * left field name in table
   *
   * @var string
   */
   protected $_left = NULL;

   /**
   * right field name in table
   *
   * @var string
   */
   protected $_right = NULL;

   /**
   * level field name in table
   *
   * @var string
   */
   protected $_level = NULL;

   /**
   * virtual field name used has id of parent
   *
   * @var string
   */
   protected $_parent = NULL;

   public function __construct($config = array())
   {
      parent::__construct($config);
      if (null == $this->_left)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEFT_KEY);
      if (null == $this->_right)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_RIGHT_KEY);
      if (null == $this->_level)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEVEL_KEY);
      if (null == $this->_parent)
         throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_PARENT);
      $this->_cols[] = $this->_parent;
   }

   public function getById($id) {
      $rows = $this->find($id);
      if ($rows) {
         return $rows->current();
      }
      return false;
   }

   public function deleteById($id) {
      if ($id == 1) return false; // on ne peut supprimer la racine

      $this->_db->beginTransaction();
      $parent = $this->_db->select();
      $parent->from($this->_name, array('delete_left' => $this->_left,
                                        'delete_right' => $this->_right))
             ->where($this->_primary[1].' = :_deleteId');
      $statement = $this->_db->prepare($parent);
      $statement->execute(array('_deleteId' => $id));
      list($deleteLeft, $deleteRight) = array_values($statement->fetch());
      $res = false;
      if ($deleteLeft) {
         $row = $this->getById($id);
         $res = $row->delete();

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_left.' = '.$this->_left.' - 1
                WHERE '.$this->_left.' >= '.$deleteLeft.'
                AND '.$this->_right.' < '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_left.' = '.$this->_left.' - 2
                WHERE '.$this->_left.' >= '.$deleteLeft.'
                AND '.$this->_right.' > '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' - 1
                WHERE '.$this->_right.' >= '.$deleteLeft.'
                AND '.$this->_right.' < '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' - 2
                WHERE '.$this->_right.' >= '.$deleteLeft.'
                AND '.$this->_right.' > '.$deleteRight.';');
            $statement->execute();
         }

         if ($res) {
            $this->_db->commit();
         } else {
            $this->_db->rollback();
         }
      }
      return $res;
   }

   public function UpdateById($data) {
      // on ne peut mettre à jour les donnée hiérarchique
      // ie on ne peut déplacer un noeud dans l'arbre.
      unset($data[$this->_parent]); // ne fait pas partie de la table
      unset($data[$this->_left]);   //ne peut être changé
      unset($data[$this->_right]);  //ne peut être changé
      unset($data[$this->_level]);  //ne peut être changé
      $res =  parent::UpdateById($data);
      return $res;
   }

	public function insert(array $data) {
      # select left and level of parent
      $parentId = $data[$this->_parent];

      $this->_db->beginTransaction();
      $parent = $this->_db->select();
      if (null != $this->_level) {
         $fields = array('parent_left' => $this->_left,
                         'parent_level' => $this->_level);
      } else {
         $fields = array('parent_left' => $this->_left,);
      }

      $parent->from($this->_name, $fields)
             ->where($this->_primary[1].' = :_parentId');
      $statement = $this->_db->prepare($parent);
      $statement->execute(array('_parentId' => $parentId));
      list($parentLeft, $parentLevel) = array_values($statement->fetch());

      $res = false;
      if ($parentLeft) {
         #update tree
         $statement = $this->_db->prepare(
            'UPDATE '.$this->_name.'
             SET '.$this->_left.' = '.$this->_left.' + 2
             WHERE '.$this->_left.' > '.$parentLeft.';');
         $res = $statement->execute();
         if ($res) {
            $statement = $this->_db->prepare(
               'UPDATE '.$this->_name.'
                SET '.$this->_right.' = '.$this->_right.' + 2
                WHERE '.$this->_right.' > '.$parentLeft.';');
            $statement->execute();
         }

         #insert node
         if ($res) {
            unset($data[$this->_parent]);
            $data[$this->_left] = $parentLeft + 1;
            $data[$this->_right] = $parentLeft + 2;
            if (null != $this->_level)
               $data[$this->_level] = $parentLevel + 1;
            $res =  parent::insert($data);
   		}
         if ($res) {
            $this->_db->commit();
         } else {
            $this->_db->rollback();
         }
      }
      return $res;
	}

    /**
     * Support method for fetching rows.
     *
     * @param  string|array $where  OPTIONAL An SQL WHERE clause.
     * @param  string|array $order  OPTIONAL An SQL ORDER clause.
     * @param  int          $count  OPTIONAL An SQL LIMIT count.
     * @param  int          $offset OPTIONAL An SQL LIMIT offset.
     * @return array The row results, in FETCH_ASSOC mode.
     */
    protected function _fetch($where = null, $order = null, $count = null, $offset = null)
    {
        // selection tool
        $select = $this->_db->select();

        //no _parent col on master table
        $cols = $this->_cols;
        unset($cols[array_search($this->_parent,$cols)]);

        // the FROM clause
        $select->from($this->_name, $cols, $this->_schema);
        // add the parent col
        $select->join(array('parent' => $this->_name),
                      '(parent.'.$this->_left.' < workgroup.'.$this->_left.') AND
                       (parent.'.$this->_right.' > workgroup.'.$this->_right.') AND
                       (parent.'.$this->_level.' = workgroup.'.$this->_level.' -1)',
                      array('parent_id' => 'parent.'.$this->_primary[1].''));

        // the WHERE clause
        $where = (array) $where;
        foreach ($where as $key => $val) {
            // is $key an int?
            if (is_int($key)) {
                // $val is the full condition
                $select->where($val);
            } else {
                // $key is the condition with placeholder,
                // and $val is quoted into the condition
                $select->where($key, $val);
            }
        }

        // the ORDER clause
        if (!is_array($order)) {
            $order = array($order);
        }
        foreach ($order as $val) {
            $select->order($val);
        }

        // the LIMIT clause
        $select->limit($count, $offset);
        // return the results
        $stmt = $this->_db->query($select);
        $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
        return $data;
    }

    public function update(array $data, $where)
    {
      unset($this->_cols[array_search($this->_parent,$this->_cols)]);
      unset($data[$this->_parent]);
      $res = parent::update($data, $where);
      $this->_cols[] = $this->_parent;
      return $res;
    }

   protected function _parent($row, $fiels) {
      $parent = $this->_parents($row, $fiels)
             ->order($this->_right)
             ->limit(1);
      return $parent;
   }
   protected function _parents($row, $fiels) {
      $parent = $this->_db->select();
      $parent->from($this->_name, $fiels)
             ->where($this->_left.'  < '.$row->{$this->_left})
             ->where($this->_right.' > '.$row->{$this->_right});
      return $parent;
   }
   protected function _childs($row, $fiels) {
      $childs = $this->_db->select();
      $childs->from($this->_name, $fiels)
             ->where($this->_left.'  > '.$row->{$this->_left})
             ->where($this->_right.' < '.$row->{$this->_right});
      return $childs;
   }

    /**
     * This is the find Zend_Db_Table Abstract method
     * But the where closes are prefixed by the table name
     *
     * Fetches rows by primary key.
     * The arguments specify the primary key values.
     * If the table has a multi-column primary key, you must
     * pass as many arguments as the count of column in the
     * primary key.
     *
     * To find multiple rows by primary key, the argument
     * should be an array.  If the table has a multi-column
     * primary key, all arguments must be arrays with the
     * same number of elements.
     *
     * The find() method always returns a Rowset object,
     * even if only one row was found.
     *
     * @param  mixed                         The value(s) of the primary key.
     * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria.
     * @throws Zend_Db_Table_Exception
     */
    public function find()
    {
        $args = func_get_args();
        $keyNames = array_values((array) $this->_primary);

        if (empty($args)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("No value(s) specified for the primary key");
        }

        if (count($args) != count($keyNames)) {
            require_once 'Zend/Db/Table/Exception.php';
            throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
        }

        $whereList = array();
        $numberTerms = 0;
        foreach ($args as $keyPosition => $keyValues) {
            // Coerce the values to an array.
            // Don't simply typecast to array, because the values
            // might be Zend_Db_Expr objects.
            if (!is_array($keyValues)) {
                $keyValues = array($keyValues);
            }
            if ($numberTerms == 0) {
                $numberTerms = count($keyValues);
            } else if (count($keyValues) != $numberTerms) {
                require_once 'Zend/Db/Table/Exception.php';
                throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
            }
            for ($i = 0; $i < count($keyValues); ++$i) {
                $whereList[$i][$keyPosition] = $keyValues[$i];
            }
        }
        $whereClause = null;
        if (count($whereList)) {
            $whereOrTerms = array();
            foreach ($whereList as $keyValueSets) {
                $whereAndTerms = array();
                foreach ($keyValueSets as $keyPosition => $keyValue) {
                    $whereAndTerms[] = $this->_db->quoteInto(
                        $this->_db->quoteIdentifier($this->_name).'.'
                       .$this->_db->quoteIdentifier($keyNames[$keyPosition], true) . ' = ?',
                        $keyValue
                    );
                }
                $whereOrTerms[] = '(' . implode(' AND ', $whereAndTerms) . ')';
            }
            $whereClause = '(' . implode(' OR ', $whereOrTerms) . ')';
        }

        return $this->fetchAll($whereClause);
    }

}

Vous aurrez noté la présence de la méthode find alors qu’elle est disponible dans la classe Zend_Db_Table. Cela vient du fait que je fais une auto-jointure je joins la table sur elle-même. Du coup tous les champs de la table sont en double dans la requête. Or la méthode find construit des closes where simple. Il est nécessaire dans ce cas de les préfixer du nom de la table c’est ce que j’ai ajouté à la méthode find.

Conclusion

Cette façon de dériver la classe Zend_Db_Table permet d’imaginer toute sorte de mapping entre un objet et un ensemble de tables dans la base. Par exemple un modèle ou les adresses sont dans une table à part des clients alors que l’objet de mapping remonte toujours l’ensemble. Ou la remonté systématique des valeurs des tables de références etc.

A+JYT
ZIP File : Hierarchical Table

Utiliser une base de données avec Zend Framwork

Lundi 26 novembre 2007

Zend Framework contient déjà tout le nécessaire pour accéder à de nombreuse base de données. Je vais aborder ici son utilisation. La littérature sur la connexion à une base est suffisamment dense pour que je considère que celle-ci est déjà faite. Je vais donc me concentrer sur l’accès aux données et leurs manipulations.

Approche conceptuelle

Une base de données est avant tout un gisement organisé de données structurée. Que ce soit une base de donnée relationnelle ou un annuaire, ou encore un fichier de donnée structurée, on se trouve devant deux notion fondamentales. Une collection et des éléments de cette dernière. Dans une base de données les collections sont appelées Tables et les éléments Rows. Une base de donnée relationnelle peut comporter bien d’autre objets mais dans sont fondement elle est constituée de Tables qui contiennent des Rows. Cette séparation distincte de ces deux éléments et primordiale pour obtenir une conception claire de son application. En effet n ne fait pas les mêmes opérations sur une table que sur un row. En même temps on voit que ces deux types d’objet sont intimement liés.

La représentation Objet

Zend Framwork nous fournit deux classe pour représenter ses éléments. Il s’agit de Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract. Comme leur nom l’indique ces classes étant abstraites, ne sont pas utilisables directement. Pour aller au plus simple ZF fournit aussi un classe Zend_Db_Table dérivant de Zend_Db_Table_Abstract qui par simple instanciation permet de manipuler une table. Cette classe fournit déjà pas mal de possibilités mais on peut constater qu’il n’existe pas de classe pour les row correspondante. Simplement parce que de façon très générales un classe Zend_Db_Table_Row ne comporterait rien de particulièrement intéressent. En effet les opérations que l’on peut faire sur un row dépendant complètement du type de données qu’il contient. Et a priori il peut s’agit de n’importe quoi.

Approche par dérivation

Si on définit des classe abstraite c’est pour qu’elles soient dérivées en classes concrètes. Et si les concepteurs de ZF s’en sont donné la peine ce n’est surement pas pour la beauté de la chose. Que peut nous apporter la dérivation des classes Zend_Db_Table_Abstract et Zend_Db_Table_Row_Abstract ? La première chose que nous apporte la dérivation c’est d’associer une classe drivée de Zend_Db_Table_Abstract à une table particulière de notre base. Par exemple Client_Table lié à la table client de la base. Pour cela il suffit de très peut de chose

Class Client_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'client';
}

Après instanciation de cette classe un appel aux méthodes de consultation de la table remontera des rows de la table client.
Ces dernier étant les objets qu’est sensé manipuler mon modèle. La conception de mon application m’a normalement amené à la manipulation de clients et donc un ensemble de méthode pour parvenir à me fins. Mais ainsi définit ma Client_Table me retourne juste des données. Il serait intéressant qu’elle me donne des objets clients c’est justement ce que permet la dérivation de Zend_Db_Table_Row_Abstract.je peux ainsi définir ma classe

Class Client_Row extends Zend_Db_Table_Row_Abstract {
}

J’ai donc une classe pour mes clients à la quelle je vais pouvoir attacher les méthodes que ma conception ma permit de définir. Reste à associer la classe Client_Table à la classe Client_Row cela se fait très simplement.

Class Client_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'client';
   protected $_rowClass = 'Client_Row';
 }

Je bénéficie maintenant de tous les méthodes de manipulation de ma table définit par ZF et à chaque fois que je récupère un row j’ai entre mes mains un client qui possède toutes les méthodes de manipulation de celui-ci.

le lien inverse

On vient de voir que la classe Client_Table et en relation avec la classe Client_Row en ce sens que tout row sortit de la table est un Client_Row. Mais qu’en est-il dans l’autre sens ? La réponse est La relation existe aussi. Intrinsèquement, la classe Client_Row connait la table qui à donné naissance à ses instances. En clair chaque Client_Row sortit d’une instance de Client_Table contient un référence à cette instance.
Il devient alors simple d’accéder à la table depuis le row. On peut se demander pourquoi. Simplement pour se faciliter la vie.

 //trouver l’enregistrement client d’id 25
$aclient = $clientTable->find(25);
$aClient->maMethode();
$aClient->save();

Ce n’est qu’une facilité on pourrait très bien en repasser par une instance de la table pour faire cette mise à jour. Mais le client ayant sa propre instance de la table pourquoi ne pas en profiter. ZF a d’ailleurs prévu cela et fournit la méthode save et quelques autres.
On le voit il n’est pas bien compliqué d’utiliser la table à partir du row.

Et l’ajout de donnée ?

Si je crée une instance de Client_Row directement je n’aurais pas associé la table à mon nouvel objet. J’aurais bien toutes les méthodes définies mais je ne pourrais mettre mon objet en base sans en passer par une instance de la table. Pour recréer se lient on peut, soit, passer part la méthode setTable, soit par le tableau de paramètre du constructeur. Personnellement j’ai l’habitude de demander mes rows à la Table pourquoi ne pas demander les nouveaux rows à la table.

Class Client_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'client';
   protected $_rowClass = 'Client_Row';

   /**
    * Make new row associated to this table.
    *
    * @param StdClass|array $obj OPTIONAL object to cast
    * @return Fast_Db_Row
    */
    public function newRow($obj = null) {
      if ($obj)
      {
         if (is_object($obj))
            $obj = get_object_vars($obj);
         $row = $this->createRow($obj);
      } else {
         $row = $this->createRow();
      }
      return $row;
   }
 }

Ainsi j’ai le même fonctionnement que précédemment.

//Créer un nouvel enregistrement client.
$aclient = $clientTable-> newRow();
$aClient->maMethode();
$aClient->save();

Est-ce tout ?

Nous venons de voir que cette approche permet de faire un mapping d’une classe issue de ma conception sur les enregistrements d’une table de ma base. ZF offre au passage toute la manipulation des relations. Et toutes les fonctionnalités de manipulation des données sur de la table ajout, mise à jour, suppression. Mais cette approche permet aussi d’enrichir cette couverture déjà riche de méthode spécifique à notre modèle de données. Ainsi la classe Client_Table peut être enrichie de toutes les méthodes qui me sont nécessaire et qui doivent me retourner un Client_Row. J’ai un réceptacle naturel pour toutes les requêtes qui retourne un ou des client_Row mais aussi des données ayant rapport direct avec la table client.
Par exemple la méthode getBoutiqueList qui me retourne la liste des noms de boutique trouve sa place dans Boutique_Table.
Je bénéficie au passage d’un nettoyage automatique des champs de mes objets. Il arrive parfois qu’on associe des valeurs à l’ensemble des données d’un formulaire pour se simplifier la vie. Si je donne tous ses champs de formulaire à la méthode newRow mon objet ne contiendra que les valeurs correspondant à un champ de la table les autres étant supprimé au passage. Notez que si vous utilisez des tableaux de donnée à la place d’objet la méthode createRow est là pour vous. De même pour obtenir le tableau des données d’un objet métier vous avez la méthode toArray. Pour ma part j’ai ajouté la méthode toStdClass pour obtenir un objet standard qui est parfois intéressant (pour la mise en session par exemple).

Restrinction d’accès

Les ACL permettent de restreindre l’accès aux fonctionnalités de votre application. Pour restreindre l’étendu des données il est nécessaire d’en passer par des restrictions sur les requêtes. Il est parfaitement possible d’intégrer cette restriction dans la table elle-même

Class Client_Table extends Zend_Db_Table_Abstract {
   protected $_name = 'client';
   protected $_rowClass = 'Client_Row';
   /**
   * Restriction for query
   * @var string
   */
   protected $_restrict = array('cli_level > 0');
   public function __construct($config = array())
   {
      parent::__construct($config);
      $user = Zend_Auth::getInstance()->getIdentity();
      if ($user) {
         $this->_restrict[] = 'cli_group IN
          (SELECT grp_id FROM group WHERE usr_id = '.$user->usr_id.')';
      } else {
         //sans identité on ne peut rien voir dans la base
         $this->_restrict = 'false';
      }
   }

    /**
     * Fetches one row in an object of type Zend_Db_Table_Row_Abstract,
     * or returns Boolean false if no row matches the specified criteria.
     *
     * @param string|array $where  OPTIONAL An SQL WHERE clause.
     * @param string|array $order   OPTIONAL An SQL ORDER clause.
     * @param boolean $restrict     OPTIONAL use restrict SQL clause.
     * @return Fast_Db_Row  The row results per the
     *     Zend_Db_Adapter fetch mode, or null if no row found.
     */
   public function fetchRow($where = null,
                            $order = null,
                            $restrict = true)
   {
      if ($restrict&&
          isset($this->_restrict)&&
          is_string($this->_restrict))
      {
         if (is_array($where))
         {
            $where[] = $this->_restrict;
         } else {
            $where = '('.$this->_restrict.') AND ('.$where.')';
         }
      } elseif ($restrict&&
                isset($this->_restrict)&&
                is_array($this->_restrict))
      {
         if (is_array($where))
         {
            $where = array_merge($where, $this->_restrict);
         } else {
            foreach ($this->_restrict as $contraint) {
               $where = '('.$contraint.') AND ('.$where.')';
            }
         }
      }
      $res = parent::fetchRow($where,$order);
      return $res;
   }

   /**
    * Make new row associated to this table.
    *
    * @param StdClass|array $obj OPTIONAL object to cast
    * @return Fast_Db_Row
    */
    public function newRow($obj = null) {
      if ($obj)
      {
         if (is_object($obj))
            $obj = get_object_vars($obj);
         $row = $this->createRow($obj);
      } else {
         $row = $this->createRow();
      }
      return $row;
   }
 }

Ainsi lorsqu’on fait appel à une méthode d’extraction de données sur la table la restriction s’applique automatiquement. Notez que j’ai prévu de pouvoir débrailler cette restriction avec le paramètre supplémentaire $restrict cela peut parfois s’avérer utile.

Conclusion

Cette approche conceptuelle permet facilement de coller la conception en classe sur son modèle de donnée. Elle permet un découpage logique de la partie métier de son application. Mais elle ne résous pas tout. Il reste en effet des requêtes qu’on ne sait pas toujours où placer. Mais les possibilités d’extensions de ses classes est grand et laisse libre court à votre imagination.
A+JYT

De la granularité des actions.

Samedi 24 novembre 2007

Le modèle MVC propose de découper son application en actions regroupées dans des contrôleurs. Une question qu’on est amené naturellement à se poser dans ce contexte est : quel est le bon le découpage de l’application en divers contrôleurs. Pour répondre à cette question il existe des méthodes de conceptions qui permettent des découpages logique de l’application en sous ensembles connexes. De ces approches on trouve facilement les contrôleurs qui vont composer l’application. Mais quid des actions qui composent ses contrôleurs ? Quel niveau de granularité adopter pour décider des actions à implémenter ? Si l’on regarde globalement l’application, en imaginant que comme sous MVC nous ayons qu’un seul script nous aurions une espèce d’immense « Switch » pour déterminer quoi faire et dans quelles conditions. Heureusement avec MVC le dispatcher et là pour assurer cet aiguillage. Au niveau de notre contrôleur nous pouvons faire une seule actions qui à sont tour va devoir en fonction du contexte déterminer ce qu’il y a à faire. Nous n’aurons alors peut être pas un gros « Switch », mais probablement un ou plusieurs « If ». On trouve dans les exemples beaucoup de contrôleurs dont le code ressemble à ceci

if ($this->_request->isPost()) {
   $formvar = $f->filter($this->_request->getPost('varname'));
   if (empty($formvar)) {
      $this->view->message = 'Please provide a ...';
   } else {
      ...
      if ($result->isValid()) {
         ...
      } else {
         ...
      }
   }
} else {
...

Dans cet exemple on va utiliser la présence ou pas de donnée de formulaire pour savoir où on en est au cours du processus de gestion d’un formulaire. Dans le cas où on à des données on va alors tenter de faire le nécessaire, et dans le cas contraire afficher le formulaire pour la saisie. À ce moment la on va devoir déterminer si on est en cours d’une édition où d’un ajout etc.
Au final suivant la complexité de l’objet à traiter on va se retrouver avec un gros paquet de « if » imbriqués. Ne pourrait-on pas tout comme on l’a fait au niveau général pour découper l’application en contrôleurs, découper la gestion de notre formulaire en une succession d’actions ? La réponse est oui évidemment. Mais dans ce cas quand s’arrêter ? Car si je commence à découper le gros algorithme de gestion de mon formulaire en plus petits bouts, je peux très bien découper et découper encore jusqu’à obtenir une seule instruction pas actions ce qui deviendrait plutôt compliqué. Là encore il faut trouver un juste milieu.

Une action atomique.

Avec le temps et l’expérience, j’en suis arrivé à la même conclusion que pour le découpage en contrôleur. Le bon niveau est celui qui permet d’avoir un découpage logique et connexe. Reprenons l’exemple de la gestion d’un formulaire. Au niveau général on peut découper le processus de gestion de la façon suivante :

  • Préparer le formulaire
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données
  • Voilà déjà un découpage qu’on peut faire et qui peut être déporté vers des actions différentes. La préparation consiste soit à récupérer l’enregistrement à éditer soit à pré remplir un enregistrement en vu d’un ajout, l’affichage ne fait que gérer la vue, la vérification récupère les données du formulaire et vérifie quelles sont acceptable. Et traiter les données sera par exemple l’enregistrement en base. Logiquement on voit que les actions Préparer, Vérifier et traiter n’affiche rien, elles se terminent donc par un « rediect » vers une autre action. Seule l’action Afficher utilise une vue. Peut-on aller plus loin, ou avons-nous atteint un niveau logique unitaire ? En clair chaque action ainsi déterminé fait-elle une action unique ou doit elle encore déterminer par rapport à son contexte des traitements différents à effectuer ?
    Si on regarde de plus près on voit que l’action préparer fait soit une édition soit un pré remplissage. On peut donc la couper en deux. Les autres n’ont pas de choix à faire autre que ceux induit par les données. Ses choix ne relève pas de la logique d’enchaînement mais des données traitées. Ainsi la vérification va faire un « redirect » soit vers le traitement soit vers l’affichage suivant que les données sont valides ou pas. On peut donc affiner se découpage ainsi :

  • Préparer formulaire pour un ajout
  • Rechercher les données de l’enregistrement à éditer
  • Afficher le formulaire
  • Vérifier les données du formulaire
  • Traiter les données
  • À ce niveau là on a un découpage logique du traitement d’un formulaire. Il faut noter qu’on travaille sur un contrôleur qui doit traiter un formulaire. Donc il n’est pas question d’aborder le traitement du métier, ni la façon de coder la vue. On peut aussi remarquer qu’en agissant ainsi on a déterminé un algorithme général de traitement des formulaires.

    Tout cela sert-il à quelque chose ?

    On légitimement se poser la question. Rien ne m’empêche de faire cinq méthodes privées appelées par une seule action pour arriver au résultat. J’aurais un découpage clair et je n’aurais qu’une action. L’action contiendra alors un aiguillage vars la fonction à effectuer. Les enchaînements de fonction se faisant simplement. Or le dispatcher de MVC sert justement à faire les aiguillages. En reportant ses fonctions vers des actions on va utiliser le dispatcher plutôt que d’écrire l’aiguillage. En imaginant qu’une autre partie de l’application ait besoin que d’une partie de ses fonctions il suffira d’enchaîner vers celle-ci pour obtenir le résultat. Si une nouvelle étape intermédiaire (une confirmation par exemple) venait à devoir être insérer il suffirait de changer les redirections. Bref en passer par les actions va permettre un de ne pas écrire de code pour aiguiller les actions, le dispatcher le faisant très bien, et au passage ajouter de la souplesse dans l’évolution de l’application.
    Mais il y a un autre petit avantage. C’est que finalement il est possible d’écrire les enchaînements sans savoir quels objets un traite. Je n’ai à aucun moment discourus sur l’objet que traitait mon formulaire. Or je peux déjà écrire le contrôleur et ses enchaînements.

    Et la transmission des données

    Reste qu’à changer d’action, j’ai introduit une complexité nouvelle. En effet lorsque dans une fonction je passe les valeurs issues d’une fonction à une autre je n’ai pas de problème. Lorsque je passe d’une action à une autre je perds toutes mes variables. En effet une redirection est un nouvel appel au contrôleur. Je vais devoir utiliser la session pour les conserver. De même lorsque je n’ai qu’une action pour faire afficher un message, je le mets dans la vue. Lorsque j’en ai plusieurs il me faut les garder dans la session.

    Un prototype de contrôleur

    Je vais ici donner un prototype d’un tel contrôler. Je reviendrais dessus dans un article ultérieur car on va voir que la gestion des messages et de la session peut être rendu générique pour toute l’application. Ce qui fera l’objet d’un article prochain.

    <?php
    class FormController extends Zend_Controller_Action
    {
       /**
        * Affiche la liste des éléments
        *
        * @return null
        */
       public function showListAction(){
          $messenger = new Zend_Session_Namespace('messenger');
          $this->view->list = $this->model->getObjectList();
          $this->view->messages = $messenger;
          unset($messenger); //on supprime les messages ils sont affiché on en a plus besoin en session
       }
    
       /**
        * Prépare un enregistrement pour l'afficher dans le formulaire
        * redirige vers showForm
        * @see showForm
        */
       public function addAction() {
          $context = new Zend_Session_Namespace('context');
          $messenger = new Zend_Session_Namespace('messenger');
    
          $context->returnPath = '/FormController/showList';
          $context->saveMethod = 'add';
          // Demander au model un nouvel enregistrement avec les valeurs par défaut
          $context->formData = $this->model->newObject();
          $this->_redirect('/FormController/showForm');
       }
    
       /**
        * Recherche l'enregistrement pour l'afficher dans le formulaire
        * redirige vers showForm
        * @see showForm
        */
       public function editAction() {
          $context = new Zend_Session_Namespace('context');
          $messenger = new Zend_Session_Namespace('messenger');
    
          $context->returnPath = '/FormController/showList';
          $context->saveMethod = 'update';
    
          $id = $this->_request->get('id');
          $context->formData = $this->model->getItemById($id);
          if (!$context->formData) {
             // revenir au point de retour
             $messenger = 'no object for: '.$id;
             $redirect = $context->returnPath;
          } else {
             $redirect = '/FormController/showForm';
          }
          $this->_redirect($redirect);
       }
    
       /**
        * Affiche le formulaire d'édition d'un élément.
        *
        * @return null
        */
       public function showFormAction(){
          $context = new Zend_Session_Namespace('context');
          $messenger = new Zend_Session_Namespace('messenger');
    
          $this->view->cancelAction = $context->returnPath;
          $this->view->saveAction = '/FormController/checkForm/';
    
          $this->view->form = clone($context->formData);
          $this->view->messages = $messenger;
          unset($messenger); //on supprime les messages ils sont affiché on en a plus besoin en session
       }
    
       /**
        * Récupère les données du formulaire les filtres et les vérifie
        * redirige vers save si le formulaire est valide showFrom sinon
        * @see save
        */
       public function checkFormAction() {
          $context = new Zend_Session_Namespace('context');
          if ($context->formData = $this->_request->get('form'))
          // vérification
          $ok = true;
          if ($ok) {
             $redirect = '/FormController/save';
          } else {
             $messenger = new Zend_Session_Namespace('messenger');
             $messenger = 'invalid datas';
             $redirect = '/FormController/showForm';
          }
          $this->_redirect($redirect);
       }
    
       /**
        * enregistre l'élément dans la collection
        * redirige vers l'action qui précédé l'action add ou edit en cas de succès
        * vers showForm en cas d'échec
        * @see add
        * @see edit
        * @see showForm
        */
       public function saveAction($perform = true) {
          $context = new Zend_Session_Namespace('context');
          $messenger = new Zend_Session_Namespace('messenger');
    
          $data = $context->formData;
          $method = $context->saveMethod;
    
          $ok = $this->model->saveObject($data, $method);
          if ($ok) {
             $messenger = 'data saved';
             $redirect = $context->returnPath;
          } else {
             $messenger = 'error data could not be saved';
             $redirect = '/FormController/showForm';
          }
          $this->_redirect($redirect);
       }
    }

    On voit ici un des avantages de procéder ainsi. Le contrôleur peut être rendu générique. On pourrait en faire une classe abstraite et la dériver en autant de formulaire à traiter. Mais on peut aussi l’utiliser sans traitement pour présenter un prototype de l’application.

    Enfin ce contrôleur tel qu’il est écrit pose un petit problème en effet on met des choses dans la session dans un namespace nommé contexte mais si on utilise plusieurs instance de ce contrôleur on va avoir des conflits, de même avec le messager.
    Je pratique cette approche en php maintenant depuis plusieurs années et elle à montré de gros avantages. Par exemple dans une application les utilisateurs ont plusieurs profils. Et en calquant dans une première version les développements sur ce principe nous avions l’affichage d’une liste d’utilisateur, puis tout le processus d’ajout modification. Puis pour chaque fiche utilisateur un lien vers la liste de ses profils qui lui-même avait c’est enchainement. Une évolution qui n’a quasiment rien coûté fut de reporter le liste des profils de l’utilisateur dans la ficher de ce dernier. Le contenu du showList de ProfileController a été reporté dans showForm de UserController. Il a suffit de changer la valeur de returnPath dans profileController pour que tout soit opérationnel.
    Au fil du temps j’ai automatisé pas mal de choses liées à cette approche. Je vous les détaillerais dans les prochains articles.
    A+JYT

    Utiliser une façace pour accéder au modèle

    Samedi 27 octobre 2007

    Parmi les design patterns que l’on trouve en programmation il en est un qu’on oublie un peu trop à mon avis c’est la notion de façade. Pour bien comprendre que quoi il s’agit un petit exemple sera plus parlant. Imaginé que vous travaillez dans un centre de conception automobile. Votre boulot consiste à concevoir toute la partie commande de la voiture. Vous allez collaborer avec les designers qui conçoivent l’habitacle et le tableau de bord (la vue) et avec les ingénieurs qui conçoivent la partie mécanique (le modèle ou métier). À vous de relier le tout (faire le contrôleur). Si les mécanos commence à vous parler de boulon de six ou de durite d’injection vous allez droit à la crise de nerf. Ce dont vous avez besoin c’est de savoir comment on fait marcher le tout. Par exemple comment accélérer. Que la commande d’accélération, action un régulateur électrique, ouvre un carburateur, ou action un injecteur de gasoil vous importe peut. Vous avez besoin de définir une API pour votre partie mécanique. Cette définition peut se présenter simplement par une documentation. Mais aussi par un point d’entrée unique qui vous cache la complexité du métier. Cela s’appelle une façade.
    Vous ne vous êtes jamais penché sur le problème de savoir comment votre banquier traitait vos chèques. Vous fournissez au guichet le bordereau dûment rempli comme le prévois la façade et ce qu’il se passe après vos importe peu du moment que votre argent va sur votre compte.
    C’est donc une pratique courante qui étrangement se perd lorsqu’on développe des logiciels. Bien souvent on fait ce qu’il y a à faire sans trop se demander s’il ne serait pas opportun d’isoler un sous ensemble derrière une API. En programmation à objet on encapsule ainsi des éléments. Mais rarement on pense que tout un ensemble pourrait être caché par une seule classe. La littérature sur ce sujet est pourtant vaste. Et utiliser une façade à bon escient est un gage pour l’avenir.

    Encore un exemple pour comprendre pourquoi.

    Sur L’A300 les commandes de vols étaient mécaniques. Pour les commandes moteur un levier au centre du poste de pilotage actionnait une série de cames qui finalement jouait sur les commandes du moteur : Directement. Les compagnies demandant à Airbus des moteurs différents il était nécessaire de changer une bonne partie de cette quincaillerie pour adapter le moteur à l’avion. Sur l’A320 l’ensemble des commandes à été dé-corrélées de leur support. Pour adapter un nouveau moteur il suffit alors de redéfinir l’interface.
    Il en va de même de vos développements. Si vous arrivés à bien cloisonner vos sous-ensembles alors en changer une partie n’impliquera que des changements mineurs sur le reste, tant que l’interface ne change pas.

    Utiliser une façade avec MVC

    Le modèle MVC propose une séparation nette et précise dans votre application. Dans Zend Framework, l’interface entre la vue et le contrôleur est bien définie. L’interaction entre la vue et le Modèle est à prohiber. Mais l’interface entre le contrôleur et le modèle est complètement à votre charge. Utiliser une façade comme modèle revient à n’avoir qu’un seul est unique objet à qui le contrôleur peut s’adresser. Comme pour votre banquier (le guichet). Cet objet ne va rien faire lui-même il va juste vous mettre à disposition vos objets métiers. Mais au passage il va vous cacher la complexité de leur organisation. Le contrôleur peut alors s’occuper de son travail la logique applicative, pendant que le modèle lui s’occupe de la logique métier. D’un côté vous parlez de clients de facture, d’automobile, d’avion, de téléphone, de livre et que sais-je d’autre. De l’autre vous parlez de table, de base de données, d’annuaire ldap, de ressources xml, de services web etc.
    Agissant ainsi vous allez être contrait de définir clairement les fonctionnalités que le modèle mets à disposition du contrôleur. Un peu de réflexion sur son travail n’est jamais mauvais en soit. De plus vous allez pouvoir définir toute l’interface sans avoir à l’implémenter. Ainsi le développement du contrôleur pour commencer sans attendre que le modèle soit prêt. Dans un travail en équipe c’est un bon moyen de répartir le travail. Cela permet de mettre en place toute la cinématique de l’application sans qu’elle ne fasse quoi que ce soit. La maitrise ouvrage peut alors la valider. De son côté le métier n’a pas à se préoccuper de savoir comment ses objets seront utilisées il suffit qui réponde à la définition de l’interface. Et enfin en cours de vie de l’application un pant entier peu changer dans le métier sans impacter le reste.
    Un autre point intéressant dans cette c’est que le contrôleur accédant à son modèle au travers de la façade, peu importe le contenu de cette façade, il devient possible tout comme pour la vue de prévoit à l’avance la relation entre le contrôleur et le modèle. Pouvoir accéder à sa vue juste au travers d’un membre et bien pratique. Pourquoi ne pas profiter de la façade et faire de même. Le contrôleur demande les services à son modèle et fournis les valeurs à sa vue.

    <?php
    $this->view->clientList = $this->model->getClientListOfCurrentUser() ; 

    Il serait intéressant de rendre l’attachement du membre model transparent pour le développeur comme l’est l’attachement de la vue. A fin de garder un comportement du framework le plus standard possible, j’ai décidé de rendre cet attachement optionnel et commandé par un paramètre. Comme on l’a vu dans un article précédent.
    J’ai choisi le paramètre : « useModel »

    Une première approche simple

    Définir une classe Model au sein de l’application. Dans /appclication/Model.php,
    Puis dans chaque contrôleur dans la méthode ini on ajoute

          if ($parameters = Fast_Registry::getParameters()) {
             if (parameters->fast->get('useModel', false)) {
                Zend_Loader::loadClass('Model');
                $this->model = new Model();
             }
          }

    Reste au développeur à ajouter les méthodes spécifiques à son métier dans la classe Model. Peu importe ce qu’il mettra dans la méthode getClientListOfCurrentUser si celle-ci réponds au prototype alors le contrôleur vu plus haut sera fonctionnel.
    Mais devoir intervenir sur tous les contrôleurs pour bénéficier de l’attachement du modèle, n’est pas plus pratique que de devoir aller le chercher lorsqu’on en a besoin.
    Un attachement automatique et transparent est plus pertinent. Gardons dans un premier temps la classe model dans l’application et ajoutons un classe d’action générique qui aura pour but de collecter ce qui est commun aux différents contrôleurs. C’est ce que fait déjà Zend avec la classe Zend_Controller_Action. En faisant dériver cette classe de Zend_Controller_Action puis nos contrôleurs de cette classe nous aurons un endroit pour mettre toutes les parties communes.
    Un petit souci : où placer cette classe ? En suivant la nomenclature Zend je serais tenté de l’appeler Application_Controller_Action et elle se trouverait alors dans le dossier /application/controller/ or ce dossier n’existe pas. L’ajouter alourdirait la hiérarchie sans améliorer la lisibilité. Nous aurions cote à cote les dossiers controller et controllers. La différence sémantique entre les deux est subtile. Controller est le dossier contenant les éléments propre au mécanisme de contrôle et controllers est le dossier contenant les contrôleurs de l’application. Pour ne pas alourdir la hiérarchie, j’ai décidé de mettre ma classe dans controllers. Elle s’appellera donc Applications_Controllers_Action. Je déplace les quelques lignes de la méthode ini de mes contrôleurs dans celle de la classe action et je fais dériver mes contrôleurs de la classe Action.

    class IndexController extends Application_Controllers_Action

    Si le paramètre useModel est à true alors tous mes contrôleurs ont le membre model.

    Helpers et autre plugins

    J’avoue sincèrement ne pas avoir eut le temps de me pencher sur cette approche. Il me semble possible de reproduire un comportement similaire de cette façon. Se sera peut être une évolution future.

    Les modules et composants.

    Si j’utilise des modules je vais pouvoir reproduire dans le module le fonctionnement décrit ci-dessus. Mes modules auront alors leur propre façade sur la partie métier qui les concerne. C’est plutôt pas mal. Mais comment faire si un module à besoin de tout ou partie du modèle d’un autre ? S’il utilise que le modèle d’un autre module je peu m’arranger pour qu’il prenne comme façade celle de ce module. Il suffit pour cela de bien définir le chargement de la classe. Mais s’il doit utiliser son propre modèle plus des morceaux de celui d’un autre je suis obligé de recopier une partie de cette façade. Dupliquer du code n’est jamais une bonne chose. Surtout lorsqu’il n’y a aucun changement entre les deux copies. De plus on perd alors l’intérêt d’une façade dont le but est justement de n’avoir qu’un point d’accès.
    Pour répondre à cette problématique tout comme Zend l’a fait pour les vues, j’ai définis un classe modèle générale qui accepte des plugins, qui dans mon cas seront des Composants de modèle, des partie de modèle. Permettant aux contrôleurs ainsi de ne charger que les composants dont ils ont besoin. Chaque composant étant une façade sur un sous ensemble du modèle. Le remplacement d’un composant par un autre est ainsi facilités et l’ajout de composant dans l’application aussi. De plus les modules peuvent toujours définir leurs propres modèles, tout en laissant la possibilité à d’autres de les utiliser.
    Reprenant le fonctionnement décrit plus haut j’ai cette fois définis une classe Action générale. Fast_Controller_Action qui dérive de Zend_Controller_Action. Mes contrôleurs ou Application_Contollers_Action peuvent maintenant dériver de celle-ci. C’est elle qui embarque le chargement de la classe Fast_Model dont une instance est attachée aux contrôleurs. Elle dispose d’une méthode addComponent qui prend comme paramètres le nom d’une classe composant et d’un chemin optionnel. Elle charge alors cette classe et ajoute ses méthodes au modèle. La classe modèle à fournir doit dériver de Fast_Model_Component. Un composant pouvant avoir besoin d’interroger le modèle un membre protégé est disponible.

    Exemple d’utilisation

    Parammeters.ini

    useModel = true

    Classe client

    <?php
    Zend_Loader::loadClass('Fast_Model_Component');
    class Model_Client extends Fast_Model_Component
    {
       public function getClientList($nameStart) {
           …
       }
    }

    Contrôleur

    Zend_Loader::loadClass('Fast_Controller_Action');
    class ClientController extends Fast_Controller_Action
    {
       public function showListAction(){
          //charger le composant model_Client du dossier model de ce module
          $this->model->addComponent('Model_Client' , dirname(dirname(__FILE__)));
          $this->view->clientList = $this->model->getClientList() ;
       }
    }

    Il y a fort à parier que le ClientController devra utiliser ce composant dans la majorité des ses méthodes. Le plus simple est alors de le charger dans la méthode ini. Mais il est parfaitement possible de charger un composant pour tout le module et en même temps d’en avoir un autre qui n’est chargé que pour une action.

    Zend_Loader::loadClass('Fast_Controller_Action');
    class ClientController extends Fast_Controller_Action
    {
       public function ini(){
          //charger le composant model_Client du dossier model de ce module
          $this->model->addComponent('Model_Client' , dirname(dirname(__FILE__)));
       }
       public function showListAction(){
          //charger le composant model_Article du dossier model de ce module
          $this->model->addComponent('Model_Article' , dirname(dirname(__FILE__)));
          $this->view->clientList = $this->model->getClientList() ;
          $this->view->articleList = $this->model->getArticleList() ;
       }
    }

    Notez au passage qu’il est très simple ici de faire un bouchon. Il suffit dans la méthode getClientList de retourner une valeur du type attendue sans brancher réellement celle-ci sur le vrai modèle. Lorsque le modèle est prêt les branchements sont relativement simple puis qu’il n’y a plus à chercher où sont utilisé les objets métier. Ils ne le sont qu’au travers de la façade.
    A+JYT

    Utiliser Le moteur de template de son choix

    Vendredi 26 octobre 2007

    Je fais partit de ces gens qui utilisent un moteur de template. Les raison en sont multiples. La première est qu’il est particulièrement compliqué dans un moteur de template de faire du traitement. Ainsi on n’est pas tenté de faire faire à la vue des choses qui ne lui sont pas dévolue.
    La vue présente un point c’est tout. Au contrôleur de lui fournir la matière. À chacun son boulot. Il est parfaitement possible de faire de même avec le système que propose Zend_Framework. Mais il est tout aussi facile de mettre directement dans sa présentation quelque chose comme

    <?php echo Zend_Db:: getInstance()->getMyUserName() ?>

    Voir des choses encore plus complexes. Si la vue à besoin de myUserName c’est au contrôleur de lui donner. L’évolutivité et la maintenabilité de l’application en dépendent. Je n’ai pas été le premier à me pencher sur ce problème. Et force est de constater que les moteurs de templates on leur aficionados.
    J’utilise le plus fréquemment, le moteur ETS (Easy Template System) c’est le moteur utilisé par Marcopoly entre autre. J’ai aussi usé de Smarty. Les deux sont relativement proches dans leur syntaxe. ETS est un moteur ultra léger (89 521 octets) il n’utilise pas de système de cache. Je développe des applications qui sont très dynamique et dans mon cas le cache est plus un handicap qu’un avantage. Ce n’est pas le cas de toutes les applications et utiliser un cache peut s’avérer efficace.

    Comme je l’ai dis je travaille en équipe et suivant les projets un moteur ou un autre voire pas du tout est plus opportun. Je me suis donc demandé comment intégrer efficacement des moteurs à ZF. La littérature de ZF sur le sujet est plutôt légère et la solution proposée est totalement inefficace. Elle revient à demander au programmeur d’écrire autant de fichier phml comme on le ferait avec ZF sans template. Plus tout autant de templates plus du code pour relier le tout.
    On trouve sur internet quelques implémentations plus probantes. Je dois à un collègue (Patrick Dubois) une première intégration d’ETS dans ZF qui avait l’avantage de ne pas avoir à changer une seule ligne dans son contrôleur. Mais pour y parvenir nous avions fait un méchant hack qui ne me paraissait pas viable. À la suite de cela nous avons trouvé sur internet une intégration de Smarty particulièrement bien faite. Mais qui elle nécessitait de changer le code du contrôleur.
    Je trouvais la solution de Patrick séduisante et celle de Philippe Le Van (KitPage) élégante. Concilier les deux serait une bonne chose. Je dois vous avouer que j’ai déroulé ZF en pas à pas pour arriver à comprendre comment il fonctionnait en interne et particulièrement Zend_View. Après avoir échauffé les neurones. J’espère ne pas en avoir perdu de trop au passage. Je suis enfin arrivé à une solution. Qui a demandé une petite demi-journée de travail de mise au point. Dans la foulée, avec l’aide de Patrick (on pense mieux à deux têtes) nous avons intégré à ZF cinq moteurs dans une demi-journée.

    Notre approche consiste à dériver Zend_View et à remplacer l’instance de cette dernière dans le contrôleur, par la notre. Pour y parvenir il va falloir respecter le travail de Zend. Lors de la phase préparatoire au lancement de l’action de votre contrôleur, ZF va instancier un objet Zend_View. Puis dans votre action vous allez lui indiquer les valeurs à afficher. Éventuellement vous donnez l’ordre à la vue d’effectuer un rendu, puis vous rendez la main au Front contrôleur qui lui va passer au rendu final. Donc si comme moi vous mettez toujours que le minimum dans votre action, votre code se contente de faire des affectations de valeurs dans la vue. N’indiquant pas de rendu dans l’action ZF va demander à la vue de rendre le modèle de vue correspondant au nom du contrôleur et de l’action. La proposition de Philippe Le Van nécessite d’appeler explicitement un rendu dans l’action. Pour ma part je serais content de conserver le fonctionnement par défaut de ZF.

    Include ou main page ?

    Une autre chose que je trouve pratique avec les templates c’est de pouvoir les imbriquer. Avec phtml tel que proposé par ZF pour avoir une application qui a toujours la même apparence et don seul le contenu change, il est nécessaire d’en passer par un include d’un entête et d’un pied de page. Solution simple qui existait bien avant php lui-même. Mais cette approche implique de gérer la cohérence entre ce qui est ouvert dans l’entête et doit être refermé dans le pied. Avec un modèle principal qui inclut le contenu on a un système de boites, qui est homogène. Ainsi tout ce qui est ouvert dans un fichier est fermé dans ce fichier. On peu résumer ces deux approches ainsi

    <?php
    include 'header.phtml';
    <h2>contenu de ma page</h2>
    include 'footer.phtml';

    et

    <html>
    <body><h1>ma belle application</h1>
    < ?php include 'content.phtml'; ?>
    </body>
    </html>

    Incontestablement je préfère l’approche main page, qui comme on le voit peut très bien être mise en œuvre en phtml. Mais on a vu que ZF allait chercher à rendre le fichier controller/action.phtml. Si on adopte l’approche main page, on va être en contradiction avec ZF. Il va falloir adapter la vue pour qu’elle accepte de fonctionner ainsi tout laissant croire au contrôleur qu’elle fonctionne exactement comme une Zend_View.
    En supposant qu’on y parvienne, on a introduit alors un nouveau problème. Comment faire si on a un rendu à faire qui n’utilise pas la même main page, ou qui n’en utilise pas du tout ? De ce côté-là inutile de fouiller la doc de ZF, il n’y a rien car Zend_View ne connais pas cette notion. Il sera alors nécessaire d’introduire une nouvelle fonctionnalité à la vue : setMainTemplate.

    Un exemple Simple

    En tout premier lieu, je vais essayer de faire une vue qui accepte la notion de main template, et qui respecte le fonctionnement de ZF. A savoir pas d’intervention dans le contrôleur et pas d’appel sauvage dans un fichier de script comme le propose ZF. On va garder exactement la même architecture les mêmes fichiers de script que ce que propose ZF mais au lieu d’avoir à inclure l’entête et le pied dans toutes les pages, on fournira un fichier main.phtml
    Première étape : dériver de Zend_View

    <?php
    /**
     *
     * @author Jean-Yves Terrien
     *
     */
    
    Zend_Loader::loadClass('Zend_View');
    class Fast_View_Phtml extends Zend_View

    Prévoyant plusieurs moteurs, il me fallait trouver une façon de les nommer. Se sera Fast_View_Engine. Ces vues ne sont pas les moteur de template mais l’interface entre ZF et le moteur qui lui doit rester inchangé. Il n’est pas question ni de modifier le code d’un moteur ni de modifier le code de ZF
    J’ai dit que je voulais pouvoir écrire : include ‘content.phtml’; or mon content va dépendre du script à rendre. J’ai donc besoin d’une variable dans ma vue pour identifier ce script. J’ai aussi dit que je devais pouvoir changer de main template. Et au passage suivant le moteur mes fichiers ne doivent pas toujours avoir le même type phtml pour php tpl pour Smarty et html pour ETS. La façon d’écrire un include dépend du moteur de template. Je ne me suis pas penché sur tous les moteurs pour mettre en œuvre l’approche main template. Je l’ai préparé dans mes moteurs mais il faut pour smarty et phptal trouver comment on fait un include d’un fichier dont le nom est fournit par une variable. Dans le fichier joint vous trouverez donc des exemple avec ou sans main page en fonction du moteur de template.

       public $_content;
       public $_mainTemplate;
       protected $_suffix = 'phtml';

    Arrivé là il me faut surcharger une seule méthode.

       /**
         * Includes the view script in a scope with only public $this variables.
         *
         * @param string The view script to execute.
         */
       protected function _run()
       {
          // récupère le chemin complet du template demandé
          $name = func_get_arg(0);
          // le template principal est considéré à la racine des templates
          // de l'application (ou du module).
          if (!isset($this->_mainTemplate)) $this->setMainTemplate(dirname(dirname($name)) . '/main.phtml');
          // initialiser ets
          // on indique à la quel est le template à inclure
          $this->_content = $name;
          // rendu de la page
         include $this->_mainTemplate;
       }

    Un copier coller de la méthode _run de Zend_View, une petite adaptation et le tour est joué. On va chercher la main page à la racine des vues. La méthode _run reçoit en argument le chemin complet vers le script à rendre. Il suffit donc de remonter d’un répertoire pour se trouver à la racine des vues soit de l’application soit du module. Pour fixer la main page j’ai dit que j’allais ajouter une méthode setMainTemplate je l’ai donc utilisé. Comme pour la page principale il peut être utile de permettre à qui en aurait besoin de connaitre le suffixe utilisé. J’ajoute donc une méthode getSuffix et pour faciliter l’écriture des scripts avoir un membre qui donne le chemin d’inclusion n’est pas mal non plus. Ne nous privons pas. Voici la classe en entier.

    <?php
    /**
     *
     * @author Jean-Yves Terrien
     *
     */
    
    Zend_Loader::loadClass('Zend_View');
    class Fast_View_Phtml extends Zend_View
    {
       public $_content;
       public $_templatesDir;
       public $_mainTemplate;
       protected $_suffix = 'phtml';
    
       public function setMainTemplate($main)
       {
          $this->_mainTemplate = str_replace(chr(92), '/', $main);
       }
    
       public function getSuffix() {
          return $this->_suffix;
       }
    
       /**
         * Includes the view script in a scope with only public $this variables.
         *
         * @param string The view script to execute.
         */
       protected function _run()
       {
          // récupère le chemin complet du template demandé
          $name = func_get_arg(0);
          // le template principal est considéré à la racine des templates
          // de l'application (ou du module).
          if (!isset($this->_mainTemplate)) $this->setMainTemplate(dirname(dirname($name)) . '/main.phtml');
          // initialiser ets
          // on indique à la vue quel est le template à inclure
          $this->_content = $name;
          $this->_templatesDir = dirname(dirname($name)) . '/';
    
          // rendu de la page
         include $this->_mainTemplate;
       }
    }

    Un exemple de main page

    <html>
    <body><h1>ma belle application</h1>
    <?php include $this->_content; ?>
    </body>
    </html>

    Ajouter la vue au Framework

    Reste maintenant à indiquer au front contrôleur d’utiliser notre vue. ZF n’a rien prévu pour ce remplacement. Impossible de lui indiquer quelle classe utiliser comme vue. Impossible aussi d’ajouter des plugins comme pour les actions. La seule solution instancier la vue soit même et la donner au contrôleur. Cela se passe au démarrage.

    Zend_Loader::loadClass('Fast_View_Phtml');
    self::$_instance->_view = new $'Fast_View_Phtml' ();
    $suffix = self::$_instance->_view->getSuffix();
    $viewManager = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
    $viewManager->setView(self::$_instance->_view)->setViewSuffix($suffix);

    J’ai justement prévu dans mon front contrôleur la possibilité d’ajouter des éléments de ce type en fonction de la configuration. J’ai donc ajouté une entrée dans mon fichier de paramètre et l’ai ajouté le nécessaire dans mon front contrôleur.

       public static function setViewEngine ($name = null) {
          $className = ucfirst($name);
          if (null == $className) {
             $className = 'Zend_View';
             $suffix = 'phtml';
          } else {
    
             if (substr($className, 0, 10) != 'Fast_View_') {
                $className = 'Fast_View_' . $className;
             }
          }
          try {
             Zend_Loader::loadClass($className);
             self::$_instance->_view = new $className();
             self::$_instance->_templateEngine = strtolower($name);
             if (!isset($suffix)) $suffix = self::$_instance->_view->getSuffix();
             $viewManager = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
             $viewManager->setView(self::$_instance->_view)->setViewSuffix($suffix);
          } catch (Exception $e) {
             Zend_Loader::loadClass('Fast_Exception_View');
             throw new Fast_Exception_View('Invalid View Engine: '. $className);
          }
       }

    Toujours une exception typée pour mieux gérer les problèmes et au passage un petit membre privé qui conserve le nom du moteur ça mange pas de pain.
    Et dans la méthode run

          if ($parameters&&$config) {
             $engine = $parameters->fast->get('templateEngine', null);
             $controller = self::getInstance();
             Fast_Controller_Front::setViewEngine($engine);

    C’est fini. Ma Fast_View_Phtml remplace Zend_View et utilise main.phtml. Les fichiers touchées sont donc parameters.ini pour le paramètre ‘templateEngine’, Fast_Controller_Front pour charger la vue, Fast_Exception_View, et Fast_View_Phtml.

    D’autres moteurs

    Maintenant tout est en place.
    Pour ajouter Ets par exemple je crée le fichier Fast_View_Ets que je place dans le dossier library/Fast/View/. Il me faudra aussi garder quelque part le moteur ets lui-même. Je les places tous dans library/TemplatesEngines/
    En gros la vue Ets va préparer le moteur et faire un appel à printt comme Zend_View fait un include pour lancer le rendu. Cette classe aura la charge de gérer tous ce qui relève du paramétrage du moteur. S’il y a besoin de paramètre de configuration elle peut obtenir les valeurs avec Fast_Registry.
    Ets à besoin d’un arbre de valeur. On ajoute donc à la vue un membre pour les stocker : _ets on va utiliser les setters et getters de php pour affecter les valeurs tout comme le fait Zend_View on va aussi fournir la méthode assign qui a ses adeptes et qui existe aussi dans Zend_View. Et c’est dans la méthode run qu’on va faire le gros du travail.

       /**
         *
         * @param string The view script to execute.
         */
       protected function _run()
       {
          // récupère le chemin complet du template demandé
          $name = func_get_arg(0);
          // le template principal est considéré à la racine des templates
          // de l'application (ou du module).
          if (!isset($this->_ets->_mainTemplate))
             $this->setMainTemplate(str_replace(chr(92), '/', dirname(dirname($name)) . '/main.html'));
          // initialiser ets
          require_once("TemplatesEngines/Ets/Ets.php");
          // on indique à l'arbre de données d'Ets quel est le template à inclure
          $this->_ets->_content = str_replace(chr(92), '/', $name);
          $this->_ets->_templatesDir = dirname(dirname($name)) . '/';
          // rendu de la page
          printt($this->_ets, $this->_ets->_mainTemplate);
       }

    Elle ressemble à celle de Phtml qui n’est qu’une copie de celle de Zend légèrement amélioré. La différence on charge le moteur (require) et on l’invoque (printt)
    Vous trouverez dans le fichier joint Phtml (phtml script avec main page) Ets, Smarty et Phptal. Ajouter un moteur est devenu quelque chose d’abordable et je vous laisse le loisir d’ajouter le votre.
    Une petite note à propos de Phptal : TAL est le système de template de Zope. Son moteur a été porté dans plusieurs langages. Il n’est pas très gros (en ko) mais il est très structuré au niveau de son code. Mais surtout il utilise des fichiers conforme xhtml la structure XML du document n’est pas altérée. Tous les éléments de tal étant dans un namespace. Du coup tous les templates TAL peuvent être édité par un designer web avec l’outil de son choix. Il est même possible d’embarque dans le modèle des données d’exemples. Imaginé que vous demandez à votre designer en chez de vous faire vos écran et qu’il doive présenter un tableau de donnée s’il l’édite sans aucune valeur dedans il ne pourra se faire une idée du rendu final. Il va donc naturellement mettre des lignes dans sa table. Avec TAL ces ligne ne sont pas gênantes Tal sait en tenir compte pour afficher les valeurs réelles de l’application à lors qu’il y a des lignes d’exemples dans la table. Bref il est très souple, propre et bien structuré. Je vous conseil d’y jeter un œil, même si vous n’envisagez pas l’usage d’un tel système.

    Zend Layout

    Pendant que je faisais cette intégration, Zend Layout est apparu. Je n’ai pas encore eu le temps de me pencher dessus. Mais à première vu cela me parait intéressant. Mais je ne peux en dire plus pour le moment.

    Helper

    Une autre approche est d’utiliser les helpers. Je n’ai pas nom plus eu le temps de tester la chose. Une chose est sure si je le fait je garderais en vue que l’utilisation d’un moteur ne doit en aucun cas nécessiter un changement quelconque dans mes contrôler. Ce n’est pas parce qu’on change de présentation qu’on doit changer la logique de l’application. C’est le principe de fondement de MVC. Et je ne veux pas le remettre en question.

    Pour tester

    A fin de vous permettre de tester je vous ai fait une copie de la chose. il faut pour la rendre fonctionnelle placer la librarie Zend dans le library.
    vous pouvez alors jouer avec les paramettre debug et template dans parameters.ini

    A+JYT

    Le front Contrôleur

    Lundi 22 octobre 2007

    J’ai abordé précédemment la problématique du BootStrap et de la configuration. Pour la configuration l’essentiel est fait. Il sera peut être nécessaire d’adapter un peu la chose. Mais la structure est suffisamment solide pour supporter de telles évolutions. Quant au BootStrap il est resté aussi simple que je le souhaitais. Facile me direz-vous, j’ai botté en touche. J’ai donc un démarrage simplifié, un chargeur de configuration, mais dans tout ça rien qui dise à mon application comment l’utiliser.

    Le chef d’orchestre

    Le front contrôleur est un élément central dans le modèle MVC. C’est lui le grand chef d’orchestre. Son rôle est de distribuer le travail. Il va préparer le travail de l’application, distribuer les activités, vérifier la cohérence du tout et s’assurer que la demande aboutie bien à une réponse correcte. En tant que chef d’orchestre il est le mieux placer pour prendre en compte les paramètres de configuration générale et préparer les éléments nécessaires pour les rendre accessible à l’application.
    Le front contrôleur de Zend_Framework est particulièrement adaptable. Mais pour cela il est nécessaire d’écrire beaucoup de code. Chose que je cherche justement à éviter. Je pourrais écrire mon propre front contrôleur comme je l’ai fait par le passé en php4. Mais celui de ZF est plutôt bien fait et je n’ai pas envie de m’en priver. Comme toujours la programmation par objet permet d’adapter une classe à son besoin. Un petit retour sur le BootStrap vous montrera que finalement vu de ce dernier mon front contrôleur n’est rien d’autre qu’un Zend_Controller_Front. Il ne fera rien d’autre que tenir compte de la configuration avant de lancer l’application.
    Tout ce que vous trouvez comme paramétrage du contrôleur dans le BootStrap parce que dépendant de l’application l’application peut être chargé automatiquement dans la méthode run. Il suffit pour cela de définir un attribut de paramétrage

    Un exemple

    Par exemple l’authentification. Vos applications utilisent une authentification des utilisateurs, d’autre pas. Vous pouvez placer un paramètre auth à true ou false dans le fichier de paramètre. Et dans la méthode run en fonction de sa valeur utiliser un Zend_Auth il est nécessaire pour cela d’avoir les paramètres du système d’authentification. Ajoutons alors une section auth dans le fichier de configuration.
    La méthode run ressemblera alors à ceci

       public static function run($controllerDirectory)
       {
          // choix du moteur de
          $config = Fast_Registry::getConfiguration();
          $parameters = Fast_Registry::getParameters();
          if ($parameters&&$config) {
             $controller = self::getInstance();
             $controller->throwExceptions(true);
             $controller->setControllerDirectory($controllerDirectory);
             $controller->setRequest('Fast_Controller_Request_Http');
             $controller->_setDispatcher('Fast_Controller_Dispatcher');
    
             Zend_Loader::loadClass('Zend_Session');
             Zend_Session::start();
    
             $auth = $parameters->fast->get('auth', false);
             if($auth) {
                Zend_Loader::loadClass('Zend_Db_Table');
                Zend_Loader::loadClass('Zend_Auth');
                if (!$config->get($parameters->fast->db)) {
                   Zend_Loader::loadClass('Fast_Exception_Db');
                   throw new Fast_Exception_Db('No configuration found for database: '.$parameters->fast->db);
                }
                $params = $config->get($parameters->fast->db)->toArray();
                // connexion
                $dbAdapter = Zend_Db::factory($parameters->fast->db, $params);
                Zend_Db_Table::setDefaultAdapter($dbAdapter);
                Zend_Registry::set('dbAdapter', $dbAdapter);
             }

    Etc. encore une fois dès qu’une nouvelle option du front contrôleur est utilisée par une application. La reporter ici permet de la mettre à disposition de toutes les applications futures.
    Vous trouverez dans mon code des classes dont je n’ai pas encore évoqué la fonctionnalité. Supprimez les lignes ou remplacez les part les classes Zend correspondantes. Je reviendrais dessus au fur et à mesure. Elle sont simplement la preuve que l’ont peut capitaliser beaucoup de développements tout en gardant une facilité de mise en œuvre.

    Fast_Controller_Front
    A+JYT

    Gérer la configuration d’une application

    Lundi 22 octobre 2007

    Mon but est de mettre en place une structure générique qui me permette de gérer les configurations des diverses applications que je suis amené à écrire. Le contexte dans lequel je me trouve fait que mes applications ont beaucoup de points communs (elles sont toutes destinées à la même entreprise). Je sais très bien que je ne pourrais mettre en place un système de configuration universel. Mais il doit être suffisamment souple pour accepter la flexibilité nécessaire à la réutilisation.

    Configuration et paramétrage

    Dans l’article précédent je débâtais de la différence entre le paramétrage et la configuration. C’est le modèle alors proposé, que je vais ici tenter de mettre en œuvre.
    J’ai donc trois fichiers : le fichier environment.ini, le fichier parameters.ini et le fichier de configuration désigné par le fichier environnement.
    Si on se reporte au code de l’article précédent dans le BootStrap le chargement de la config se fait par

    <?phpFast_Config::load();

    Ou

    <?phpFast_Config::load("mon fichier d'environnement");

    Le chargeur de configuration

    Au vue de ce code, le chargeur de configuration est donc une classe possédant une méthode statique.
    En regardant de plus près l’application ne doit avoir qu’une seule configuration, au sens opérationnel. C’est-à-dire qu’une instance de l’application tournant à un moment donné ne peut avoir deux configurations différentes. La configuration doit donc au sein de l’application être unique. Une solution est de faire un singleton. Mais ici nous n’avons pas vraiment d’intérêt à créer un objet même unique pour charger la configuration.
    Charger une configuration est purement fonctionnel. Notre chargeur doit prendre en compte plusieurs fichiers mais il ne fait qu’un seul chargement. Je dirais même que c’est typiquement une simple fonction.
    Que faire alors de la configuration chargée ? Zend Framework nous propose une solution : Zend_Registry. La base de registre du framework sert à garder toujours accessible un ensemble de valeur structurée.
    Il n’y a pas de raisons de se priver.
    Chargeur de configuration et lecteur de fichier de configuration.
    ZF nous fournit la classe Zend_Config et quelques dérivées. La classe Zend_Config ne ferait-elle pas l’affaire pour notre chargeur ?
    Pour répondre à cette question il faut se pencher sur sont fonctionnement. Zend_Config est une classe qui permet de lire des fichiers de configurations. Mais elle ne détient pas le mécanisme à trois fichiers que j’ai décrit. Il est parfaitement envisageable de mettre ce mécanisme dans le BootStrap et d’utiliser Zend_Config pour cela.
    Cette approche simple à l’inconvénient d’alourdir le BootStrap et si on y prend garde celui-ci va se retrouver follement encombré par tout un tas de choses qu’il est facile de faire ainsi, jusqu’à le rendre illisible.
    Fast_Config doit donc porter le mécanisme de chargement de la configuration. Ainsi s’il évolue seule cette classe devra changer. Une autre question se pose alors. Fast_Config doit-elle dériver de Zend_Config. Nous venons de voir que leur rôle est fondamentalement différent. Une dérivation n’aurait pas de sens. J’aurais peut-être dû choisir un autre nom. Comme Fast_Config_Loader par exemple. Fast_Config m’est apparu naturel.

    Organiser la base de registre.

    On peut se poser la question de l’organisation de la base de registre. Cette base étant structurée quelle structure adopter. Je pense que pour le commun des mortel retrouver dans une structure la même organisation que celle qu’on à choisit dans les fichiers de configuration est une facilité appréciable, même si d’autre regroupement logiques sont envisageable.
    Ma base de registre contiendra donc trois parties environnement, parameters, et configuration.
    Mon algorithme de chargement sera donc
    Lecture du fichier environnement
    Écriture de l’environnement dans la base.
    Lecture du fichier parameters
    Écriture des paramètres dans la base.
    Lecture du fichier de configuration
    Écriture de la configuration dans la base.
    Je serais bien tenté par une base de registre qui m’offre les services suivants.

    <?php
    Zend_Loader::loadClass('Zend_Registry');
    class Fast_Registry extends Zend_Registry
    {
       public static function getEnvironment();
       public static function setEnvironment($env);
       public static function getConfiguration();
       public static function setConfiguration($config);
       public static function getParameters();
       public static function setParameters($config);
    }

    Ainsi mon chargeur pourrait utiliser les méthodes set et l’application les méthodes get. De plus Fast_Registry dérivant de Zend_Registry, j’ai à disposition toutes les fonctionnalités d’une base de registre.

    Une première approche de Fast_Config

    <?php
    Zend_Loader::loadClass('Fast_Registry');
    class Fast_Config
    {
       /**
          @function load() charge toute la config en fonction du fichier environment.
          @param $environement String|null Nom du fichier d'environnement
          @return Fast_Config le contenu du fichier désigné par la clef environnement.
       */
       public static function load($environement = null)
       {
          // initialise une base de registre
          Zend_Registry::setClassName('Fast_Registry');
          if ($environment == null) {
             $environment = './application/config/environment.ini';
          }
          Fast_Registry::setConfigPath(str_replace(chr(92), '/', dirname($environment).'/'));
          // recherche un fichier de config déterminant l'environnement de travail.
          $config = Fast_Registry::setEnvironment(lireLeFichier($environment));

    La ligne 13 montre comment utiliser sa propre classe comme classe de registre dans ZF.
    Une petite remarque au passage str_replace(chr(92), ‘/’, dirname($environment).’/') Cet appel permet de remplacer tous les \ par des / cela n’est pas bien grave sauf dans quelques cas. Ainsi « c:\real\path » ne sera pas toujours bien compris simplement parce que le \r est un caractère qui sera interprété par php. Il existe bien des solutions pour se sortir de ce problème. J’utilise pour ma part cette expression et je travaille ainsi toujours avec des chemins à la UNIX.
    Revenons à notre code. On le voit une petite méthode pour lire le fichier serait bien venue.
    J’ai dit que j’allais utiliser Zend_Config pour lire le fichier. Un des avantages ce celui-ci est de pouvoir lire différents types de fichiers de configuration .ini .xml etc.
    Ma petite procédure de lecture serait bien pratique si en plus elle prenait divers types de fichier.
    Comme pour la base de registre faisons comme si et écrivons notre méthode.
    La suite à donner est de regarder dans le fichier environnement si nous trouvons bien les informations sur la configuration. Si ce n’est le cas inutile de continuer nous avons là un crash de l’application.

    <?php
    Zend_Loader::loadClass('Fast_Registry');
    class Fast_Config
    {
       /**
          @function load() charge toute la config en fonction du fichier environment.
          @param $environment String|null Nom du fichier d'environnement
          @return Fast_Config le contenu du fichier désigné par la clef environnement.
       */
       public static function load($environment = null)
       {
          // initialise une base de registre
          Zend_Registry::setClassName('Fast_Registry');
          if ($environment == null) {
             $environment = './application/config/environment.ini';
          }
          Fast_Registry::setConfigPath(str_replace(chr(92), '/', dirname($environment).'/'));
          // recherche un fichier de config déterminant l'environnement de travail.
          $config = Fast_Config::getFile('setEnvironment', $environment);
          if(!isset($config->main->environment)) {
             Zend_Loader::loadClass('Fast_Exception_Config');
             throw new Fast_Exception_Config('Environment non défini.');
          } else {
             $environmentFile = Fast_Registry::getConfigPath().$config->main->environment;
          }

    Inutile de chercher une échappatoire s’il n’y a pas de config on plante tout. Une exception est parfaite pour cela.
    À l’appelant du charger de trapper l’exception et de traiter le problème. Le chargeur lui ne peut rien.
    Là encore vous pouvez constater que j’utilise une Fast_Exception_Config, je pourrais très bien utiliser une Zend_Exception. Mais utiliser des exceptions typées permet de les traiter plus facilement. Je vous conseille de toujours typer les exceptions. Pour cela il suffit de fournir une classe qui dérive de Zend_Exception.
    Voici non pas la mais les miennes.
    class Fast_Exception extends Exception{}
    class Fast_Exception_Config extends Exception{}
    Un gestionnaire d’exception peut ainsi trapper les exceptions en général, trapper les exceptions fast, trapper les exceptions de configuration fast et apporter un traitement approprié à chaque cas.

    Fast_Config

    À ce stade j’ai le nom du fichier de configuration qui m’a été fourni par la clef environnement dans la section main du fichier environnement.ini
    Je serais bien tenté de faire pareil avec les paramètres laissant ainsi un peu de liberté à l’application.

    <?php
    Zend_Loader::loadClass('Fast_Registry');
    class Fast_Config
    {
       /**
          @function load() charge toute la config en fonction du fichier environment.
          @param $environment String|null Nom du fichier d'environnement
          @return Fast_Config le contenu du fichier désigné par la clef environnement.
       */
       public static function load($environment = null)
       {
          // initialise une base de registre
          Zend_Registry::setClassName('Fast_Registry');
    
          if ($environment == null) {
             $environment = './application/config/environment.ini';
          }
    
          Fast_Registry::setConfigPath(str_replace(chr(92), '/', dirname($environment).'/'));
    
          // recherche un fichier de config déterminant l'environnement de travail.
          $config = Fast_Config::getFile('setEnvironment', $environment);
    
          if(!isset($config->main->environment)) {
             Zend_Loader::loadClass('Fast_Exception_Config');
             throw new Fast_Exception_Config('Environment non défini.');
          } else {
             $environmentFile = Fast_Registry::getConfigPath().$config->main->environment;
          }
    
          if(!isset($config->main->parameters)) {
             Zend_Loader::loadClass('Fast_Exception_Config');
             throw new Fast_Exception_Config('Parameters non défini.');
          } else {
             $parametersFile = Fast_Registry::getConfigPath().$config->main->parameters;
          }
          // recherche un fichier de configuration générale de l'application.
          $config = Fast_Config::getFile('setConfiguration', $environmentFile);
          // recherche un fichier de parametres généraux de l'application.
          $params = Fast_Config::getFile('setParameters', $parametersFile);
          return $config;
       } // end function load
    }

    Me reste à définir la méthode getFile qui va déterminé le type de fichier lire le contenu et le mettre dans la base de registre avec la méthode spécifiée.
    Vous trouverez le tout dans le fichier join.
    Au passage il serait intéressant d’activer certains éléments de la configuration immédiatement. Pour en bénéficier pendant le BootStrap

    <?php      // affichage des erreurs
          $display_errors = $config->debug->get('display_errors', false);
          if (ini_get('display_errors') != $display_errors) {
             // pas d'ini_set inutile, c'est gourmand !
             ini_set('display_errors', $display_errors);
          }
          // niveau d'erreur (ne pas les afficher n'empęche pas de les loguer)
          // avec un niveau raisonnable d'alarme par défaut en prod
          // le niveau peut ętre indiqué avec des opérateurs (ex. E_ALL ^ E_STRICT)
          $error_reporting = $config->debug->get('error_reporting', E_WARNING);
          if (!is_numeric($error_reporting)) {
             eval("error_reporting($error_reporting);");
          } else {
             error_reporting($error_reporting);
          }

    Placé jute avant le return $config; ce permet d’avoir les messages d’erreur dès le démarrage lors du développement. Et de les retirer en production juste en modifiant le fichier de configuration.

    Les fichiers de config

    Environnement.ini

    ; Définit le fichier de configuration générale à charger au bootstrap.
    ; Ce fichier varie généralement selon que l'on est en développement, en recette ou en production.
    ; Exemple :
    ; [main]
    ; environment = dev.ini
    ; parameters = parameters.ini
    
    [main]
    environment = dev.ini
    parameters = parameters.ini

    Parameters.ini

    [fast]
    ; debug true ou false charge la classe Fast_Debug absent pas de classe
    ; auth active le la protection
    debug = true
    db = Pdo_Mysql
    auth = true
    audiance = false
    menu = true
    version = G0-RC1
    
    [login_messages]
    login_need = l'identifiant est obligatoire
    pass_need = le mot de passe est obligatoire
    unknow = Identifiant ou mot de passe inconnu.
    unknow_user = Veuillez vous identifier avant de poursuivre.

    Dev.ini

    [debug]
    error_reporting = E_ALL ^ E_STRICT
    display_errors = true
    
    [app]
    baseUrl = /myApp/
    
    [Pdo_Mysql]
    host = 127.0.0.1
    port = 3307
    username = sekaijin
    password =
    dbname = test

    Note :
    Vous trouverez dans les fichiers joint des appels à d’autres classes. Je les ai ajoutées au fil du temps. Et je reviendrais dessus plus tard.
    A+JYT

    Zend_Framework BootStrap

    Dimanche 21 octobre 2007

    Zend_Framework à besoin pour démarrer de quelques petites choses, comme, par exemple le chemin où trouver les contrôleurs ou encore les vues. Et bien d’autres encore.

    Démarrage à froid

    On trouve sur le net nombre d’exemples de BootStrap pour ZF qui explique comment ajouter telle ou telle option au démarrage.

    Par exemple : celui de Simon Mundy et Alain Sahli dans leur tutoriel sur la mise en œuvre des ACL developpez.com

    <?php
    // Initialisation de la configuration / environnement
    $config = new Zend_Config(new Zend_Config_Ini('../application/config/config.ini', 'live'));
    
    // Création du sitemap à partir du .ini en utilisant la structure de l'exemple
    $sitemap = new Zend_Config(new Zend_Config_Ini('../application/config/sitemap.ini', 'live'));
    
    // Création de l'objet de base de données et activation / désactivation du débogage
    $db = Zend_Db::factory($config->db->connection, $config->db->asArray());
    ...etc...
    
    // Création de l'objet Auth
    $auth = Zend_Auth::getInstance();
    
    // Création de l'objet Acl
    $acl = new MyAcl($auth); // see
    
    // Création du routeur et configuration (Ordre LIFO pour les routes)
    $router = new Zend_Controller_RewriteRouter;
    ...add rules...
    
    // Création des vues et enregistrement des objets
    $view = new My_View;
    ...init view...
    
    $front = Zend_Controller_Front::getInstance();
    $front->throwExceptions(true);
    $front->setRouter($router)
          ->setDispatcher(new Zend_Controller_ModuleDispatcher())
          ->registerPlugin(new My_Plugin_Auth($auth, $acl))
          ->registerPlugin(new My_Plugin_Agreement($auth))
          ->registerPlugin(new My_Plugin_View($view))
          ->setControllerDirectory(array('default' => realpath('../application/controllers/default'),
                                         'admin' => realpath('../application/controllers/admin')))
          ->setParam('auth', $auth)
          ->setParam('view', $view)
          ->setParam('config', $config)
          ->setParam('sitemap', $sitemap)
          ->dispatch();
    

    Si je m’en réfère à la documentation de ZF le BootStrap est on ne peut plus simple

    <?php
    require_once 'Zend/Controller/Front.php';
    Zend_Controller_Front::run('/chemin/vers/application/controllers');

    Cette simplicité est séduisante, mais elle ne permet pas de personnaliser le démarrage.

    Et devoir reproduire quelque chose d’aussi complexe que l’exemple ci-dessus est source d’erreurs.

    En passer par une config

    C’est pourquoi j’ai envisagé d’en passer par un fichier de configuration. Le BootStrap se résumant alors à charger la config, charger le front controller et le démarrer.

    <?php
    /**
     * Bootstrap.
     *
     * Point d'entrée unique de l'application, ce script fixe l'environnement
     * de travail par rapport aux fichiers de configuration disponibles et
     * lance un Front Controller qui est le moteur principal de l'application
     * qui tourne sous le design pattern MVC tel qu'implémenté par
     * Zend Framework.
     */
    
    // chemin des librairies
    set_include_path(
    realpath('library')
    . PATH_SEPARATOR . realpath('application')
    . PATH_SEPARATOR . get_include_path()
    );
    // charge le loader général.
    require 'Zend/Loader.php';
    Zend_Loader::loadClass('Fast_Config');
    Fast_Config::load(); //ou Fast_Config::load("mon fichier d'environement");
    
    // instancie un front contrôleur
    // et lui indique le chemin des contrôleurs d'action
    Zend_Loader::loadClass('Zend_Controller_Front');
    Zend_Controller_Front::run('application/controllers');
    

    Notre BootStrap reste relativement simple et portable d’une application sur l’autre les variations des options étant définie par la configuration.

    Reste que Zend_Controller_Front ne sait pas utiliser mon Fast_Config. De plus, un fichier de configuration peut facilement devenir aussi compliqué que d’écrire un BootStrap.

    Organiser la config.

    Dans mon travail, mes développements se font de la façon suivante le projet est géré avec CVS ou SVN, chaque développeur a une copie de l’application, et l’exécute sur un environnement qui lui est propre. L’application passe ensuite, sur une plateforme de développement commune qui est identique à la plateforme cible. Cette dernière permet de tester les relations avec les autres applications lorsque cela est nécessaire. Ensuite l’application est déployée sur une plateforme de recette. Celle-ci est destinée à la maitrise d’ouvrage qui va valider l’application. Très souvent, l’étape suivante est une mise en près production. Don le but est de tester l’application en situation. Et, au final, elle est enfin placée sur son environnement de production. Il va sans dire qu’immanquablement tous ses environnements ne sont pas parfaitement identiques. La configuration de l’application va donc en dépendre pour partie. À l’opposé, il existe toujours dans une application des paramètres qui ne sont pas liés à l’environnement. Pour ne pas avoir à systématiquement rééditer un gros fichier de configuration, j’ai pris l’habitude d’en faire plusieurs. En premier lieu, séparer ce qui est du paramétrage de l’application de ce qui est de la configuration due à l’environnement.

    J’ai donc un fichier parameters qui définit les paramètres de l’application, et un fichier de configuration. Ce dernier existe en autant d’exemplaires que de plateforme ou presque. Une solution pour sélectionner le bon est de le nommer config et de renommer les autres qui pourraient éventuellement traîner par là. L’inconvénient de cette approche est que lorsqu’on voit dans le dossier config le fichier config on ne sait pas à quel environnement il se rapporte. J’ai donc choisis de les nommer en fonction de l’environnement. dev, recette,preprod, prod, etc. Reste donc à indique au BootStrap lequel choisir. J’ai pour cela défini un fichier qui ne fait que ça. Environnement.ini

    Si vous regardez la hiérarchie que j’ai choisie dans l’article précédent, il faut déterminer où placer ces fichiers. Pour moi il s’agit de la configuration de l’application. J’ai donc ajouté un dossier config dans le dossier application. Dans lequel je place les fichiers environnement.ini, parameters.ini et dev.ini etc. Le dossier application étant déjà protégé la configuration n’est normalement pas accessible pour le client.

    Comment déterminer ce que l’on place dans parameters et ce qui va dans dev ou autre. Il suffit pour cela de se poser la question l’attribut de configuration doit-il changer si je change de plateforme. Par exemple l’option « utiliser les acl » ne dépend pas de l’endroit où l’on place l’application. C’est un paramètre. « database host » quant à lui dépend de la plateforme il va donc dans la config.

    Ainsi le passage d’une plateforme à l’autre se résume à créer un fichier config propre à la plateforme et à changer la valeur de environnement.ini.

    Cette façon de faire n’est peut-être pas la meilleure, mais elle a fait ses preuves.

    Apprendre au Front_Controller à utiliser la config.

    Améliorer le FrontController

    La seule approche que permet par défaut ZF pour configurer le Front Controller est d’en passer par le BootStrap. Mais ZF est une belle architecture à objet qui autorise l’héritage et le polymorphisme. Je vais me servir de cette caractéristique pour construire un Front_Controller paramétrable par la configuration.

    Fast_Controler_Front va donc dériver de Zend_Controller_Front est fournir les mêmes fonctionnalités, mais, au passage, il va utiliser la configuration pour ajouter seul, les options demandées.

    Pour cela, il suffit de surcharger la méthode statique run

    <?php
    require_once 'Zend/Loader.php';
    Zend_Loader::loadClass('Zend_Controller_Front');
    
    /**
     * Contrôleur principal permettant l’utilisation de la configuration.
     *
     * @author Patrick Dubois
     * @author Jean-Yves Terrien
     *
     * @uses Zend_Controller_Front
     * @package Fast_Controller
     */
    class Fast_Controller_Front extends Zend_Controller_Front
    {
    /**
         * Convenience feature, calls setControllerDirectory()->setRouter()->dispatch()
         *
         * In PHP 5.1.x, a call to a static method never populates $this -- so run()
         * may actually be called after setting up your front controller.
         *
         * @param string|array $controllerDirectory Path to Zend_Controller_Action
         *   controller classes or array of such paths
         * @return void
         * @throws Zend_Controller_Exception if called from an object instance
         */
       public static function run($controllerDirectory)
       {
          $config = Fast_Registry::getConfiguration();
          $parameters = Fast_Registry::getParameters();
          if ($parameters&&$config) {
            …
          } else {
             Zend_Controller_Front::run('application/controllers');
          }
    
       }
    }
    

    Ainsi dans notre BootStrap nous pouvons garder un code simplifié, il suffit d’utiliser Fast_Controller_Front à la place de Zend_Controller_Front.

    <?php
    /**
     * Bootstrap.
     *
     * Point d'entrée unique de l'application, ce script fixe l'environnement
     * de travail par rapport aux fichiers de configuration disponibles et
     * lance un Front Controller qui est le moteur principal de l'application
     * qui tourne sous le design pattern MVC tel qu'implémenté par
     * Zend Framework.
     */
    
    // chemin des librairies
    set_include_path(
    realpath('library')
    . PATH_SEPARATOR . realpath('application')
    . PATH_SEPARATOR . get_include_path()
    );
    // charge le loader général.
    require 'Zend/Loader.php';
    Zend_Loader::loadClass('Fast_Config');
    Fast_Config::load(); //ou Fast_Config::load("mon fichier d'environement");
    
    // instancie un front contrôleur
    // et lui indique le chemin des contrôleurs d'action
    Zend_Loader::loadClass('Fast_Controller_Front');
    Fast_Controller_Front::run('application/controllers');
    

    Notez que j’ai pris la précaution de lancer un Zend_Controller_Front, si je n’ai pas de config. Permettant ainsi de garder le fonctionnement par défaut de ZF.

    Je n’ai abordé ici que la structure générale. La mise en œuvre fera l’objet d’une publication future.