Zend Framework : base de données, modèles et Zend_Db_Adapter
Maintenant que nous avons mis en place l'infrastructure MVC de notre site, nous pouvons commencer à nous intéresser à son contenu. Le but de cet article sera de lier notre site à une base de données MySQL afin de pouvoir afficher nos différents billets. Nous verrons pour cela comment utiliser de manière raisonnée les modèles, et nous approcherons brièvement le connecteur Zend_Db_Adapter.
Création du Zend_Db_Adapter
Le Zend_Db_Adapter est une classe pré-définie du Zend Framework, qui va nous permettre de faire abstraction de la base de données (c'est une surcouche de PDO). Nous communiquerons donc à notre serveur MySQL à travers celui-ci, et à travers celui-ci seulement.
Afin de ne pas à avoir à recréer notre connecteur à chaque fois, nous allons définir un connecteur comme connecteur par défaut. Pour ce faire, éditons notre fichier bootstrap.php.
$dbAdapter = Zend_Db::factory($configuration->database);
$dbAdapter->query('SET NAMES UTF8');
$dbAdapter->setFetchMode(Zend_Db::FETCH_OBJ);;
Zend_Db_Table_Abstract::setDefaultAdapter($dbAdapter);
Nous créeons tout d'abord une instance de la classe Zend_Db_Adapter en récupérant une partie de notre fichier de configuration. On se souvient, dans le tuto précédent, que l'objet $configuration contient sous forme d'un objet la totalité des paires clefs/valeurs de notre fichier app.ini. Il va servir ici.
La seconde ligne (facultative) indique à MySQL que toutes nos requêtes seront à traiter en UTF-8. Je conseille très vivement l'utilisation de cet encodage (bien qu'il soit difficilement supporté) pour des raisons de pérennité de votre application. Il est en effet probable que l'UTF-8 va progressivement remplacer tous les encodages, dont notre si vieux ISO-8859-1(5). Ce choix est d'autant plus conforté par le fait que notre application va être multilingue. Si nous voulons donner un accès à notre blog à nos amis Chinois ou à nos amis Arabes, il nous faudra passer par là, leur alphabet n'étant pas inclus dans notre encodage.
La troisième ligne, similaire à PDO, indique sous quel forme retourner les résultats de nos requêtes. Ici, ce sera sous forme d'objets instanciant la classe standard. Ce choix est tout à fait personnel : je préfère manipuler des objets que des tableaux. Vous avez le choix entre de très nombreux fetchModes, dont FETCH_ASSOC ou FETCH_NUM.
Enfin, on indique à Zend de communiquer avec notre base de données grâce à ce connecteur, par défaut. Cela nous économisera régulièrement quelques lignes de code.
Tant que nous y sommes, libérons la mémoire occupée par notre $dbAdapter.
unset($frontController, $view, $configuration, $dbAdapter, $registry);
Configuration de l'accès à la base de données
Comme nous venons de le voir, nous avons besoin de l'objet $configuration pour créer notre connecteur. Il est donc temps de remplir notre fichier de configuration app.ini.
[production]
database.adapter = "PDO_MYSQL"[development : production]
database.params.dbname = "blog"
database.params.username = "root"
database.params.password = "root"[testing : production]
Je ne m'étendrais pas sur ces quelques lignes, elles sont suffisamment explicites d'elles-même. Une unique remarque : la base de données sous-jacente sera une base de données MySQL à la fois en environnement de production et à la fois en environnement de développement. Donc, plutôt que de répeter deux fois ce paramètre, je profite de l'héritage de sections (development hérite de production) pour factoriser la première ligne.
Tout est à présent prêt pour accéder à notre base de données. Bien moins fastidieux que la mise en place du modèle MVC, n'est-ce pas ? :p
Création de notre base de données
Manipuler une base de données, c'est bien. Mais c'est encore mieux si elle existe. Créons donc une table posts qui contiendra les billets de notre blog.
--
-- Table structure for table `posts`
--
DROP TABLE IF EXISTS `posts`;
CREATE TABLE IF NOT EXISTS `posts` (
`id` int(10) unsigned NOT NULL auto_increment,
`title` varchar(150) NOT NULL,
`description` text NOT NULL,
`content` longtext NOT NULL,
`created` datetime NOT NULL,
`status` enum('draft','published') NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
Nous partons volontairement sur une structure assez simple, afin de la compléter au fur et à mesure de nos besoins.
J'en profite au passage pour un petit coup de gueule auprès de certains développeurs : cessez de prévoir des fonctionnalités en ajoutant des dizaines de champs qui ne servent à rien dans vos bases. Cela alourdit la compréhension, et généralement ces modifications tombent dans l'oubli. Alors, pensez aux développeurs qui reprendront votre projet : pensez l'instant présent. Une fois ces quelques épicurismes informatiques sortis, nous pouvons reprendre le cours normal de notre tuto. :)
Pour s'assurer que cela fonctionne, nous allons ajouter quelques enregistrements.
-- -- Dumping data for table `posts` -- INSERT INTO `posts` (`id`, `title`, `description`, `content`, `created`, `status`) VALUES (1, 'Mon premier billet', '<p>Ceci est la description, ô combien passionante, de ce premier billet.</p>', '<p>Et cela est le bla bla habituel de l''article.</p>\r\n<p>Sur deux paragraphes. Et avec un <a href="http://www.google.fr">lien</a>. :p</p>', '2009-02-08 10:05:05', 'published'), (2, 'Un autre billet', '<p>Sinon, l''autre se serait ennuyé tout seul.</p>', '<p>Heureusement que je pense au bien-être de mes billets.</p>', '2009-02-07 10:05:45', 'published');
Notre base étant désormais prête à être utilisée, nous pouvons commencer par créer notre modèle, qui se chargera de recueillir les données de notre base.
Modèles de table (ORM)
Le Zend Framework prend en charge des notions d'ORM (Object Relationnal Mapping), qui nous permet de retrouver automatiquement tous les champs d'une base de données sans rien avoir à coder. Créons donc la classe qui nous permettra de faire cela. Directions le dossier models/tables et créons un fichier PostsTable.php.
<?php
class PostsTable extends Zend_Db_Table_Abstract
{
protected $_name = 'posts';
public function insert(array $data)
{
$data['created'] = date('Y-m-d H:i:s');
return parent::insert($data);
}
}
?>
C'est tout. A présent, nous avons tous les champs de notre table posts (indiqué dans l'attribut protégé $_name) directement dans notre classe PostTable. Avouez que cela est tout de même très simple et efficace.
La seule contrainte est de faire dériver toutes nos classes correspondant à des tables de la classe Zend_Db_Table_Abstract. Nous verrons, dans le prochain chapitre, comment mettre en place des liaisons inter-tables. Quant à la méthode insert ici nous permet de s'assurer que la date de création de notre billet (nom de la colonne created) sera bien remplie avec la date actuelle. Il y a l'équivalent avec la méthode update pour l'édition d'un enregistrement.
Faisons maintenant le modèle objet de notre classe Post. Nous le ferons, au hasard, dans models/Post.php.
Modèle de classe
<?php
class Post extends Zend_Db_Table_Row_Abstract
{
protected $table;
}
?>
Notre modèle héritera de la classe abstraite Zend_Db_Table_Row_Abstract (vivement les namespaces, afin de raccourcir un peu tous ces noms à rallonge), qui correspondra à un enregistrement (c'est-à-dire à une ligne bien précise). C'est au tour du développeur de la remplir. Le Zend Framework ne peut nous aider ici que de façon limitée, étant donné qu'il s'agit vraiment de la partie métier de l'application.
L'attribut protégé $table nous permettra de récupérer une instance de notre classe PostTable, et nous permettra donc d'optimiser notre application. A ne pas négliger donc. ;)
Nous retrouverons très régulièrement les deux mêmes méthodes dans notre modèle, à savoir :
public function getTable()
{
if (is_null($this->table))
{
require_once APPLICATION_PATH . '/models/tables/PostsTable.php';
$this->table = new PostsTable();
}
return $this->table;
}
public function save(array $data)
{
$table = $this->getTable();
$fields = $table->info(Zend_Db_Table_Abstract::COLS);
foreach ($data as $field => $value)
{
if (!in_array($field, $fields))
{
unset($data[$field]);
}
}
return $table->insert($data);
}
La méthode getTable nous permettra de récupérer l'instance de la table déjà existante, ou de la créer si elle ne l'est pas déjà. On se rapproche ainsi de la technique du singleton (une seule instance dans tout le script). Comme nous n'avons pas mis notre dossier models dans le include_path de notre application PHP, nous devons inclure le fichier correspondant à notre classe. Si tel n'est pas le cas, le Zend_Loader sera incapable de retrouver notre fichier, et donc de définir automatiquement notre classe.
Quant à la méthode save, elle vérifie, avant l'insertion dans la base de données, que nous n'avons pas mis des valeurs parasites. En effet, si $data contient des données qui ne correspondent à aucune colonne de la base de données, cela risque de poser quelques problèmes au niveau du framework. Donc, on s'en assure en utilisant la méthode info de Zend_Db_Table_Abstract, qui nous permet dans ce cas précis de récupérer tous les noms de colonnes de notre table.
Contrôleur de Post (aussi appelé facteur)
Nous continuons notre processus en trois étapes pour la réalisation de nos pages, à savoir : définition du modèle, définition du contrôleur, définition des vues. Trois composantes essentielles pour la réalisation du contrat MVC. Après le modèle, voici donc le tour du contrôleur (controllers/PostsController.php).
<?php
class PostsController extends Zend_Controller_Action
{
protected $model;
protected function getModel()
{
if(is_null($this->model))
{
require_once APPLICATION_PATH . '/models/Post.php';
$this->model = new Post();
}
return $this->model;
}
}
?>
De manière similaire à notre modèle, nous implémentons une technique du singleton, afin de ne pas avoir à recréer plusieurs fois notre instance de modèle.
Créeons donc la méthode associée à l'action list, qui nous permettra d'afficher les derniers billets de notre blog.
public function listAction()
{
$model = $this->getModel();
$this->view->posts = $model->getLatestPosts(5);
}
Nous faisons appel ici à une méthode qui n'existe pas, à savoir getLatestPosts. Cette méthode nous permettra de récupérer les n derniers billets enregistrés en base. Il nous suffit donc de l'implémenter, très simplement. Direction donc notre modèle de classe.
public function getLatestPosts($numberPosts)
{
$table = $this->getTable();
$select = $table->select()->where('status = ?', 'published')->order('created')->limit($numberPosts, 0);
return $table->fetchAll($select);
}
On peut ainsi remarquer que les requêtes se construisent en utilisant les méthodes select, where, order et limit. A noter que la clause where prend, tout comme PDO, des marqueurs de positionnement. Ainsi, nous n'avons pas à nous soucier de la sécurisation de nos données, si nous utilisons des marqueurs (un point d'interrogation dans ce cas).
Nous retournons nos derniers billets sous forme d'un tableau d'objets Post.
Affichage de nos billets : couche Vue
Il ne nous reste plus qu'à les afficher dans notre vue. Créons donc le fichier views/scripts/posts/list.phtml.
<h1>My blog</h1> < ?php foreach($this->posts as $post):?> <h2> <a href="<?= $this->url( array( 'controller' => 'posts', 'action' => 'view', 'id' => $post->id ), 'default', true) ?>"> < ?= $post->title?> </a> </h2> < ?= $post->description ?> < ?php endforeach;?>
Cela nous affichera la liste des billets avec leurs titres et la description correspondante.
La seule fonction nouvelle est la méthode url de l'objet view. Celle-ci construit dynamiquement l'URL en prenant en compte les différents paramètres qui lui sont passés. Ainsi, on redirigera vers l'action view du contrôleur posts, en lui passant un paramètre supplémentaire : l'id du post à consulter. Nous reverrons les deux derniers paramètres plus tard.
Il ne vous reste plus qu'à tester votre site en vous rendant à l'URL /posts/list. Et là, vous aurez alors accès à la liste de vos derniers billets. :)
Pour ceux qui voudraient aller plus loin, voici quelques idées d'améliorations :
- Mettre en place la page view permettant la lecture de l'article
- Mettre un lien suivant et précédent sur chacun de vos billets
- Retravailler un peu le CSS histoire d'avoir un design ne serait-ce que de base
Comme d'habitude, si vous bloquez sur une partie de ce tuto ou avez des optimisations à lui apporter, les commentaires sont juste en dessous. :)

Hum, j'avoue que tu me donnes quelques idées =D
Oh! It is so great using Zend_Db_Table_Row_Abstract. So, there is no need for the data mapper. I think the people who don't like Zend_Db_Table should read this article: they should prefer using Zend_Db to Zend_Db_Table). This may be the native solution for model layer of Zend Framework (< 2.0).
Sorry for my poor English.