Pratique Object/Relational Mapping avec Doctrine La gestion de la base de données est un point épineux de la création d'une application. La librairie d'object/ Relational Mapping Doctrine vous permet de travailler d'un point de vue objet avec votre base de données relationnelle et ainsi de créer une application plus souple, cohérente et facile à maintenir. Cet article explique : Comment utiliser la librairie d'object/ Relational Mapping Doctrine. Niveau de difficulté En utilisant les fonctions natives de PHP et le langage SQL, le développeur prend le risque de faire un code difficile à porter, peu flexible et en proie aux failles de sécurité. Nous allons voir dans cet article comment, grâce à la librairie d'object/ Relational Mapping Doctrine, passer outre les requêtes classiques pour travailler uniquement à partir d'objets, avec tous les avantages que cela comporte. Tout d'abord, qu'est-ce que l'objet/ Relational Mapping (ORM)? Il s'agit d'une solution pour le problème suivant : comment sauvegarder des objets dans un système de base de données relationnelle? Les deux concepts sont effectivement très éloignés. Pour y répondre, le système d'orm va utiliser une carte des tables de la base et de leurs relations pour créer un comportement objet. Avec l'évolution actuelle de PHP qui est de plus en plus orientée objet, connaître un système d'orm est un avantage certain. Doctrine est une librairie d'orm pour PHP débutée en 2005 qui prend actuellement de l'importance. La dernière version, la 1.0.6, requiert au minimum la version 5.2 de PHP, ce qui montre la volonté des développeurs de créer et maintenir un outil d'actualité, utilisant les dernières fonctionnalités de PHP. Pourquoi choisir Doctrine et pas une autre solution Ce qu'il faut savoir : La programmation orientée objet avec PHP. Le concept des bases de données relationnelles. d' ORM Open Source? Avant tout pour sa simplicité. Pas besoin d'écrire de longs fichiers de configuration ou de compiler quoi que ce soit à la moindre modification : quelques minutes suffisent pour mettre en place Doctrine et commencer à coder. Ensuite pour le Doctrine Query Language, qui permet de créer des requêtes complexes tout en restant dans le milieu objet. Les nombreux outils bonus fournis avec la librairie sont aussi un avantage certain. Enfin, la documentation est très complète et mise à jour (voir encadré Sur Internet). Mise en place de Doctrine Doctrine requiert au minimum la version 5.2.3 de PHP pour fonctionner; l'utilisation d'une version récente de PHP a des avantages : inciter les administrateurs système à mettre à jour leur version de PHP, et créer un code plus Quelques autres librairies d'orm pour PHP clair, entre autres grâce à l'utilisation de la Standard PHP Library et du modèle objet de PHP5. Dans cet article nous utiliserons la version 1.0 de Doctrine. Après avoir téléchargé Doctrine sur http:// www.doctrine-project.com/, installez-la dans le répertoire de librairies de votre projet (au hasard, lib/ ). On peut voir que la librairie comporte un fichier principal, Doctrine.php, et que le reste des sources est dans un sous-répertoire Doctrine/. Ce détail a son importance pour le chargement automatique des classes. Il nous faut un fichier d'initialisation commun à toutes les pages pour configurer l'environnement de travail et se connecter à la base. Ce fichier devra avant tout inclure Doctrine. php situé à la racine de la librairie : require_ once 'lib/doctrine.php';. Il s'agit d'ailleurs du seul fichier à importer manuellement, les autres seront chargés via la méthode de la Standart PHP Library autoload, une nouveauté de PHP 5.2 qui permet de définir une fonction personnalisée pour l'inclusion des fichiers de classe. Dès qu'un fichier de définition de classe est manquant, PHP va lancer cette fonction. Doctrine propose son propre autoload, qu'il faut configurer de cette manière : Propel est le principal concurrent de Doctrine. Cette librairie propose presque toutes les mêmes fonctionnalités, en dehors du Doctrine Query Language qui est remplacé par un système de critères à ajouter à une requête. Propel est en phase d'être détrônée par Doctrine en tant que librairie d'orm par défaut livrée avec le framework symfony, Zend_Db, module intégrant le framework Zend propose les fonctionnalités de base attendues par une ORM. Il peut être intéressant de l'utiliser dans le cas où votre application est déjà basée sur le Zend Framework, pour conserver l'unicité du code ou limiter la quantité de librairies utilisées, PhpMyObject, nouveau venu dans le monde des ORM présente l'originalité de ne pas utiliser les modèles : les informations des tables sont chargées automatiquement et les relations se font de manière transparente. Attention néanmoins, la vitesse d'exécution en pâtit énormément, bien qu'une solution de mise en cache soit prévue dans une prochaine version. 1 3/2009
Doctrine spl_autoload_register(array('doctrine', 'autoload'));. La connexion à une base se fait via Doctrine_ Manager::connection() qui peut prendre comme paramètre un objet PDO ou une chaîne de caractères contenant les informations d'accès à la base, par exemple : $conn = Doctrine_Manager::connection('mysql: //utilisateur:password@localhost/blog'); Maintenant que la connexion est établie, il faut décider de la méthode à utiliser pour charger les modèles (des classes qui représentent les tables, nous reviendrons sur leur définition précise plus tard). Pour charger ces fichiers, Doctrine propose deux méthodes : conservative : les modèles ne seront chargés que s'ils sont nécessaires lors du script en cours, agressive : tous les fichiers PHP du répertoire désigné seront inclus dès l'initialisation. La méthode conservative est la meilleure du point de vue des performances, mais le chargement agressif peut résoudre des problèmes dans certains cas spécifiques. Notre blog n'utilisant que quatre tables, n'importe quelle méthode fera l'affaire. Ajouter ceci au fichier d'initialisation : $conn->setattribute('model_loading', 'conservative'); Doctrine::loadModels('models/generated'); Doctrine::loadModels('models/'); Doctrine est maintenant configurée et initialisée. Les tables et le mapping Doctrine utilisant entre autres le motif Row Data Gateway, chaque table aura une classe associée. Cela permet de séparer nettement les requêtes SQL de la logique métier de l'application. Doctrine générera une partie de ces classes pour vous, tandis que les relations entre les tables, les templates et autres événements devront être écrits à la main. Plus précisément, Doctrine va générer deux classes par table. Une première, nommée BaseNomdelatable contiendra les informations indispensables au fonctionnement de l'orm : le nom de la table ainsi que les noms et types des champs de cette table. La seconde, nommée Nomdelatable et vide lors de la génération des classes par Doctrine, servira à étendre la classe de base. C'est cette deuxième classe qui nous intéressera, puisque c'est dans celle-ci que seront indiquées les informations ne pouvant être générées automatiquement. Ces deux classes forment ce que j'appellerai par la suite un modèle. Lexique ORM : Object/Relationnal Mapping, système permettant de simuler un comportement objet sur une base de données relationnelle, SGBD : Système de Gestion de Base de Données, les plus répandus sont MySQL et Oracle, PDO : PHP Data Objects, une extension fournie d'office depuis PHP 5.1 offrant une interface pour communiquer avec les SGBD, SQL : Structured Query Language, pseudo-langage utilisé pour communiquer avec la majorité des SGBD du marché, DQL : Doctrine Query Language, système de création de requêtes de Doctrine en objet, calqué sur SQL. Création des tables et des modèles Il existe plusieurs méthodes pour générer les modèles via Doctrine, la plus simple et rapide restant de laisser Doctrine les créer à partir d'une base de données déjà existante. Commencez par créer les tables qui sont décrites dans le modèle conceptuel des données de la Figure 1 grâce à votre système d'administration de base de données favori (ne créez pas de clés étrangères pour le moment). Listing 1. Le modèle de la table article au format YAML Article: tablename: article columns: id: type: integer(4) primary: true autoincrement: true titre: type: string(255) notnull: true texte: type: string(2147483647) notnull: true date_ajout: type: timestamp(25) notnull: true date_modification: type: timestamp(25) Listing 2. Plusieurs moyens d'ajouter des enregistrements $categorie1 = new Categorie(); $categorie1->nom = 'Php'; Une fois que c'est fait, il suffit d'ajouter la ligne suivante dans le fichier d'initialisation pour générer les modèles : Doctrine::generateModelsFromDb ('models/'); $categorie1->save(); // Nouvelle catégorie nommée 'Php' $categorie2 = new Categorie(); $article = new Article(); $article->titre = "Premier post sur mon nouveau blog!"; Exécutez le script pour lancer la génération. En regardant dans le dossier models, on peut voir que quatre classes ont été créées à la racine et quatre autres dans le dossier generated. $article->texte = "Remarquez qu'il n'y a pas besoin d'échapper les données sortantes"; $article->commentaire[0]->nom = 'John Doe'; $article->commentaire[0]->email = 'em@ail.fr'; $article->commentaire[0]->texte = 'Premier commentaire sur le blog'; $article->link('categorie', array( $categorie1->id, $categorie2->id ) ); $article->save(); // Enregistre le nouvel article, le commentaire lié et associe l'article aux catégories 'Php' et 'Mysql' www.phpsolmag.org 2
Pratique Les classes stockées dans le dossier generated correspondent aux classes de base et seront ré-écrites à chaque fois que la commande generatemodelsfromdb sera exécutée, tandis que les quatre autres classes ne seront jamais ré-écrites pour nous permettre de les éditer en toute sérénité. Vous remarquerez que les noms de table ont été transcrits au format CamelCase lors de la création des modèles : chaque mot commence par une majuscule et les underscores sont considérés comme des séparations de mots, ainsi commentaire se transforme en Commentaire et article _ categorie devient ArticleCategorie. Listing 3. Différentes manières de sélectionner des enregistrements // Récupérer un article via son identifiant $article = Doctrine::getTable('Article')->find(1); echo $article->titre; echo $article->commentaire->count(); // Récupérer les articles des catégories nommées 'Php' $categories = Doctrine::getTable('Categorie')->findByNom('Php'); if( $categories->count() > 0 ) { foreach( $categories[0]->article as $article ) { echo $article->titre; // Récupérer les articles contenant 'donnée' // et commentés par John Doe $articles = Doctrine_Query::create() ->select('a.titre, a.texte, c.nom, c.email') ->from('article a') ->innerjoin('a.commentaire c') ->where('c.nom =?', "John Doe") ->andwhere('a.texte LIKE?', "%donnée%") ->execute(); if( $articles!== null ) { echo $articles[0]->commentaire[0]->email; Listing 4. Exemple d'utilisation de la pagination $pageactuelle = isset($_get['page'])? $_GET['page'] : 1; $articlesparpage = 2; $requete = Doctrine_Query::create()->select('*')->from('Article'); Il faut savoir qu'il est aussi possible d'utiliser des fichiers de configuration YAML (YAML Ain't Markup Language) pour générer les modèles et les tables. Ce format, bien connu des utilisateurs de symfony, présente des avantages mais crée un intermédiaire supplémentaire qui complexifie légèrement l'application et que nous n'utiliserons donc pas dans cet article. Voyez le Listing 1 pour un exemple de modèle au format YAML. $pagination = new Doctrine_Pager( $requete, $pageactuelle, $articlesparpage ); $articles = $pagination->execute( array(), Doctrine::HYDRATE_ARRAY ); // affiche les résultats : foreach( $articles as $article ) { echo '<p>'. $article['texte']. '</p>'; $affichage = new Doctrine_Pager_Layout( $pagination, ); new Doctrine_Pager_Range_Jumping( array('chunk' => 4) ), // le nombre maximum de pages '?page={%page_number' $affichage->settemplate('[<a href="{%url">{%page</a>]'); $affichage->setselectedtemplate('[<b>{%page</b>]'); $affichage->display(); Indiquer les relations Il faut maintenant écrire les relations entres les tables. Grâce à ces quelques lignes, Doctrine pourra construire automatiquement les liaisons entre les tables lors des requêtes en Doctrine Query Language ou des modifications via les instances de classes : un gain de temps qui sera plus qu'appréciable par la suite! Notre application de blog comporte deux relations. Un article peut avoir plusieurs commentaires, mais un commentaire ne peut avoir qu'un seul article. En revanche, un article peut être dans plusieurs catégories, et chaque catégorie peut contenir plusieurs articles. Commençons par indiquer la relation entre les articles et les commentaires, qui est du type one-to-many, en ajoutant au fichier Article.php : public function setup() { $this->hasmany('commentaire', array( )); 'local' => 'id', 'foreign' => 'article_id' Nous venons d'indiquer que le modèle Article possède plusieurs commentaires. Dans la foulée, ajouter le code suivant au fichier Commentaire.php : public function setup() { $this->hasone('article', array( )); 'local' => 'article_id', 'foreign' => 'id' Nos tables sont liées! Quelques explications sont peut-être nécessaires. La méthode setup est appelée lors de l'initialisation du modèle, c'est donc dans celle-ci qu'il faut entre autres préciser les relations. Les méthodes internes hasone et hasmany (respectivement "a une" et "a plusieurs") permettent d'indiquer les relations entre les tables. Ainsi, pour créer une liaison de type one-to-one, les deux modèles posséderont chacun une méthode hasone, pour une relation one-to-many un modèle possédera la méthode hasmany et l'autre hasone (comme c'est le cas ici), et pour une relation many-to-many les deux modèles auront la méthode hasmany. Ces deux méthodes prennent deux paramètres : en premier le nom de la table liée, en second un tableau de paramètres dans lequel nous indiquons les colonnes qui servent de références pour les liaisons : local pour la colonne locale, et foreign pour la colonne de la table liée. Passons maintenant à la relation entre les articles et les catégories, qui est de type many-tomany. Comme vous le savez peut-être, ce genre de liaison se fait via une table intermédiaire, categorie_article dans notre exemple. Grâce à Doctrine, vous n'aurez jamais besoin d'accéder à cette table, les liaisons se feront de façon invisible (comme le montre la deuxième 3 3/2009
Doctrine Si l'on est déjà en possession d'un enregistrement et que l'on souhaite le lier à un autre, il faut utiliser la méthode link, comme montré dans le Listing 2. Figure 1. Le modèle conceptuel des données de notre blog, sans les relations ligne du Tableau 1). Ajouter le code suivant aux méthodes setup des modèles Article et Categorie : // Article.php : $this->hasmany('categorie', array( 'local' => 'article_id', 'foreign' => 'categorie_id', 'refclass' => 'CategorieArticle')); // Categorie.php $this->hasmany('article', array( 'local' => 'categorie_id', 'foreign' => 'article_id', 'refclass' => 'CategorieArticle')); Un nouveau paramètre fait son apparition : refclass. Il permet de signaler que la liaison se fait via un modèle intermédiaire. Vous remarquerez que les colonnes de référence sont celles du modèle CategorieArticle. comme une propriété de l'objet. La table article ayant un champ titre, il est possible de définir le titre du nouvel enregistrement de cette manière : $article->titre = "titre de l'article". Plus intéressant, l'instance du modèle a aussi un accès direct à ses tables liées. Il est donc possible d'ajouter directement de nouveaux commentaires lors de l'ajout d'un nouvel article, sans créer d'instance de Commentaire! Si la relation de l'objet vers les enregistrements liée est créée via hasmany, celui-ci sera accessible via un tableau ( par exemple $article-> Commentaire est un tableau de modèles Commentaire). Veillez bien à ne pas indiquer explicitement les identifiants de liaison entre les enregistrements, ceux-ci seront créés par Doctrine au moment d'enregistrer les données, comme ceci : $article->save();. Sélectionner des enregistrements Doctrine propose tout un panel d'outils pour sélectionner des données à partir d'une ou plusieurs tables. Une façon simple et rapide de récupérer des enregistrements est de passer par la méthode statique gettable, qui donne accès à plusieurs sélecteurs : find, findall et findby[...]. Les sélecteurs find et findall permettent respectivement de sélectionner soit un enregistrement par son identifiant, soit tous les enregistrements de la table. Le sélecteur magique findby[...] quant à lui, permet de sélectionner les enregistrements selon la valeur d'une colonne, en remplaçant [...] par le nom de la colonne au format CamelCase. Voyez le Listing 3 pour des exemples. Mais ce moyen de récupérer des données présente un inconvénient : les critères de sélection ne se font que sur une seule table. Pour les requêtes multi-critères Doctrine propose son propre système, le Doctrine Query Language (DQL), qui permet de construire des requêtes complexes de façon modulaire et compatible avec la grande majorité des SGBD. Pour faire une nouvelle requête avec DQL, il faut commencer par créer un nouvel objet Doctrine _Query comme ceci : $query = Doctrine_Query::create();. Il faut ensuite ajouter le type de requête via select(), update() ou delete(), puis des critères avec from(), leftjoin(), innerjoin() ou where() par exemple. Comme vous pouvez le voir, la syntaxe de DQL est très similaire La gestion des enregistrements Nos modèles prêts, il est temps de faire quelques expériences avec les enregistrements. Les données récupérées via Doctrine sont encapsulées dans un objet de type Doctrine_ Record, qui permet d'accéder aux données en lecture, mais aussi en écriture! En effet, n'importe quel enregistrement récupéré ou créé pourra être modifié puis enregistré via $enregistrement->save();. D'autre part, les objets Doctrine_Record peuvent aussi charger de manière transparente les enregistrements liés, qui seront à leur tour modifiables puisqu'eux mêmes sont des instances de Doctrine_Record... L'ajout de nouveaux enregistrements se fait via l'instantiation d'un modèle, par exemple pour créer un nouvel enregistrement dans la table article, il faut créer un nouvel objet Article : $article = new Article();. Chaque champ de la table est alors accessible Tableau 1. Les principales méthodes DQL avec leur équivalence en SQL DQL $query->from('article'); $query->select('a.titre, a.texte, c.email, cat.nom') ->from('article a') ->leftjoin('a.commentaire c') ->leftjoin('a.categorie cat') ->orderby('a.date _ ajout') ->limit(5); $query->select()->from('article a') ->where('a.titre =?', "Titre de l'article") ->andwhere('a.texte LIKE? OR a.id =?', array('%hpsol%', 5)); $query->update('article') ->set('date _ modification', '?', new Doctrine _ Expression('NOW()') ) ->wherein('id', array(7, 4, 5, 89)) $query->delete()->from('article') ->where('id >= :maxarticle', array(':maxarticle' => 12)) SQL SELECT * FROM article SELECT a.titre, a.texte, c.email, cat.nom FROM article AS a LEFT JOIN commentaire AS c ON c.article _ id = a.id LEFT JOIN categorie _ article AS ca ON ca.article _ id = a.id LEFT JOIN categorie AS cat ON ca.categorie _ id = cat.id ORDER BY a.date _ ajout ASC LIMIT 0,5 SELECT * FROM article AS a WHERE a.titre = 'Titre de l\'article' AND (a.texte LIKE '%hpsol%' OR a.id = '5') UPDATE article SET date _ modification = NOW() WHERE id IN ('7,4,5,89') DELETE FROM article WHERE id >= '12' www.phpsolmag.org 4
Pratique à celle de SQL ce qui rend son apprentissage aisé pour qui a déjà utilisé SQL par le passé. Remarquez que les jointures n'ont pas besoin d'être précisées explicitement, mais qu'il faut en revanche préciser l'alias de la table parente pour que la structure des résultats soit correcte. Il faut effectivement que Doctrine sache entre quelles tables se fait la liaison : en imaginant qu'on ajoute la possibilité de mettre des commentaires sur une catégorie, il faudra bien montrer au moment de la requête si l'on souhaite les commentaires de l'article ou de la catégorie. Les critères de condition avec where utilisent un système de template qui permet de séparer les paramètres de la condition : en utilisant un ou plusieurs points d'interrogation, le second paramètre de la condition sera une valeur ou un tableau de valeurs qui remplaceront les points d'interrogation par ordre d'apparition, en utilisant le format :clef, le second paramètre sera un tableau associatif de clefs/ valeurs. Il est aussi possible de passer tous les paramètres d'une requête d'un seul coup en passant un tableau à la méthode execute(). Ce système de template est très pratique lorsque l'on souhaite utiliser la même requête plusieurs fois, mais avec des paramètres différents. Listing 5. Modifier les valeurs des enregistrements avec les événements // ajouté à la classe Commentaire : public function prehydrate(doctrine_event $event) { $data = $event->data; $data['mailantispam'] = str_replace( array('@', '.'), array(' AT ', ' POINT '), $data['email'] ); $event->data = $data; // dans le script principal : $c = Doctrine_Query::create()->from('Commentaire')->fetchOne(); // récupère le premier enregistrement echo $c['mailantispam']; // affiche : mail AT host POINT com Listing 6. Ajouter des éléments dans une structure hiérarchique $progcategorie = new Categorie(); $progcategorie->nom = 'Programmation'; $tree = Doctrine::getTable('Categorie')->getTree(); $tree->createroot($progcategorie); // nouvelle racine $phpcategorie = new Categorie(); $phpcategorie->nom = 'PHP'; $phpcategorie->getnode()->insertaslastchildof($progcategorie); // nouvelle feuille $sqlcategorie = new Categorie(); $sqlcategorie->nom = 'SQL'; $sqlcategorie->getnode()->insertaslastchildof($progcategorie); // deuxième feuille Listing 7. Récupérer des enregistrements en utilisant une structure hiérarchique $compteurquery = Doctrine_Query::create() ->select('count(a.id) as nombrearticle, c.nom') ->from('categorie c') ->leftjoin('c.article a') ->groupby('c.id'); $tree = Doctrine::getTable('Categorie')->getTree(); $tree->setbasequery($compteurquery); $completetree = $tree->fetchtree(); foreach ($completetree as $categorie) { echo str_repeat(' ', $categorie['level']). ' - '. $categorie['nom']. '('. $categorie['nombrearticle']. ')<br />'; Figure 2. Schéma résumant le fonctionnement d'une application utilisant Doctrine Comme nous l'avons vu, les enregistrements récupérés sont stockés dans un objet Doctrine_ Record qui, s'il a de nombreux avantages, est plutôt lourd. Si vous n'y croyez pas, essayez de faire un var_dump d'un enregistrement, les dizaines de milliers de lignes de données affichées devrait finir de vous convaincre. C'est pourquoi Doctrine propose un autre format plus léger pour les résultats lorsque l'on ne souhaite y accéder qu'en lecture, sous la forme d'un tableau associatif. Pour obtenir les enregistrements de cette manière, deux méthodes principales : $resultat = $requete->fetchall(); $resultat = $requete->execute( array(), Doctrine::HYDRATE_ARRAY ); Le contenu de $resultat est le même dans les deux cas. L'accès aux enregistrements se fait de manière similaire à ceux d'un objet Doctrine _ Record : $article['nom'] à la place de $article->nom. En fait, vous feriez bien d'utiliser ce format de résultats à chaque fois que l'accès en écriture n'est pas nécessaire; votre serveur vous en sera reconnaissant. Les outils de Doctrine En plus de toutes ses fonctionnalités d'orm, Doctrine propose une collection d'outils simplifiant la vie des programmeurs. Dans ce chapitre nous allons aborder quelques-uns de ces suppléments qui seront utiles pour notre application de blog. La pagination Doctrine propose un système permettant de paginer automatiquement des résultats, en gérant de la modification de la requête à l'affichage des résultats au format HTML. Concrètement, le système va calculer le nombre total 5 3/2009
Doctrine de résultats pour une requête et sélectionner uniquement la plage d'enregistrement voulue. Pour mettre en place cet outil, il faut commencer par préparer une requête DQL quelconque sans l'exécuter. Il suffit ensuite de la placer dans un nouvel objet Doctrine_Pager avec le nombre de résultats souhaité par page et la page à afficher, puis d'utiliser la méthode execute comme on le ferait sur un objet Doctrine_ Query pour récupérer les résultats. L'affichage de la pagination se fait via une nouvelle instance de Doctrine_Pager_Layout, qui prend trois paramètres : l'objet Doctrine _ Pager créé précédemment, un objet du type Doctrine _ Pager _ Range, qui contrôle l'écart maximal entre les pages affichées. Il y en a deux livrés de base avec la librairie : Jumping (affichage par blocs) et Sliding (affichage fluide), mais rien n'empêche de créer les vôtres, le template de l'url affiché, qui sera ensuite accessible dans les autres templates via {%url Il reste à définir le template du lien de la page en cours et celui des autres pages en utilisant settemplate et setselectedtemplate pour que la pagination soit complète. Le Listing 4 montre comment mettre rapidement un système de pagination en place, mais cet outil propose de nombreuses fonctionnalités avancées pour créer à peu près n'importe quel type de pagination. Les transactions Déjà connues de ceux utilisant régulièrement les bases de données relationnelles, les transactions permettent de valider ou d'annuler une séquence de requêtes. Doctrine utilise déjà les transactions en interne, par exemple lors de l'ajout simultané d'un Article et d'un Commentaire, si l'un des deux enregistrements pose problème, aucun des deux ne sera enregistré afin de garantir l'unicité de la base. On débute une transaction avec : $conn = Doctrine_Manager::getInstance() ->getconnection(0); $conn->begintransaction(); Puis, après avoir lancé les requêtes voulues, on valide via $conn->commit() ou annule avec $conn->rollback(). À noter que Doctrine supporte les transaction imbriquées, c'est à dire qu'il est possible de commencer une nouvelle transaction à l'intérieur d'une transaction. Les événements Les événements de Doctrine sont en réalité des Hooks, des appels de fonction lancés à certains moments précis. De cette manière, quasiment toutes les actions peuvent être accompagnées par l'exécution d'une méthode de façon transparente (voyez la documentation pour la liste complète des hooks). Par exemple, mettre automatiquement à jour les dates d'ajout et de modification d'un article serait bénéfique : une ligne de code en moins à chaque fois et la garantie que les dates seront toujours à jour. La mise en place d'un écouteur d'événements sur le modèle Article permet de le faire en ajoutant la méthode suivante à la classe Article : public function preinsert() { Sur Internet http://www.doctrine-project.org/ Le site officiel de la librairie, http://www.doctrine-project.org/documentation/manual/1_0/en La documentation (en anglais), http://php.developpez.com/faq/?page=doctrine La F.A.Q. Doctrine de developpez.com, http://www.symfony-project.org/cookbook/1_1/fr/doctrine Tutoriel d'installation avec symfony. $this->date_ajout = new Doctrine_ Expression('NOW()'); Nous utilisons ici la méthode spéciale preinsert : cette méthode est appelée juste avant l'enregistrement d'un nouvel Article. Il existe une pléthore de méthodes du même type pour exécuter des actions après une suppression, lors d'une requête DQL, etc. Voici un autre exemple : dans notre application de blog, nous enregistrons l'email des utilisateurs postant des commentaires. Au moment de l'affichage des commentaires, nous pourrions afficher l'email tel quel, mais ce serait donner en pâture aux spammeurs les adresses de nos chers lecteurs! Il faut donc encoder l'email pour qu'il ne soit pas récupérable par des robots mais lisible pour un humain, par exemple en remplaçant le @ par AT et le. par POINT. Tant qu'à faire, autant que cette valeur soit directement accessible dans les enregistrements sélectionnés. Le Listing 5 montre comment faire en quelques lignes. Mise en place d'une structure hiérarchique Une table a une structure hiérarchique lorsqu'elle comporte une référence sur elle-même, le plus souvent via l'identifiant de l'enregistrement parent. On trouve des structures hiérarchiques dans des applications diverses comme les forums des discussion ( un forum peut contenir un forum qui contient un forum etc. ) ou dans les menus multi-niveaux. Ce genre de système est difficile à mettre en place, puisqu'on ne peut pas connaître à l'avance le nombre d'éléments enfants ou parents que comporte un enregistrement. Pour nous aider dans cette tâche ardue, Doctrine propose ni plus ni moins de s'occuper de toute la gestion du système hiérarchique. Nous allons nous en servir pour la gestion des catégories du blog. Il faut commencer par modifier la table commentaire en ajoutant 3 champs : lft, rgt et level, tous les trois du type INT. Ces champs serviront à la gestion de l'arbre des catégories en interne, il ne faut en aucun cas assigner leur valeur manuellement ou les renommer, sous peine de corrompre la structure. Il faut ensuite signaler à Doctrine que le modèle Categorie doit agir comme une structure hiérarchique en ajout le code suivant dans le setup du modèle : $this->actas('nestedset'); Le modèle Catégorie peut maintenant être vu comme un arbre : les racines sont les catégories principales tandis que les feuilles représentent les catégories les plus éloignées. Un nœud quant à lui est une catégorie intermédiaire. Quand on rajoute une catégorie dans une catégorie "feuille", celle-ci devient un nœud. C'est de cette manière que le système de structure est vu par Doctrine, comme vous pouvez le constater dans le Listing 6. Il est ensuite possible de récupérer non seulement l'arbre des catégories, mais aussi les enregistrements liés en injectant une requête DQL lors de la génération de la structure. Il suffit pour cela d'utiliser setbasequery;. Le Listing 7 montre comment générer l'arbre des catégories de notre blog, avec le nombre d'articles contenus par chaque catégorie, le tout en une dizaine de lignes! Conclusion Doctrine est un outil puissant qui, non content d'offrir tout ce que l'on peut attendre d'une librairie d'orm, propose une multitude de fonctionnalités annexes. Après avoir vu les bases des modèles et des enregistrements, nous avons abordé certaines de ces fonctionnalités avec la pagination et les structures hiérarchiques. D'autres, comme la migration ou la mise en cache des requêtes, sont à découvrir dans la documentation. La prochaine version de Doctrine, la 1.1 est actuellement en release candidate et devrait être disponible sous peu. JONATHAN PRÉVOST Jonathan Prévost est développeur d'applications pour la société Odenti. Passionné depuis toujours par la programmation, l'auteur s'est spécialisé en développement web et plus particulièrement en ActionScript, Flex et PHP. Contact : jo.prevo@gmail.com. www.phpsolmag.org 6