Aller au menu - Aller au contenu

Icône Les templates

Avatar
Mise à jour : 04/09/2010
Difficulté : Intermédiaire Intermédiaire Creative Commons BY-NC-SA
487 visites depuis 7 jours, dont 12 sur ce chapitre classé 233/786
Nous allons maintenant nous attaquer à une partie importante de Doctrine : les templates (on parle aussi de behavior).
Ces behaviors vont nous être utiles lorsque nous avons plusieurs modèles qui partagent des points communs.
Ces points communs peuvent être :
  • des colonnes de la base de données ;
  • des algorithmes ;
  • des relations ;
  • n'importe quoi d'autre... ^^


Dans ce tutoriel, j'utiliserai indifféremment les termes template ou behavior. Littéralement, template signifie modèle alors que behavior se traduirait par comportement.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Introduction

Qu'est-ce qu'un template ?



De manière générale, en informatique, un template est un modèle. Comme vous le savez, un programmeur est fainéant (M@teo21 le répète pas mal dans ses tutoriels :D ). Cette fainéantise le pousse à détester une chose plus que tout : la duplication de code.

Cette réécriture de code identique (ou presque) se retrouve dans un grand nombre de concepts différents liés à l'informatique (ou pas, d'ailleurs ;) ), et le principal inconvénient est une baisse de la maintenabilité de l'application.

Exemples



Peut-être utilisez-vous un système de templates dans votre application afin de séparer le code PHP de la mise en page (en HTML ou autre). Ceci offre un avantage considérable : l'utilisation, par exemple, d'une page HTML sans se soucier de comment sont récupérées les données. On se contente de les afficher. Cette page HTML (template) peut alors être réutilisée par plusieurs scripts différents, tout comme le script peut envoyer ses informations à plusieurs pages HTML différentes.

Un autre exemple, qui n'existe pas en PHP dû à son typage dynamique faible, mais dans d'autres langages comme le C++, est le template de fonctions. Cela permet d'outrepasser le fait qu'une fonction ne peut accepter qu'un type de paramètre défini à l'avance. Mais je ne m'étends pas trop là-dessus, puisque nous avons affaire au PHP.

Et Doctrine dans tout ça ?



Lien avec Doctrine



Après ces brèves explications concernant les templates, vous ne voyez peut-être pas le rapport avec Doctrine.
Cherchez bien : où pourrions-nous avoir de la duplication de code dans notre projet ?

Vous ne voyez pas ? Dans notre schéma pardi ! ;)
Bien souvent, dans une application relativement complète, vos objets devront être datés. Ce peut être le cas d'articles, de commentaires liés à l'article, etc. Prenez par exemple Facebook (au hasard :D ), où toutes les actions sont datées : la mise en ligne de photos, publication de messages, ajout d'amis, participation à des évènements...

Image utilisateur

Imaginez que les développeurs veuillent changer quelque chose dans la gestion de la datation des objets. Vous croyez qu'ils vont s'embêter à modifier toutes les classes une par une ? :o Les pauvres !

Bref, je vous ai assez fait mariner comme ça. Avec Doctrine, nous allons pouvoir définir des modèles de classes. Lorsque vous aurez plusieurs objets (articles, etc.) à dater, vous aurez juste à dire « je veux le dater ! » et Doctrine s'en occupera automatiquement pour vous. Elle est pas belle la vie ? :-°

Quand dois-je utiliser les templates ?



Dès qu'il y a quelque chose de commun entre plusieurs classes, utilisez-les ! Votre application n'en sera que plus simple à maintenir en cas de changement.
Un point commun peut être des colonnes identiques (par exemple, une colonne date), un comportement, des relations, etc.


Je ne comprends pas. D'habitude, lorsque j'ai des points communs entre des classes, j'utilise l'héritage, ce n'est pas comme ça qu'il faut faire ?

Et bien, ça dépend. (Quoi, vous vous attendiez à une autre réponse ? :p )
Lorsque des classes ont entre elles une réelle relation parent-enfant, alors oui, l'héritage est là pour ça et il faut l'utiliser. Mais lorsque ce n'est pas le cas, mais qu'il y a des points communs, il paraît stupide de dupliquer du code. De plus, PHP lui-même, en ne supportant pas l'héritage multiple, nous limite.

La solution pour pallier cela : les templates ! ;)

Utilisation des templates

Doctrine propose plusieurs behaviors intégrés par défaut. Nous allons commencer par en étudier quelques-uns afin de voir comment ça marche.

J'utiliserai le plus souvent le terme objet pour définir une instance d'une classe du modèle (comme Article), et le terme table pour une instance d'une classe représentant une table (comme ArticleTable). Il s'agit d'un abus de langage, mais ça ne vous gênera pas pour comprendre. ;)


Découverte des behaviors



Il est très simple d'appliquer un behavior sur nos objets. Il faut l'indiquer dans le schéma de données.

Schéma en Yaml



Il suffit d'une ligne dans le schéma Yaml pour appliquer un behavior :

Code : Autre - ~/config/schema.yml
1
2
3
4
5
6
7
8
Article:
  actAs:
    Timestampable: ~
  columns:
    title:
      type: string(255)
      notnull: true
# ...


J'ai pris comme exemple le template nommé Timestampable (nous l'étudierons juste après). Comme vous le voyez, nous ajoutons une clé actAs qui indique la liste des behaviors et de leurs options. Ici, il n'y a pas d'option particulière (d'où le ~, qui est équivalent à null, pour rappel).

Notez que si aucun de vos templates n'a d'option, vous pouvez utiliser la notation simple de tableau :

Code : Autre
1
2
3
Article:
  actAs: [Timestampable, UnAutreTemplate]
# ...


Schéma en PHP



En PHP, il est recommandé de placer la déclaration dans la méthode setUp() :

Code : PHP - ~/lib/models/generated/BaseArticle.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
class BaseArticle extends Doctrine_Record
{
    /* ... */

    public function setUp()
    {
        /* ... */
 
        $this->actAs('Timestampable');
    }
}


Pour ceux qui génèrent les classes de base à partir du Yaml, vous aurez peut-être remarqué que le code généré n'est pas tout à fait identique :

Code : PHP
1
2
3
<?php
$timestampable0 = new Doctrine_Template_Timestampable();
$this->actAs($timestampable0);


C'est un objet qui est passé à la méthode actAs(). En effet, les templates sont des classes nommées de la forme Doctrine_Template_NomDuTemplate. Si vous passez juste son nom, Doctrine essayera de l'instancier lui-même.

Nous allons maintenant passer en revue les templates intégrés.

Timestampable



C'est probablement le template le plus connu. Il donne le moyen de dater facilement les objets (date de création et date de mise à jour).

Tous vos objets déclarés comme timestampables se voient ajouter deux nouvelles colonnes : created_at et updated_at. Notez qu'il est possible de personnaliser leur nom, ou même de n'utiliser que l'une des deux. Jetez un œil :

Code : Autre
1
2
3
4
5
6
7
8
9
10
# ...
actAs:
  Timestampable:
    created:
      disabled: true
    updated:
      name: updated_date
      onInsert: false
      format: Y-m-d
# ...


Voici la correspondance en PHP :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
/* ... */
$this->actAs('Timestampable', array(
    'created' => array(
        'disabled' => true
    ),
    'updated' => array(
        'name'     => 'updated_date',
        'onInsert' => false,
        'format'   => 'Y-m-d',
    )
));
/* ... */

Les options sont passées en deuxième argument de actAs(), qui lui-même les passe au constructeur du template.
Pour voir toutes les options disponibles, je vous invite à ouvrir le fichier ~/lib/vendor/doctrine/Doctrine/Template/Timestampable.php. Les options par défaut sont définies dans l'attribut $_options. Voyez ci-dessous pour quelques explications sur chacune.

(Notez que cela n'a pas beaucoup de sens de désactiver le champ created_at et de mettre l'option onInsert de updated à false, c'est juste un exemple. ^^ )

Un template possède un comportement très proche d'un objet. Il a, entre autres, la possibilité d'avoir des listeners. Je ne vous en parle pas plus pour l'instant, vous comprendrez leur fonctionnement lorsque nous en créerons un par la suite. ;)
Sachez simplement que cela donne la possibilité d'intervenir juste avant certains évènements, par exemple juste avant la sauvegarde d'un objet dans la base de données. Et c'est précisément comme ça que le template Timestampable fonctionne : juste avant que vous sauvegardiez votre objet (méthode save() par exemple), Doctrine va mettre à jour ces champs avec la valeur de timestamp courante.

Vous n'avez donc jamais à vous soucier de ces champs. La seule chose qui compte pour vous, c'est de pouvoir les récupérer.
Et cela se fait tout naturellement en y accédant comme on le ferait pour n'importe quel autre champ :

Code : PHP
1
2
<?php
echo $article->created_at;


Rien qu'avec ce premier exemple, assez banal en fait, vous voyez la subtilité et la puissance des behaviors. Doctrine fournit un comportement par défaut qui correspond à la plupart des besoins, mais en même temps vous laisse la possibilité de personnaliser chaque détail.

Options disponibles



Les options suivantes sont personnalisables. Elles sont identiques pour chacun des deux champs (created et updated).
  • name : le nom de la colonne (par défaut, created_at et updated_at).
  • alias : un alias pour chaque colonne. Par défaut il n'y en a pas.
  • type : type du champ, par défaut timestamp.
  • format : le format (nécessaire pour un champ de type timestamp/date). Par défaut Y-m-d H:i:s.
  • disabled : si égal à true, désactive cette colonne. Vaut false par défaut.
  • expression : donne la possibilité d'utiliser une Doctrine_Expression pour le champ. Désactivé par défaut.
  • options : options à passer pour la définition de la colonne. Par défaut, indique seulement que la colonne ne peut être vide (NOTNULL).
  • onInsert : utilisé uniquement sur le champ updated. Indique s'il doit être mis à jour aussi lors de la création de l'objet. Par défaut, vaut true.


SoftDelete



Ne vous êtes-vous jamais dit « Ah mince ! Je voulais le garder ! » après avoir supprimé un article (ou n'importe quoi d'autre) ?
SoftDelete est là pour vous !

Je ne vais pas remettre à chaque fois le code de l'implémentation Yaml ou PHP, ça fonctionne à chaque fois de la même manière. ;) Seules les options sont variables.


Ce template vous permet de ne pas supprimer réellement vos objets. Il ajoute une colonne deleted_at dans votre modèle, qui indique la date à laquelle l'objet a été supprimé.
À chaque fois que vous ferez <?php $objet->delete(), votre objet ne sera pas supprimé. À la place, sa colonne deleted_at sera remplie avec la date courante.

Oui mais c'est pas très pratique tout ça, maintenant à chaque fois que je récupèrerai un objet depuis ma base de données, il faudra que je vérifie s'il a été supprimé ou non. C'est nul ton truc !

Vous croyez donc que notre ORM préféré ne gère pas ça ? Erreur ! ^^
En effet, ce template ajoute un « filtrage » automatique de tout ce que vous sélectionnez dans la base. Et il ne vous renverra pas les éléments virtuellement supprimés.
De votre côté, vous ne vous en souciez donc vraiment plus.

Pour cela, il faut changer la valeur d'un attribut de Doctrine_Manager :

Code : PHP
1
2
3
<?php
Doctrine_Manager::getInstance()
    ->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);

En effet, ce template utilise les callbacks DQL dans un listener. Je ne détaille pas plus pour l'instant, mais si vous avez lu le chapitre précédent, vous devriez comprendre de quoi il s'agit. ;)
Placez ceci dans ~/config/global.php par exemple.

Notez que vous pouvez quand même récupérer ces enregistrements supprimés : il faut pour cela le spécifier explicitement dans une requête :
Code : PHP
1
2
3
4
5
<?php
$deletedArticles = Doctrine_Query::create()
    ->from('Article a')
    ->where('a.deleted_at IS NOT NULL') // Si deleted_at contient une valeur (une date), c'est que l'article est supprimé.
    ->execute();


Vous avez aussi la possibilité de supprimer définitivement vos objets :

Code : PHP
1
2
3
4
5
<?php
foreach($deletedArticles as $article)
{
    $article->hardDelete();
}


Comme vous le voyez, le template ajoute aussi une méthode hardDelete() aux objets (oui, un template, ça peut faire ça aussi ^^ ). Maintenant, si vous supprimez des enregistrements, c'est parce que vous l'aurez bien voulu ! ^^

Options disponibles



  • name : nom de la colonne. Par défaut, c'est deleted_at.
  • type : type de la colonne, timestamp par défaut. Vous pouvez aussi utiliser boolean pour simplement indiquer si l'article est supprimé ou non, sans la date.
  • hardDelete : indique si les enregistrements doivent être supprimés définitivement. Par défaut, vaut false. Je ne vous conseille pas de le mettre à true, sinon le template n'est pas extrêmement utile...
  • options : options à passer lors de la construction de la colonne.


Versionable



Ce template vous donne la possibilité de versionner vos objets.
C'est-à-dire que vous conservez tout l'historique des modifications apportées à chaque objet, et que vous avez à tout moment la possibilité de revenir en arrière et d'annuler des modifications.

En fait, Doctrine va créer une deuxième table (dont vous ne vous servirez jamais directement), qui va stocker toutes les anciennes versions. Cette table est nommée avec le suffixe '_version'.
Une colonne version est aussi ajoutée, qui stocke le numéro de version de l'objet.

Du point de vue utilisateur (c'est-à-dire vous ;) ), vous ne vous apercevez de presque rien : vous voyez le numéro de version de votre objet s'incrémenter chaque fois que vous y apportez des modifications et le sauvegardez.

En interne, à chaque modification/suppression, l'ancienne version est déplacée dans la table dédiée, et la dernière version est enregistrée normalement dans la table avec un numéro de version incrémenté.

Pour finir, vous avez la possibilité de restaurer une ancienne version, grâce à la méthode revert() :

Code : PHP
1
2
3
<?php
// Nous restaurons l'objet tel qu'il était dans sa version N°2.
$objet->revert(2);


Attention cependant à ne pas abuser de ce behavior. Le fait d'ajouter une table supplémentaire pour chaque table versionnable, et de sauvegarder toutes les versions, peut finir par faire augmenter considérablement la taille de votre base de données.


Options disponibles



Je vous invite, comme toujours, à aller voir les options disponibles dans ~/lib/vendor/doctrine/Doctrine/Template/Versionable.php.

Voici quelques explications :

  • generateFiles : Doctrine génère une classe supplémentaire pour la table de version. Par défaut, ces classes ne sont pas créées « en dur », mais générées à la volée. Vous pouvez forcer à créer ces fichiers en passant cette option à true.
  • deleteVersions : Indique tout simplement si oui ou non les anciennes versions doivent être supprimées lorsque la version actuelle est supprimée, ou si elles doivent être conservées.


Sluggable



Si vous avez déjà eu envie de faire apparaître le titre de vos articles (par exemple) dans leur URL, vous vous êtes sûrement heurté au fait que certains caractères posent problème.

Doctrine apporte une solution à cela aussi, avec le template Sluggable. Celui-ci va ajouter une colonne slug qui contiendra le titre de vos articles, mais adapté à une URL (suppression des caractères spéciaux, etc.).

Pour indiquer sur quels champs doit être basé le slug, il faut renseigner l'option fields. Dans le cas de notre article, nous indiquerons par exemple la colonne title.

Vous avez aussi la possibilité de forcer cette colonne à être unique. De cette manière, vous pourrez retrouver un objet précis sans utiliser son ID, mais uniquement avec son slug. Pour cela, passez l'option unique à true. Si plusieurs objets ont le même slug, alors un chiffre sera ajouté pour les différencier.
Vous avez la possibilité de déterminer un slug unique avec des champs particuliers : indiquez-les avec uniqueBy.

Options disponibles



Voici un résumé des options :

  • unique : si oui ou non l'unicité du champ doit être garantie. True par défaut.
  • fields : champs à utiliser pour le slug.
  • uniqueBy : champs par lesquels l'unicité est garantie.
  • uniqueIndex : si oui ou non l'index est créé sur la colonne.
  • indexName : nom à donner à l'index.
  • canUpdate : si le slug peut être mis à jour à chaque modification de l'objet, ou s'il est défini une fois pour toutes.
  • builder : fonction à utiliser pour construire le slug.
  • provider : fonction à utiliser pour récupérer une valeur représentative de l'objet pour construire le slug. Est utilisé uniquement si aucun champ n'est indiqué dans l'option fields.

Structure d'un template

J'espère que vous voyez maintenant un peu mieux ce qu'est un template, et à quoi il sert. Voyons un peu à présent comment tout cela est structuré. Vous pourrez ainsi créer vos propres templates. Alors, lisez avec attention ce qui suit ! ;)

Comment ça marche ?



Si vous avez lu le début du paragraphe sur l'utilisation des templates, vous devez vous souvenir qu'un template est une classe (par exemple, Doctrine_Template_Timestampable). Ouvrez donc le fichier ~/lib/vendor/doctrine/Doctrine/Template/Timestampable.php si vous ne l'avez pas déjà fait, vous suivrez mieux ce qui va suivre.

Nous allons étudier cette classe en détail, elle nous servira d'exemple.

Doctrine_Template



Première chose que vous remarquez, elle hérite d'une classe de base : Doctrine_Template. Cette classe fournit les éléments essentiels au template :

  • Des mutateurs pour accéder à l'invoker, c'est-à-dire le dernier enregistrement à avoir utilisé ce template : setInvoker($invoker) et getInvoker().
  • La possibilité d'utiliser un plug-in. Un plug-in est utile lorsque le template implique de faire appel à un fonctionnement relativement complexe. Par exemple, le template Doctrine_Template_Searchable (je ne vous en ai pas encore parlé) fait appel à Doctrine_Search. Le fonctionnement de ce plug-in est indépendant du template, et peut être utilisé de l'extérieur. Je vous laisse trouver les méthodes correspondantes... :p (Doctrine_Template::getPlugin() etc.)
  • Un accesseur vers la table de l'invoker : getTable(). On récupère simplement une instance de Doctrine_Table.


Héritage



Doctrine_Template possède aussi deux méthodes vides : setUp() et setTableDefinition().
Comme pour le modèle, ces méthodes servent à définir, entre autres, la table et les relations.

Les méthodes hasColumn(), hasOne(), hasMany()... sont à utiliser ici.

Et là vous allez me dire...

Eh ! Mais ça ressemble pas un peu à Doctrine_Record ça ?

Si !

Lien avec Doctrine_Record



Si vous êtes un tant soit peu curieux, vous aurez remarqué que Doctrine_Template et Doctrine_Record étendent tous les deux... Doctrine_Record_Abstract !

Image utilisateur


En effet, il faut bien comprendre qu'un objet est fusionné avec les templates qui lui sont associés (c'est une image hein ^^ ). Cela lui permet de partager tout ce que l'on vient de voir, comme des relations, etc. ainsi que des méthodes !

Méthodes déléguées



Un template offre la possibilité d'ajouter des méthodes aux objets et aux tables.

Ajout de méthodes aux objets



Il existe un moyen très simple d'ajouter des méthodes à nos objets : je vous ai dit qu'il fallait considérer un objet et ses templates comme étant fusionnés. Il suffit donc de définir une méthode dans un template, pour qu'elle soit accessible dans tous les objets l'implémentant !

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php

class Doctrine_Template_Timestampable extends Doctrine_Template
{
    /* ... */

    public function isOlderThan($date)
    {
        $date    = new DateTime($date);
        $created = new DateTime($this->getInvoker()->{$this->_options['created']['name']});

        return $date->getTimestamp() > $created->getTimestamp();
    }
}

Nous imaginons ici une méthode isOlderThan() qui indique si la date de création de l'objet est plus ancienne que la date passée en paramètre.

Maintenant, nous pouvons utiliser cette méthode comme ceci :

Code : PHP
1
2
3
4
5
6
7
8
<?php

if($article->isOlderThan("yesterday")) {
  echo "Cet article a été publié avant hier.";
}
else {
  echo "Cet article a été publié hier ou aujourd'hui.";
}


Difficile de faire plus simple, non ? :D

Ajout de méthodes aux tables



Vous avez aussi la possibilité d'ajouter des méthodes aux classes représentant les tables (ArticleTable, etc.).
Il y a simplement une petite subtilité pour les différencier.

Voyez plutôt :
Code : PHP - ~/lib/models/templates/Doctrine_Template_Publishable.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php

class Doctrine_Template_Publishable extends Doctrine_Template
{
    /* ... */

    public function findArticlesOlderThanTableProxy($date)
    {
        $date = new DateTime($date);

        return $this->getTable()
          ->createQuery('a')
          ->where('a.'.$this->_options['created']['name'].' < ?', $date->getTimestamp())
          ->execute();
    }
}


Vous l'aurez deviné, lorsque vous voulez rendre accessible une méthode depuis la classe de table, il faut suffixer son nom de TableProxy.

Et vous pouvez l'utiliser comme ceci :

Code : PHP
1
2
3
4
5
<?php
$table = Doctrine_Core::getTable('Article');

// Ne pas mettre 'TableProxy' lors de l'utilisation de la méthode.
$articles = $table->findArticlesOlderThan("yesterday");
J'espère vous avoir mis l'eau à la bouche avec ce chapitre, parce que maintenant, tout ceci va être mis en pratique. ;)

Dans le prochain chapitre, vous créerez votre propre template. Un lien sera également fait avec le chapitre précédent, en lui attachant un listener.
Chapitre précédent Sommaire Chapitre suivant

Partager

7 commentaires pour "Les templates"
Note moyenne : 3.54 / 4 (35 votes)
Pseudo Commentaire
Hors ligne Nami Doc # Posté le 03/07/2010 à 21:14:41
Lamaer taler dansk

Avis : Très bon

Oui mais je voulais dire prendre cette habitude :p.
Tu devrais peut-être ajouté une partie sur les autres behaviors, même externe, comme "Taggable" que j'aime assez :).

La flemme conquerra le monde !
Secret (cliquez pour afficher)
Image utilisateur
 
Hors ligne Cybermanu # Posté le 03/07/2010 à 21:50:34
Avatar

Avis : Très bon

Ville : Hostun
Pays : France métropolitaine

Ah ok ^^
Ben en fait j'ai l'habitude de l'utiliser surtout dans mes vues (enfin pour l'affichage). Sinon, je suis sensé savoir si j'ai affaire à un objet ou un tableau. Enfin de toute façon, je présente toutes les méthodes, après à chacun de faire comme il veut. Je peux à la limite rajouter ce conseil.

Pour les autres behaviors, je voudrais déjà essayer de finir de passer en revue les internes. Après pourquoi pas en ajouter d'autres.

Et j'envisage aussi de scinder le chapitre en 2, surtout si j'en rajoute encore.

Programmeur : c'est celui qui résout pour toi, de façon incompréhensible, un problème que tu ne savais pas que tu avait... :p
Image utilisateur

Apprenez à utiliser un ORM pour PHP : Doctrine !
 
Hors ligne Nami Doc # Posté le 03/07/2010 à 22:22:34
Lamaer taler dansk

Avis : Très bon

Oui, je pense que scinder le chapitre est une bonne idée :p.

La flemme conquerra le monde !
Secret (cliquez pour afficher)
Image utilisateur
 
Hors ligne Nami Doc # Posté le 04/07/2010 à 23:59:45
Lamaer taler dansk

Avis : Très bon

Ah et aussi tu devrais dire que les méthodes des listeners sont des hooks.

La flemme conquerra le monde !
Secret (cliquez pour afficher)
Image utilisateur
 
Hors ligne Xaviou # Posté le 04/09/2010 à 16:25:32
Avatar

Avis : Très bon

Ville : Cran-gevrier
Pays : France métropolitaine

Hello
Au passage : petite faute : le behavior "Versionable" ne prend qu'un seul "n".

@+
Xav'

Le portail francophone dédié à wxWidgets : wxDev-fr
 

Voir tous les commentaires
Ce tutoriel a été corrigé par les zCorrecteurs.