Zend Framework - Ajouter un champ calculé dans une table
Date de publication : 10 octobre 2007
Par
JYT (Les expériences Zend de Sekaijin) (Blog)
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.
I. Introduction
II. Ajouter un champ à un objet de mapping
III. La table facture
IV. Modifier la méthode _fetch
V. L'écriture
VI. Un exemple généralisé
VII. Conclusion
I. Introduction
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 facturation. 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 facturation.
Par exemple, lorsque je vérifie que le montant payé et bien le bon, il est 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 facturation. 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.
II. 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.
III. La table facture
La toute première étape consiste à créer 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 sortirai ou entrerai 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, quelle que 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 :
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;
|
|
|
IV. Modifier la méthode _fetch
Tout d'abord, voyons comment est faite la méthode de ZF :
@param string array $ where
@param string array $ order
@param int $ count
@param int $ offset
@return array
protected function _fetch($ where = null , $ order = null , $ count = null , $ offset = null )
{
$ select = $this ->_db ->select ();
$select ->from ($this ->_name , $this ->_cols , $this ->_schema );
$ where = (array) $ where ;
foreach ($ where as $ key = > $ val ) {
if (is_int($ key )) {
$select ->where ($ val );
} else {
$select ->where ($ key , $ val );
}
}
if (! is_array($ order )) {
$ order = array ($ order );
}
foreach ($ order as $ val ) {
$select ->order ($ val );
}
$select ->limit ($ count , $ offset );
$ stmt = $this ->_db ->query ($ select );
$ data = $stmt ->fetchAll (Zend_Db: : FETCH_ASSOC );
return $ data ;
}
|
Cette méthode est un peu longue mais, au final, pas 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 clauses diverses.
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.
V. L'écriture
Tant que nous ne faisons que lire dans la table avec cet objet, nous n'aurons 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é. Toutefois, 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 a un comportement équivalent mais légèrement différent car update va tenter de mettre à jour même les champs qui ne sont pas présents 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 seront 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.
VI. 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'ai 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 niveaux hiérarchiques relatifs. Par exemple tous les nœuds aux rangs 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 à jour les bornes des éléments de la table.
1. | 2. | 3. | 4. | 5. | 6. | 7. | 8. | 9. | 10. | 11. | 12. | 13. | 14. | 15. | 16. | 17. | 18. | 19. | 20. | 21. | 22. | 23. | 24. | 25. | 26. | 27. | 28. | 29. | 30. | 31. | 32. | 33. | 34. | 35. | 36. | 37. | 38. | 39. | 40. | 41. | 42. | 43. | 44. | 45. | 46. | 47. | 48. | 49. | 50. | 51. | 52. | 53. | 54. | 55. | 56. | 57. | 58. | 59. | 60. | 61. | 62. | 63. | 64. | 65. | 66. | 67. | 68. | 69. | 70. | 71. | 72. | 73. | 74. | 75. | 76. | 77. | 78. | 79. | 80. | 81. | 82. | 83. | 84. | 85. | 86. | 87. | 88. | 89. | 90. | 91. | 92. | 93. | 94. | 95. | 96. | 97. | 98. | 99. | 100. | 101. | 102. | 103. | 104. | 105. | 106. | 107. | 108. | 109. | 110. | 111. | 112. | 113. | 114. | 115. | 116. | 117. | 118. | 119. | 120. | 121. | 122. | 123. | 124. | 125. | 126. | 127. | 128. | 129. | 130. | 131. | 132. | 133. | 134. | 135. | 136. | 137. | 138. | 139. | 140. | 141. | 142. | 143. | 144. | 145. | 146. | 147. | 148. | 149. | 150. | 151. | 152. | 153. | 154. | 155. | 156. | 157. | 158. | 159. | 160. | 161. | 162. | 163. | 164. | 165. | 166. | 167. | 168. | 169. | 170. | 171. | 172. | 173. | 174. | 175. | 176. | 177. | 178. | 179. | 180. | 181. | 182. | 183. | 184. | 185. | 186. | 187. | 188. | 189. | 190. | 191. | 192. | 193. | 194. | 195. | 196. | 197. | 198. | 199. | 200. | 201. | 202. | 203. | 204. | 205. | 206. | 207. | 208. | 209. | 210. | 211. | 212. | 213. | 214. | 215. | 216. | 217. | 218. | 219. | 220. | 221. | 222. | 223. | 224. | 225. | 226. | 227. | 228. | 229. | 230. | 231. | 232. | 233. | 234. | 235. | 236. | 237. | 238. | 239. | 240. | 241. | 242. | 243. | 244. | 245. | 246. | 247. | 248. | 249. | 250. | 251. | 252. | 253. | 254. | 255. | 256. | 257. | 258. | 259. | 260. | 261. | 262. | 263. | 264. | 265. | 266. | 267. | 268. | 269. | 270. | 271. | 272. | 273. | 274. | 275. | 276. | 277. | 278. | 279. | 280. | 281. | 282. | 283. | 284. | 285. | 286. | 287. | 288. | 289. | 290. | 291. | 292. | 293. | 294. | 295. | 296. | 297. | 298. | 299. | 300. | 301. | 302. | 303. | 304. | 305. | 306. | 307. | 308. | 309. | 310. | 311. | 312. | 313. | 314. | 315. | 316. | 317. | 318. | 319. | 320. | 321. | 322. | 323. | 324. | 325. | 326. | 327. | 328. | 329. | 330. |
| Zend_Loader: : loadClass(' Zend_Db_Table ' );
|
| Class Fast_Db_Hierarchical extends Zend _Db_Table {
|
|
|
|
| @var string
|
| protected $ _left = NULL ;
|
|
|
|
| @var string
|
| protected $ _right = NULL ;
|
|
|
|
| @var string
|
| protected $ _level = NULL ;
|
|
|
|
| @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 ;
|
| $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 ) {
|
|
| unset($ data [ $this ->_parent ] );
| unset($ data [ $this ->_left ] );
| unset($ data [ $this ->_right ] );
| unset($ data [ $this ->_level ] );
| $ res = parent : : UpdateById($ data );
| return $ res ;
| }
|
| public function insert(array $ data ) {
|
| $ 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 ) {
|
| $ 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 ();
| }
|
|
| 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 ;
| }
|
|
|
|
| @param string array $ where
| @param string array $ order
| @param int $ count
| @param int $ offset
| @return array
|
| protected function _fetch($ where = null , $ order = null , $ count = null , $ offset = null )
| {
|
| $ select = $this ->_db ->select ();
|
|
| $ cols = $this ->_cols ;
| unset($ cols [ array_search($this ->_parent , $ cols )] );
|
|
| $select ->from ($this ->_name , $ cols , $this ->_schema );
|
| $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 ] . ' ' ));
|
|
| $ where = (array) $ where ;
| foreach ($ where as $ key = > $ val ) {
|
| if (is_int($ key )) {
|
| $select ->where ($ val );
| } else {
|
|
| $select ->where ($ key , $ val );
| }
| }
|
|
| if (! is_array($ order )) {
| $ order = array ($ order );
| }
| foreach ($ order as $ val ) {
| $select ->order ($ val );
| }
|
|
| $select ->limit ($ count , $ offset );
|
| $ 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 ;
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| @param
| @return
|
|
| 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 ) {
|
|
|
| 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 aurez 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 simples. 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.
VII. Conclusion
Cette façon de dériver la classe Zend_Db_Table permet d'imaginer toute sorte de mappings entre un objet et un ensemble de tables dans la base. Par exemple, un modèle dans lequel les adresses sont dans une table à part des clients alors que l'objet de mapping remonte toujours l'ensemble, ou la remontée systématique des valeurs des tables de références, etc.
A+JYT
Copyright © 2007 Sekaijin.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.