Archive pour novembre 2007

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