世界人のBlog

Les expériences Zend de Sekaijin

Auto-Jointure

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)

  1. /**
  2.  * Definition de base d’une table Fast
  3.  * elle étend la classe Zend_Table et lui adjoint un classe spécifique pour les enregistrement
  4.  * ainsi que les méthode courantes d’accès au données
  5.  *
  6.  * @see  Zend_Db_Table
  7.  * @see  Fast_Exception_Db
  8.  * @see  Fast_Db_Row
  9.  * @author Jean-Yves Terrien
  10.  */
  11. Class Fast_Db_Table extends Zend_Db_Table_Abstract {
  12.  
  13.    const FAST_RESTRICT = ‘fast_restrict’;
  14.    const FAST_AUTOJOIN = ‘fast_autojoin’;
  15.    /**
  16.    * Classname for select , Zend_Db_Table_Select,…
  17.    *
  18.    * @var string
  19.    */
  20.    protected $_selectClass = ‘Fast_Db_Table_Select’;
  21.    /**
  22.    * Restriction for query
  23.    *
  24.    * @var string
  25.    */
  26.    protected $_restrict = null;
  27.    /**
  28.    * Auto Joined table for query
  29.    *
  30.    * @var string
  31.    */
  32.    protected $_autojoin = NULL;
  33.    /**
  34.     * Returns an instance of a Zend_Db_Table_Select object.
  35.     *
  36.     * @return Zend_Db_Table_Select
  37.     */
  38.    public function select()
  39.    {
  40.       if (‘Zend_Db_Table_Select’ == $this->_selectClass) {
  41.          $select = parent::select();
  42.       } else {
  43.          Zend_Loader::loadClass($this->_selectClass);
  44.          $select = new $this->_selectClass($this);
  45.       }
  46.       return $select;
  47.    }
  48.     /**
  49.      * Returns table information.
  50.      *
  51.      * @return array
  52.      */
  53.     public function info()
  54.     {
  55.         $info = parent::info();
  56.         $info[self::FAST_RESTRICT] = $this->_restrict;
  57.         $info[self::FAST_AUTOJOIN] = $this->_autojoin;
  58.         return $info;
  59.     }
  60. }

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

  1. class Fast_Db_Table_Select extends Zend_Db_Table_Select
  2. {
  3.    private $_autojoined = false;
  4.    private $_useRestrict = true;
  5.    /**
  6.    * Performs a validation on the select query before passing back to the parent class.
  7.    * Ensures that only columns from the primary Zend_Db_Table are returned in the result.
  8.    *
  9.    * @return string This object as a SELECT string.
  10.    */
  11.    public function __toString()
  12.    {
  13.       if (!$this->_autojoined) {
  14.          $this->_autojoined = true;
  15.  
  16.          $fields  = $this->getPart(Zend_Db_Table_Select::COLUMNS);
  17.          $primary = $this->_info[Zend_Db_Table_Abstract::NAME];
  18.          $schema  = $this->_info[Zend_Db_Table_Abstract::SCHEMA];
  19.  
  20.          // If no fields are specified we assume all fields from primary table
  21.          if (!count($fields)) {
  22.             $this->from($primary, ‘*’, $schema);
  23.             $fields = $this->getPart(Zend_Db_Table_Select::COLUMNS);
  24.          }
  25.  
  26.          if ($this->_useRestrict) {
  27.             if (is_string($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
  28.                $restricts[] = $this->_info[Fast_Db_Table::FAST_RESTRICT];
  29.             } elseif (is_array($this->_info[Fast_Db_Table::FAST_RESTRICT])) {
  30.                $restricts = $this->_info[Fast_Db_Table::FAST_RESTRICT];
  31.             } else {
  32.                $restricts = array();
  33.             }
  34.             foreach ($restricts as $restrict) {
  35.                $this->where($restrict);
  36.             }
  37.          }
  38.          if (is_array($this->_info[Fast_Db_Table::FAST_AUTOJOIN])) {
  39.             $this->setIntegrityCheck(false);
  40.             foreach ($this->_info[Fast_Db_Table::FAST_AUTOJOIN] as $join) {
  41.                if (is_array($join)) {
  42.                   $this->join($join[‘table’], $join[‘on’], $join[‘fields’]);
  43.                }
  44.             }
  45.          }
  46.       }
  47.       return parent::__toString();
  48.    }
  49.    /**
  50.       @function setRestrict()
  51.       @param boolean $restrict Use restriction for this select
  52.       @return Fast_Db_Table_Select Description
  53.    */
  54.    function setRestrict($restrict) {
  55.       $this->_useRestrict = $restrict;
  56.       return $this;
  57.    } // end function setRestrict
  58. }

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.

  1. Class Adm_Model_User_Table extends Fast_Db_Table {
  2.    /**
  3.    * The table name.
  4.    *
  5.    * @var array
  6.    */
  7.    protected $_name = ‘user’;
  8.  
  9.    /**
  10.    * Classname for row
  11.    *
  12.    * @var string
  13.    */
  14.    protected $_rowClass = ‘Adm_Model_User_Row’;
  15.    /**
  16.    * Auto Joined table for query
  17.    *
  18.    * @var string
  19.    */
  20.    protected $_autojoin = array(
  21.       array(‘table’ => ‘profile’,
  22.             ‘on’ =>  ‘profile.prf_id = user.prf_id’,
  23.             ‘fields’ => array(‘prf_label’)
  24.       )
  25.    );
  26.    /**
  27.    * Restriction for query
  28.    *
  29.    * @var string
  30.    */
  31.    protected $_restrict = array(‘profile.prf_valid = 1′);
  32. }

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

  1. Select
  2.    user.*,
  3.    profile.prf_label
  4. FROM user
  5. INNER JOIN profile ON (profile.prf_id = user.prf_id)
  6. 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.

8 Comments
taintedsong.com taintedsong.com taintedsong.com

Ajouter un champ calculé dans une table.

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

  1.  
  2. Class Model_Facture_Table extends Zend_Db_Table_Abstract {
  3.    protected $_name = ‘facture’;
  4.    protected $_rowClass = ‘ Model_Facture_Row’;
  5.    public function __construct($config = array())
  6.    {
  7.       parent::__construct($config);
  8.    $this->_cols[] = ‘fac_total’;
  9.    }
  10. }

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

  1. 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

  1. SELECT
  2.    facture .*,
  3.    SUM(lig_prix*lig_qte) AS fac_total
  4. FROM facture
  5. INNER JOIN lignes USING (fac_id)
  6. GROUP BY fac_id;

modifier la méthode _fetch

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

  1.    /**
  2.      * Support method for fetching rows.
  3.      *
  4.      * @param  string|array $where  OPTIONAL An SQL WHERE clause.
  5.      * @param  string|array $order  OPTIONAL An SQL ORDER clause.
  6.      * @param  int          $count  OPTIONAL An SQL LIMIT count.
  7.      * @param  int          $offset OPTIONAL An SQL LIMIT offset.
  8.      * @return array The row results, in FETCH_ASSOC mode.
  9.      */
  10.     protected function _fetch($where = null, $order = null, $count = null, $offset = null)
  11.     {
  12.         // selection tool
  13.         $select = $this->_db->select();
  14.  
  15.         // the FROM clause
  16.         $select->from($this->_name, $this->_cols, $this->_schema);
  17.  
  18.         // the WHERE clause
  19.         $where = (array) $where;
  20.         foreach ($where as $key => $val) {
  21.             // is $key an int?
  22.             if (is_int($key)) {
  23.                 // $val is the full condition
  24.                 $select->where($val);
  25.             } else {
  26.                 // $key is the condition with placeholder,
  27.                 // and $val is quoted into the condition
  28.                 $select->where($key, $val);
  29.             }
  30.         }
  31.  
  32.         // the ORDER clause
  33.         if (!is_array($order)) {
  34.             $order = array($order);
  35.         }
  36.         foreach ($order as $val) {
  37.             $select->order($val);
  38.         }
  39.  
  40.         // the LIMIT clause
  41.         $select->limit($count, $offset);
  42.         // return the results
  43.         $stmt = $this->_db->query($select);
  44.         $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
  45.         return $data;
  46.     }

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

  1. $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

  1.  
  2. $cols = $this->_cols;
  3. unset($cols[array_search(‘fac_total’,$cols)]);
  4. $select->from($this->_name, $cols, $this->_schema)
  5. ->join(‘lignes’,’ligne.fac_id = facture.fac_id’, array(‘fac_total’ =>Zend_Db_Exp(‘SUM(lig_prix * lig_qte)’)))
  6. ->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.

  1.  
  2.    public function insert(array $data) {
  3.       unset($data[‘fac_total’]);
  4.       parent::insert($data);
  5.    }

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.

  1.  
  2. Zend_Loader::loadClass(‘Zend_Db_Table’);
  3.  
  4. Class Fast_Db_Hierarchical extends Zend _Db_Table {
  5.  
  6.    /**
  7.    * left field name in table
  8.    *
  9.    * @var string
  10.    */
  11.    protected $_left = NULL;
  12.  
  13.    /**
  14.    * right field name in table
  15.    *
  16.    * @var string
  17.    */
  18.    protected $_right = NULL;
  19.  
  20.    /**
  21.    * level field name in table
  22.    *
  23.    * @var string
  24.    */
  25.    protected $_level = NULL;
  26.  
  27.    /**
  28.    * virtual field name used has id of parent
  29.    *
  30.    * @var string
  31.    */
  32.    protected $_parent = NULL;
  33.  
  34.    public function __construct($config = array())
  35.    {
  36.       parent::__construct($config);
  37.       if (null == $this->_left)   throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEFT_KEY);
  38.       if (null == $this->_right)  throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_RIGHT_KEY);
  39.       if (null == $this->_level)  throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_LEVEL_KEY);
  40.       if (null == $this->_parent) throw new Fast_Exception_Db(Fast_Exception_Db::UNDEFINED_PARENT);
  41.       $this->_cols[] = $this->_parent;
  42.    }
  43.  
  44.    public function getById($id) {
  45.       $rows = $this->find($id);
  46.       if ($rows) {
  47.          return $rows->current();
  48.       }
  49.       return false;
  50.    }
  51.  
  52.    public function deleteById($id) {
  53.       if ($id == 1) return false; // on ne peut supprimer la racine
  54.  
  55.       $this->_db->beginTransaction();
  56.       $parent = $this->_db->select();
  57.       $parent->from($this->_name, array(‘delete_left’ => $this->_left, ‘delete_right’ => $this->_right))
  58.              ->where($this->_primary[1].‘ = :_deleteId’);
  59.       $statement = $this->_db->prepare($parent);
  60.       $statement->execute(array(‘_deleteId’ => $id));
  61.       list($deleteLeft, $deleteRight) = array_values($statement->fetch());
  62.       $res = false;
  63.       if ($deleteLeft) {
  64.          $row = $this->getById($id);
  65.          $res = $row->delete();
  66.  
  67.          if ($res) {
  68.             $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  69.                                              SET ‘.$this->_left.‘ = ‘.$this->_left.‘ - 1
  70.                                              WHERE ‘.$this->_left.‘ >= ‘.$deleteLeft.
  71.                                              AND ‘.$this->_right.‘ < ‘.$deleteRight.‘;’);
  72.             $statement->execute();
  73.          }
  74.  
  75.          if ($res) {
  76.             $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  77.                                              SET ‘.$this->_left.‘ = ‘.$this->_left.‘ - 2
  78.                                              WHERE ‘.$this->_left.‘ >= ‘.$deleteLeft.
  79.                                              AND ‘.$this->_right.‘ > ‘.$deleteRight.‘;’);
  80.             $statement->execute();
  81.          }
  82.  
  83.          if ($res) {
  84.             $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  85.                                              SET ‘.$this->_right.‘ = ‘.$this->_right.‘ - 1
  86.                                              WHERE ‘.$this->_right.‘ >= ‘.$deleteLeft.
  87.                                              AND ‘.$this->_right.‘ < ‘.$deleteRight.‘;’);
  88.             $statement->execute();
  89.          }
  90.  
  91.          if ($res) {
  92.             $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  93.                                              SET ‘.$this->_right.‘ = ‘.$this->_right.‘ - 2
  94.                                              WHERE ‘.$this->_right.‘ >= ‘.$deleteLeft.
  95.                                              AND ‘.$this->_right.‘ > ‘.$deleteRight.‘;’);
  96.             $statement->execute();
  97.          }
  98.  
  99.          if ($res) {
  100.             $this->_db->commit();
  101.          } else {
  102.             $this->_db->rollback();
  103.          }
  104.       }
  105.       return $res;
  106.    }
  107.  
  108.    public function UpdateById($data) {
  109.       // on ne peut mettre à jour les donnée hiérarchique
  110.       // ie on ne peut déplacer un noeud dans l’arbre.
  111.       unset($data[$this->_parent]); // ne fait pas partie de la table
  112.       unset($data[$this->_left]);   //ne peut être changé
  113.       unset($data[$this->_right]);  //ne peut être changé
  114.       unset($data[$this->_level]);  //ne peut être changé
  115.       $res =  parent::UpdateById($data);
  116.       return $res;
  117.    }
  118.  
  119.    public function insert(array $data) {
  120.       # select left and level of parent
  121.       $parentId = $data[$this->_parent];
  122.  
  123.       $this->_db->beginTransaction();
  124.       $parent = $this->_db->select();
  125.       if (null != $this->_level) {
  126.          $fields = array(‘parent_left’ => $this->_left, ‘parent_level’ => $this->_level);
  127.       } else {
  128.          $fields = array(‘parent_left’ => $this->_left,);
  129.       }
  130.  
  131.       $parent->from($this->_name, $fields)
  132.              ->where($this->_primary[1].‘ = :_parentId’);
  133.       $statement = $this->_db->prepare($parent);
  134.       $statement->execute(array(‘_parentId’ => $parentId));
  135.       list($parentLeft, $parentLevel) = array_values($statement->fetch());
  136.  
  137.       $res = false;
  138.       if ($parentLeft) {
  139.          #update tree
  140.          $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  141.                                           SET ‘.$this->_left.‘ = ‘.$this->_left.‘ + 2
  142.                                           WHERE ‘.$this->_left.‘ > ‘.$parentLeft.‘;’);
  143.          $res = $statement->execute();
  144.          if ($res) {
  145.             $statement = $this->_db->prepare(‘UPDATE ‘.$this->_name.
  146.                                              SET ‘.$this->_right.‘ = ‘.$this->_right.‘ + 2
  147.                                              WHERE ‘.$this->_right.‘ > ‘.$parentLeft.‘;’);
  148.             $statement->execute();
  149.          }
  150.  
  151.          #insert node
  152.          if ($res) {
  153.             unset($data[$this->_parent]);
  154.             $data[$this->_left] = $parentLeft + 1;
  155.             $data[$this->_right] = $parentLeft + 2;
  156.             if (null != $this->_level)
  157.                $data[$this->_level] = $parentLevel + 1;
  158.             $res =  parent::insert($data);
  159.          }
  160.          if ($res) {
  161.             $this->_db->commit();
  162.          } else {
  163.             $this->_db->rollback();
  164.          }
  165.       }
  166.       return $res;
  167.    }
  168.  
  169.     /**
  170.      * Support method for fetching rows.
  171.      *
  172.      * @param  string|array $where  OPTIONAL An SQL WHERE clause.
  173.      * @param  string|array $order  OPTIONAL An SQL ORDER clause.
  174.      * @param  int          $count  OPTIONAL An SQL LIMIT count.
  175.      * @param  int          $offset OPTIONAL An SQL LIMIT offset.
  176.      * @return array The row results, in FETCH_ASSOC mode.
  177.      */
  178.     protected function _fetch($where = null, $order = null, $count = null, $offset = null)
  179.     {
  180.         // selection tool
  181.         $select = $this->_db->select();
  182.  
  183.         //no _parent col on master table
  184.         $cols = $this->_cols;
  185.         unset($cols[array_search($this->_parent,$cols)]);
  186.  
  187.         // the FROM clause
  188.         $select->from($this->_name, $cols, $this->_schema);
  189.         // add the parent col
  190.         $select->join(array(‘parent’ => $this->_name),
  191.                       ‘(parent.’.$this->_left.‘ < workgroup.’.$this->_left.‘) AND
  192.                       (parent.’.$this->_right.‘ > workgroup.’.$this->_right.‘) AND
  193.                       (parent.’.$this->_level.‘ = workgroup.’.$this->_level.‘ -1)’,
  194.                       array(‘parent_id’ => ‘parent.’.$this->_primary[1].));
  195.  
  196.         // the WHERE clause
  197.         $where = (array) $where;
  198.         foreach ($where as $key => $val) {
  199.             // is $key an int?
  200.             if (is_int($key)) {
  201.                 // $val is the full condition
  202.                 $select->where($val);
  203.             } else {
  204.                 // $key is the condition with placeholder,
  205.                 // and $val is quoted into the condition
  206.                 $select->where($key, $val);
  207.             }
  208.         }
  209.  
  210.         // the ORDER clause
  211.         if (!is_array($order)) {
  212.             $order = array($order);
  213.         }
  214.         foreach ($order as $val) {
  215.             $select->order($val);
  216.         }
  217.  
  218.         // the LIMIT clause
  219.         $select->limit($count, $offset);
  220.         // return the results
  221.         $stmt = $this->_db->query($select);
  222.         $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
  223.         return $data;
  224.     }
  225.  
  226.     public function update(array $data, $where)
  227.     {
  228.       unset($this->_cols[array_search($this->_parent,$this->_cols)]);
  229.       unset($data[$this->_parent]);
  230.       $res = parent::update($data, $where);
  231.       $this->_cols[] = $this->_parent;
  232.       return $res;
  233.     }
  234.  
  235.    protected function _parent($row, $fiels) {
  236.       $parent = $this->_parents($row, $fiels)
  237.              ->order($this->_right)
  238.              ->limit(1);
  239.       return $parent;
  240.    }
  241.    protected function _parents($row, $fiels) {
  242.       $parent = $this->_db->select();
  243.       $parent->from($this->_name, $fiels)
  244.              ->where($this->_left.‘  < ‘.$row->{$this->_left})
  245.              ->where($this->_right.‘ > ‘.$row->{$this->_right});
  246.       return $parent;
  247.    }
  248.    protected function _childs($row, $fiels) {
  249.       $childs = $this->_db->select();
  250.       $childs->from($this->_name, $fiels)
  251.              ->where($this->_left.‘  > ‘.$row->{$this->_left})
  252.              ->where($this->_right.‘ < ‘.$row->{$this->_right});
  253.       return $childs;
  254.    }
  255.  
  256.     /**
  257.      * This is the find Zend_Db_Table Abstract method
  258.      * But the where closes are prefixed by the table name
  259.      *
  260.      * Fetches rows by primary key.
  261.      * The arguments specify the primary key values.
  262.      * If the table has a multi-column primary key, you must
  263.      * pass as many arguments as the count of column in the
  264.      * primary key.
  265.      *
  266.      * To find multiple rows by primary key, the argument
  267.      * should be an array.  If the table has a multi-column
  268.      * primary key, all arguments must be arrays with the
  269.      * same number of elements.
  270.      *
  271.      * The find() method always returns a Rowset object,
  272.      * even if only one row was found.
  273.      *
  274.      * @param  mixed                         The value(s) of the primary key.
  275.      * @return Zend_Db_Table_Rowset_Abstract Row(s) matching the criteria.
  276.      * @throws Zend_Db_Table_Exception
  277.      */
  278.     public function find()
  279.     {
  280.         $args = func_get_args();
  281.         $keyNames = array_values((array) $this->_primary);
  282.  
  283.         if (empty($args)) {
  284.             require_once ‘Zend/Db/Table/Exception.php’;
  285.             throw new Zend_Db_Table_Exception("No value(s) specified for the primary key");
  286.         }
  287.  
  288.         if (count($args) != count($keyNames)) {
  289.             require_once ‘Zend/Db/Table/Exception.php’;
  290.             throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
  291.         }
  292.  
  293.         $whereList = array();
  294.         $numberTerms = 0;
  295.         foreach ($args as $keyPosition => $keyValues) {
  296.             // Coerce the values to an array.
  297.             // Don’t simply typecast to array, because the values
  298.             // might be Zend_Db_Expr objects.
  299.             if (!is_array($keyValues)) {
  300.                 $keyValues = array($keyValues);
  301.             }
  302.             if ($numberTerms == 0) {
  303.                 $numberTerms = count($keyValues);
  304.             } else if (count($keyValues) != $numberTerms) {
  305.                 require_once ‘Zend/Db/Table/Exception.php’;
  306.                 throw new Zend_Db_Table_Exception("Missing value(s) for the primary key");
  307.             }
  308.             for ($i = 0; $i < count($keyValues); ++$i) {
  309.                 $whereList[$i][$keyPosition] = $keyValues[$i];
  310.             }
  311.         }
  312.         $whereClause = null;
  313.         if (count($whereList)) {
  314.             $whereOrTerms = array();
  315.             foreach ($whereList as $keyValueSets) {
  316.                 $whereAndTerms = array();
  317.                 foreach ($keyValueSets as $keyPosition => $keyValue) {
  318.                     $whereAndTerms[] = $this->_db->quoteInto(
  319.                         $this->_db->quoteIdentifier($this->_name).‘.’.$this->_db->quoteIdentifier($keyNames[$keyPosition], true) . ‘ = ?’,
  320.                         $keyValue
  321.                     );
  322.                 }
  323.                 $whereOrTerms[] = ‘(’ . implode(‘ AND ‘, $whereAndTerms) . ‘)’;
  324.             }
  325.             $whereClause = ‘(’ . implode(‘ OR ‘, $whereOrTerms) . ‘)’;
  326.         }
  327.  
  328.         return $this->fetchAll($whereClause);
  329.     }
  330.  
  331. }

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

5 Comments
taintedsong.com taintedsong.com taintedsong.com

Utiliser une base de données avec Zend Framwork

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

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected $_name = ‘client’;
  3. }

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

  1. Class Client_Row extends Zend_Db_Table_Row_Abstract {
  2. }

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.

  1. Class Client_Table extends Zend_Db_Table_Abstract {
  2.    protected