Development

Documentation/fr_FR/book/1.0/trunk/08-Inside-the-Model-Layer

You must first sign up to be able to contribute.

Version 40 (modified by berduj, 10 years ago)
--

Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

Chapitre 8 - Le Modèle

Jusqu'à présent nous avons abordé la création de page et réalisé quelques opérations simples. Nous allons maintenant nous intéresser à la logique de métier d'une application. Cette logique se trouve en grande partie dans le modèle de données. La composante modèle par défaut de Symfony s'appuie sur la couche d'abstraction objet/relationnel Propel (http://propel.phpdb.org/). Dans une application Symfony, vous accédez aux données d'une base et les modifiez à l'aide d'objets. Vous n'accédez jamais directement à cette base. Ceci permet de conserver un haut niveau d'abstraction et de portabilité.

Ce chapitre vous expliquera comment créer une base de données objet, la façon d'accéder et modifier les données via Propel. Il illustrera son utilisation au sein de Symfony.

Pourquoi utiliser un ORM et une couche d'abstraction ?

Les bases de données sont relationnelles. PHP 5 et Symfony sont objets. Afin d'accéder aux données dans un contexte objet, il est nécessaire d'utiliser une interface traduisant la logique objet en logique relationnel. Comme expliqué au chapitre 1, cette interface est appelée un mapping objet/relationnel (ORM pour objet-relational mapping) et est composée d'objets permettant l'accès aux données tout en isolant la logique de métier.

Le principal avantage d'un ORM est la réutilisabilité, permettant aux méthodes d'un objet d'être appelée de différentes parties d'une application, voir d'autres applications. La couche ORM encapsule aussi la logique de métier, par exemple le calcul du niveau d'un utilisateur d'un forum en fonction du nombre et de la popularité de ses contributions. Lorsqu'une page veut afficher cette information, elle appelle simplement une méthode du modèle de données sans se soucier des détails du calcul. Si le calcul venait à changer, vous n'auriez besoin que de modifier la méthode concernée dans le modèle de données sans toucher quoique ce soit d'autre dans l'application.

L'emploi d'objets à la place d'enregistrements et de classes à la place de tables a un autre avantage: vous pouvez ajouter de nouveaux accesseurs ne correspondant pas forcément à une colonne d’une table. Par exemple, si vous avez une table client contenant deux champs first_name et last_name vous aimeriez peut-être simplement récupérer un nom Name. Dans un environnement orienté objet il est facile d'ajouter une nouvelle méthode accesseur sur la classe Client, comme le montre le listing 8-1. Du point de vue de l'application il n'y a pas de différence entre les attributs FirstName,LastNameet Name de la classe Client. Seule la classe elle-même peut déterminer quel attribut correspond à quelle colonne de quelle table.

Listing 8-1 - Les accesseurs masquent la véritable structure des tables dans la classe Model

[php]
public function getName()
{
  return $this->getFirstName().' '.$this->getLastName();
}

Les actions répétitives d'accès aux données ainsi que la logique de métier peuvent être conservées dans de tels objets. Supposons que vous ayez un Panier dans lequel vous conservez des Items (objets eux aussi). Récupérer la valeur du panier pour affichage nécessite simplement la création d'une méthode dédiée encapsulant le calcul comme le montre le listing 8-2.

Listing 8-2 - Les accesseurs masquent la logique de métier

[php]
public function getTotal()
{
  $total = 0;
  foreach ($this->getItems() as $item)
  {
    $total += $item->getPrice() * $item->getQuantity();
  }

  return $total;
}

Un autre point important à considérer est que les revendeurs de base de données utilisent différentes variantes de la syntaxe SQL. Passer d'un système de gestion de base de données (SGBD) à l'autre vous obligerait à réécrire une bonne partie de vos requêtes SQL écrites explicitement pour l'ancienne base. Alors qu'écrire vos requêtes en utilisant une syntaxe indépendante de tout SGBD et laisser la manipulation de ces requêtes SQL à une tierce application vous permet de passer d'un SGBD à l'autre de façon transparente. Il s'agit précisément du but d'une couche d'abstraction de données : elle vous oblige à utiliser une synthaxe spécifique pour les requêtes, mais prend en charge la mise en conformité de vos ordres avec la syntaxe particulière du SGBD cible et optimise le code SQL.

Le bénéfice majeur de la couche d'abstraction est la portabilité car elle rend le changement de SGBD possible et ce même au beau milieu du projet. Imaginez que vous deviez écrire le prototype d'une application, mais que le client n'ait pas arrêté son choix de SGBD. Vous pouvez commencer à développer l'application avec SQLite puis changer pour MySQL, PostgreSQL ou Oracle une fois que le client aura pris sa décision. Vous n'aurez besoin que de changer une ligne dans un fichier de configuration et ça marchera.

Symfony est basé sur l'ORM Propel, Propel utilise lui même Creole comme abstraction de données. Ces deux composants tiers sont développés par l'équipe Propel et intégrés de façon transparente à Symfony. Vous pouvez les considérer comme parties intégrantes du framework. Leurs syntaxes et conventions, décrites dans ce chapitre, sont adaptées de manière à différer le moins possible de la syntaxe et des conventions de Symfony.

NOTE Dans un même projet Symfony toutes les applications partagent le même modèle. L'organisation par niveau du projet permet de regrouper les applications partageant les mêmes règles de gestion. C'est pourquoi le modèle est indépendant des applications et qu'il est stocké dans le répertoire lib/model à la racine du projet.

Le schéma de base de données de Symfony

Pour créer le modèle objet utilisé par Symfony, vous devez traduire la base relationnelle en modèle objet. L'ORM utilise 'une description du modèle relationnel pour faire cette cartographie : il s'agit du schéma. Dans un schéma vous définissez les tables, leurs relations et les caractéristiques de chaque colonne.

Symfony utilise le format YAML comme syntaxe des schémas. Le fichier schema.yml doit se trouver dans le répertoire myproject/config/

NOTE Symfony peut aussi utiliser le format XML natif de Propel pour les schémas, comme décrit plus bas dans le chapitre "Derrière le schéma.yml: Le schéma.xml"

Exemple de schéma

Comment traduire la structure d'une base de données en un schéma ? Un exemple vaut mieux qu'un long discours. Imaginez que vous avez la base d'un blog composée de deux tables : blog_article et blog_comment avec les structures décrites par l'image 8-1

Image 8-1 - La structure de la base d'un blog La structure de la base d'un blog

Le fichier schema.yml correspondant devrait ressembler au listing 8-3

Listing 8-3 - Exemple de schema.yml

propel:
  blog_article:
    _attributes: { phpName: Article }
    id:
    title:       varchar(255)
    content:     longvarchar
    created_at:
  blog_comment:
    _attributes: { phpName: Comment }
    id:
    article_id:
    author:      varchar(255)
    content:     longvarchar
    created_at:

Vous remarquerez que le nom de la base (blog) n'apparait pas ici. A la place, la base est définie par un nom de connexion (ici propel). Ceci est du au fait que les paramètres de connexion dépendent de l'environnement dans lequel vous travaillez. Par exemple lorsque que vous travaillez dans un environnement de développement vous utilisez surement une base dédiée au développement (quelque chose comme blog_dev) mais avec le même schéma que la base de production. Les paramètres de connexion sont définis au niveau du fichier databases.yml et décrit dans la section "Connexion aux bases" plus loin dans ce chapitre. Le schéma ne contient aucune indication de connexion, si ce n'est le nom de la connexion, et ce dans le but de préserver l'abstraction de données.

La syntaxe standard du schéma

Dans un fichier schema.yml, la première clé représente le nom de la connexion. Elle peut contenir plusieurs tables contenant chacune plusieurs colonnes. Selon la syntaxe YAML, les clés se terminent par le symbole deux-points (:) et la structure est représentée par une indentation (un ou plusieurs espaces mais pas de tabulations).

Une table peut avoir des attributs particuliers, comme phpName (le nom de l'objet php qui sera généré). Si vous ne préciser pas de phpName pour une table, celle-ci sera nommée en fonction de la version camelCase de son nom d'origine.

TIP La convention de nommage camelCase supprime les soulignés et met en majuscule la première lettre de chaque mot. Par défaut les versions camelCase de blog_article et blog_comment sont respectivement BlogArticle and BlogComment. Le nom de cette convention vient du fait que les majuscules au milieu des mots font penser aux bosses d'un chameau (camel en anglais)

Une table contient des colonnes et celles-ci peuvent être définies de trois façons différentes :

  • Si vous ne précisez rien, Symfony déduira les meilleurs attributs au regard du nom de la colonne et de quelques règles qui seront décrites dans la section "Colonnes vides" plus loin dans ce chapitre. Par exemple, la colonne id du listing 8-3 sera définie par Symfony comme integer avec incrément automatique (auto-increment) et clé primaire de la table. La colonne article_id dans blog_comment sera interprétée comme clé étrangère (foreign key) de la table blog_article (les colonnes finissant pas _id sont considérées comme clés étrangères et la table correspondante et déterminée par la première partie du nom de la colonne concernée). Les colonnes appelées created_at sont automatiquement définies comme timestamp. Vous n'avez pas besoin de définir un type pour ces colonnes et c'est une des raisons qui rend schema.ymlaussi facile à écrire.
  • Si vous ne devez définir qu'un seul attribut, ce devrait être le type de la colonne. Symfony accepte les types classiques tels que boolean, integer, float, date, varchar(size), longvarchar (traduit en text dans MySQL) etc. Pour des textes de plus de 256 caractères, vous devrez utiliser le type longvarchar qui n'a pas de taille (mais ne peut pas dépasser 65Kb sur MySQL). Notez que les types date et timestamp sont soumis aux limitations traditionnelles des dates Unix et ne peuvent donc pas être renseignées avec une valeur antérieure au 1er janvier 1970. Pour éviter ce problème (pour les dates de naissance par exemple), vous pouvez employer un format de date "pré Unix" avec bu_date et bu_timestamp ('bu' pour 'before Unix').
  • Si vous avez besoin de définir certains autres attributs (comme la valeur par défaut, obligatoire etc.) vous devrez écrire ces attributs comme un ensemble de clé:valeur. Il s'agit de la syntaxe avancée décrite plus loin dans ce chapitre.

Les colonnes aussi peuvent avoir l'attribut phpName, qui n'est que la mise en majuscule de la première lettre du nom (Id, Title, Content, etc.), mais il est peu utilisé.

Le tables peuvent aussi posséder des clés étrangères et des indexes explicitement ainsi que certaines définitions de structures dépendante d'un SGDB. Reportez-vous à la section "Syntaxe avancée du schéma" plus loin dans ce chapitre pour en apprendre plus.

Les classes du modèle

Le schéma est utilisé pour créer les classes du modèle de la couche ORM. Pour économiser du temps, les classes sont générées en ligne de commande grâce à la commande propel-build-model.

> symfony propel-build-model

Cette commande lance l'analyse du schéma et la génération des classes de base du modèle de données dans le répertoire lib/model/om/ de votre projet :

  • BaseArticle.php
  • BaseArticlePeer.php
  • BaseComment.php
  • BaseCommentPeer.php

En outre, les classes réelles d'accès au modèle de données seront créées lib/model/:

  • Article.php
  • ArticlePeer.php
  • Comment.php
  • CommentPeer.php

Vous avez défini deux tables et vous vous retrouvez avec huit fichiers. Il n'y a pas d'erreur mais cela mérite quelques explications.

Les classes de base et les classes personnalisables

Pourquoi conserver deux modèles objet de données à deux différents emplacements ?

Vous devrez probablement ajouter des propriétés et des méthodes personnalisées au modèle objet (rappelez-vous la méthode getName() du listing 8-1). Mais au long de l'avancement de vos développements vous devrez surement ajouter d'autres tables et colonnes. A chaque fois que vous modifierez le fichier schema.yml vous devrez régénérer les classes du modèle objet en exécutant la commande propel-buil_model. Si vos personnalisations ont été réalisées sur les classes réellement générées, elles seront effacées à chaque nouvelle génération.

Les classes Base conservées dans le répertoire lib/model/om/ sont celles directement générées à partir du schéma. Vous ne devriez jamais les modifier puisque chaque nouvelle construction du modèle supprime entièrement ces fichiers.

D'autre part, les classes personnalisés stockées dans le répertoire lib/model/, héritent des classes de Base. Lorsque vous exécutez la commande propel-build-model sur un modèle existant, ces classes ne sont pas modifiées. Par conséquent c'est ici que vous pouvez ajouter et/ou personnaliser vos méthodes.

Le listing 8-4 montre un exemple d'une classe personnalisée du modèle créée par la première exécution de la commande propel-build-model.

Listing 8-4 - Exemple de classe du modèle, dans lib/model/Article.php

[php]
<?php

class Article extends BaseArticle
{
}

Elle hérite de toutes les méthodes de la classe BaseArticle, mais une modification du schéma ne l'affecte pas.

Ce mécanisme vous permet de commencer à coder sans pour autant connaitre la relation définitive entre le modèle et votre base de données et rend le modèle plus personnalisable et plus évolutif.

Les objets et classes Peer

Article et Comment sont des classes d'objets représentant un enregistrement de la base. Elles permettent d'accéder aux attributs de cet enregistrement. Vous pouvez ainsi connaitre le titre d'un article en appelant la méthode approprié de l'objet Article, comme le montre le listing 8-5.

Listing 8-5 - Des accesseurs sont disponibles dans les classes des objets

[php] $article = new Article(); ... $title = $article->getTitle();

ArticlePeer et CommentPeer sont des classes Peer, ce qui veut dire qu'elles contiennent des méthodes statiques manipulant les tables. Ces méthodes renvoient le plus souvent un objet ou un ensemble d'objet à partir l'objet concerné, comme le montre le listing 8-6.

Listing 8-6 - Des méthodes statiques récupérant les enregistrements sont disponibles dans les classes Peer.

[php]
$articles = ArticlePeer::retrieveByPks(array(123, 124, 125));
// $articles est tableau d’objets de classe Article

NOTE Du point de vue de la base de données, il ne peut y avoir aucun objet Peer et c'est pourquoi ces méthodes sont appelées avec un '::' (pour les appels de méthode statique) plutôt que par l'habituel '->'. (pour les appels de méthode objet)

Donc mélanger les classes objets et Peer dans une base ainsi que les versions personnalisables implique la génération de quatre classes par table dans le schéma. En fait il en existe une cinquième dans le répertoire lib/model/map/, qui contient les métadonnées à propos de la table dont le runtime à besoin. Mais vous ne modifierez vraisemblablement jamais cette classe, nous ne nous attarderons pas plus dessus.

Accéder aux données

Avec Symfony, vos données sont accessibles via des objets. Si vous êtes habitué au modèle relationnel et à employer SQL pour retrouver et modifier vos données, le modèle objet vous semblera sans doute compliqué. Mais lorsque vous aurez gouté à la puissance de l'orientation objet pour l'accès aux données, vous ne pourrez plus vous en passer.

Pour commencer, assurons-nous que nous parlons bien de la même chose. Les modèles relationnel et objet emploient des concepts similaires mais avec des nomenclatures différentes :

           Relationnel | Orienté Objet
        -------------- | ---------------
        Table          | Classe
 Ligne, enregistrement | Objet
        Champ, colonne | Propriété

Récupérer la valeur d'une colonne

Lorsque Symfony construit le modèle, il crée une classe de base par table définie dans le fichier schema.yml. Chacune d'elles possède des constructeurs, accesseurs et manipulateurs par défaut. Les méthodes new, getXXX(), et setXXX() servent à créer des objets et à accéder aux propriétés de ces objets, comme le montre le listing 8-7.

Listing 8-7 - Méthodes des classes générées

[php]
$article = new Article();
$article->setTitle('My first article');
$article->setContent('This is my very first article.\n Hope you enjoy it!');

$title   = $article->getTitle();
$content = $article->getContent();

NOTE Dans l'exemple précédent, la classe générée est appelée Article, qui est le nom donné à la table blog_article par phpName. Si ce dernier n'avait pas été défini, la classe se serait appelée BlogArticle. Les accesseurs et manipulateurs appliquent une variante de la convention camelCase sur les colonnes. Ainsi la méthode getTitle() récupère la valeur de la colonne title.

Pour manipuler plusieurs champs simultanément, vous pouvez utiliser la méthode fromArray() aussi générée pour chaque classe, comme le montre le listing 8-8.

Listing 8-8 - La méthode fromArray() est un manipulateur multiple.

[php]
$article->fromArray(array(
  'title'   => 'My first article',
  'content' => 'This is my very first article.\n Hope you enjoy it!',
));

Récupérer des enregistrements liés

La colonne article_id dans la table blog_comment définit implicitement une clé étrangère dans la table blog_article. Chaque commentaire est lié à un article et un article peut avoir plusieurs commentaires. Les classes générées contiennent cinq méthodes pour traduire ce comportement relationnel et orienté objet:

  • $comment->getArticle(): Pour récupérer l'objet Article lié.
  • $comment->getArticleId(): Pour récupérer l'ID de l'objet Article lié.
  • $comment->setArticle($article): Pour définir l'objet Article lié
  • $comment->setArticleId($id): Pour définir l'objet Article lié à partir d'un ID
  • $article->getComments(): Pour récupérer l'objet Comment liés

Les méthodes getArticleId() et setArticleId() montrent que vous pouvez considérer la colonne article_id comme une véritable colonne et la manipuler directement, mais cela n'a pas vraiment d'intérêt. Le vrai bénéfice d'une approche orienté objet est plus visible dans les trois autres méthodes. Le listing 8-9 montre comment utiliser les accesseurs générés.

Listing 8-9 - Les clés étrangères sont converties en accesseurs particuliers

[php]
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->setContent('Gee, dude, you rock: best article ever!');

// Attacher ce commentaire à l'objet $article
$comment->setArticle($article);

// Syntaxe alternative
// N'a un sens que si l'objet a déjà été sauvegardé dans la base de données
$comment->setArticleId($article->getId());

Le listing 8-10 montre comment utiliser les accesseurs générés. Il démontre aussi comment enchainer des appels aux méthodes dans un modèle objet.

Listing 8-10 - Les clés étrangères sont converties en accesseurs particuliers

[php]
// Relation plusieurs à un (Many to one)
echo $comment->getArticle()->getTitle();
 => My first article
echo $comment->getArticle()->getContent();
 => This is my very first article.
    Hope you enjoy it!

// Relation un à plusieurs (One to many) 
$comments = $article->getComments();

La méthode getArticle() renvoie un objet de classe Article bénéficiant immédiatement de l'accesseur getTitle(). Ceci est bien mieux que réaliser la jointure soit même, ce qui prendrait bien plus de lignes de code.

La variable $comments (notez le 's' en plus) du listing 8-10 contient un tableau de classes d'objets. Vous pouvez afficher le premier avec l'ordre $comments[0] ou parcourir l'ensemble de valeurs grace à l'instruction foreach ($comments as $comment)

NOTE Vous comprenez maintenant pourquoi les noms des objets du modèle sont au singulier. La clé étrangère définie dans la table blog_comment implique la création de la méthode getComments(), nommée en ajoutant un s au nom de l'objet Comment. Si le nom de l'objet du modèle avait été au pluriel, la génération aurait alors nommé cette méthode getCommentss(), ce qui n'a pas de sens.

Sauvegarder et supprimer des données

En appelant le constructeur new, vous créez un nouvel objet mais pas un nouvel enregistrement dans la base. Modifier un objet n'a aucun effet sur la base. Pour sauvegarder les données dans la base vous devez appeler la méthode save() de l'objet.

[php]
$article->save();

L'ORM est suffisamment intelligent pour déduire les relations entre les objets, et donc sauvegarder l'objet $article sauvegardera aussi l'objet lié $comment. Il peut aussi détecter l'existence ou pas de l'objet dans la base de données et donc traduire la méthode save() par l'instruction SQL INSERT ou UPDATE en fonction des cas. La clé primaire est automatiquement mise à jour par la méthode save(), donc après la sauvegarde, vous pouvez immédiatement récupérer cette clé primaire avec $article->getId().

TIP Vous pouvez vérifier si un objet est nouveau en appelant la méthode isNew(). Si vous vous demandez si un objet a été modifié et nécessite une sauvegarde, appelez la méthode isModified().

En lisant les commentaires sur vos articles, vous pourriez ne plus vouloir publier sur Internet. Et si vous n'appréciez pas l'ironie de vos lecteurs, vous pouvez facilement supprimer les commentaires avec la méthode delete(), comme le montre le listing 8-11.

Listing 8-11 - Supprimer des enregistrements de la base avec la méthode delete() de l'objet concerné.

[php]
foreach ($article->getComments() as $comment)
{
  $comment->delete();
}

TIP Même après l'appel de la méthode delete() un objet reste disponible jusqu'à la fin de l'exécution de la requête. Pour savoir si un objet a réellement été supprimé de la base, employez la méthode isDeleted().

Récupérer des enregistrements pas la clé primaire.

Si vous connaissez la clé primaire d’un enregistrement précis, utilisez la méthode retrieveByPk()de la classe Peer pour obtenir l'objet correspondant.

[php]
$article = ArticlePeer::retrieveByPk(7);

Le champ id est défini comme clé primaire de la table blog_article dans le fichier schema.yml, ce qui implique que l'ordre précédent renverra l'article dont l'id est 7. Comme vous utilisez une clé primaire vous êtes certain de ne récupérer qu'un seul enregistrement, la variable $article contenant évidemment un objet Article.

Dans certains cas une clé primaire peut-être composée de plusieurs colonnes. Dans ce cas la méthode retrieveByPks() peut accepter plusieurs paramètres correspondant à chaque colonnes clé.

Vous pouvez aussi récupérer plusieurs objets par leur clé primaire en appelant la méthode retrieveByPKs() avec un tableau de clés primaires en paramètre.

Récupérer des enregistrements avec un critère

Si vous souhaitez récupérer plusieurs enregistrements, vous devrez utiliser la méthode Peer doSelect() de l'objet correspondant. Par exemple, pour récupérer des objets de la classe Article, vous devrez utiliser ArticlePeer::doSelect().

Le premier paramètre de la méthode doSelect() est un objet de classe Criteria, qui contient une définition de requête dépourvue de SQL pour le respect de l'abstraction de donnée.

Un Criteria vide renvoie tous les objets de la classe. Par exemple, le code du listing 8-12 renvoie tous les articles.

Listing 8-12 - Récupérer des enregistrements par critères avec doSelect() -- Critère non renseigné.

[php]
$c = new Criteria();
$articles = ArticlePeer::doSelect($c);

// Aboutira à la requête SQL suivante
SELECT blog_article.ID, blog_article.TITLE, blog_article.CONTENT,
       blog_article.CREATED_AT
FROM   blog_article;

SIDEBAR Hydrating

L'utilisation de ::doSelect() est en réalité bien plus puissant qu'une simple requête SQL. Tout d'abord le SQL est optimisé pour le SGBD que vous utilisez. Ensuite, toutes les valeurs passées à Criteria sont échappées avant d'être intégrées dans le code SQL afin d'éviter les attaques par injection SQL. Enfin, la méthode renvoie un tableau d'objets plutôt qu'un ensemble de résultats. L'ORM crée et alimente automatiquement les objets en fonction des résultats. Ce comportement est appelé l'hydrating.

Pour des sélections plus complexes, vous aurez entre autres besoin des équivalents des ordres WHERE, ORDER BY, GROUP BY de SQL. L'objet Criteria possède des méthodes et des paramètres interprétant toutes ces conditions. Par exemple, pour obtenir tous les commentaires écrit par Steve, triés par date, renseignez le Criteria comme dans le listing 8-13.

Listing 8-13 - Récupérer des enregistrements par critères avec doSelect() -- Critère avec conditions

[php]
$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);

// Correspond à la requête SQL suivante: 
SELECT blog_comment.ARTICLE_ID, blog_comment.AUTHOR, blog_comment.CONTENT,
       blog_comment.CREATED_AT
FROM   blog_comment
WHERE  blog_comment.author = 'Steve'
ORDER BY blog_comment.CREATED_AT ASC;

Les constantes de classe passées comme paramètres de la méthode add() correspondent aux noms des propriétés passé en majuscule. Par exemple, pour manipuler la colonne content de la table blog_articlevous utiliserez la constante de classe ArticlePeer::CONTENT

NOTE Pourquoi préférer CommentPeer::AUTHOR à la place de blog_comment.AUTHOR qui sera au final la valeur de la requête SQL exécutée ? Imaginez que vous devez changer le nom du champ auteur par collaborateur dans la base. Si vous utilisez blog_comment.AUTHOR vous devrez alors faire la modification dans toutes les requêtes au modèle. En préférant CommentPeer::AUTHOR vous ne modifiez que le nom dans schema.yml, conservez AUTHOR pour phpName et regénérez le modèle.

La table 8-1 montre les équivalences entre la syntaxe SQL et la syntaxe de l'objet Criteria

Table 8-1 - Syntaxe SQL et Criteria SQL | Criteria ------------------------------------------------------------ | ----------------------------------------------- WHERE colonne = valeur | ->add(colonne, valeur); WHERE colonne <> valeur | ->add(colonne, valeur, Criteria::NOT_EQUAL); Autres opérateurs de comparaison | > , < | Criteria::GREATER_THAN, Criteria::LESS_THAN >=, <= | Criteria::GREATER_EQUAL, Criteria::LESS_EQUAL IS NULL, IS NOT NULL | Criteria::ISNULL, Criteria::ISNOTNULL LIKE, ILIKE | Criteria::LIKE, Criteria::ILIKE IN, NOT IN | Criteria::IN, Criteria::NOT_IN Autres mots clés SQL | ORDER BY colonne ASC | ->addAscendingOrderByColumn(colonne); ORDER BY colonne DESC | ->addDescendingOrderByColumn(colonne); LIMIT limite | ->setLimit(limite) OFFSET offset | ->setOffset(offset) FROM table1, table2 WHERE table1.col1 = table2.col2 | ->addJoin(col1, col2) FROM table1 LEFT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::LEFT_JOIN) FROM table1 RIGHT JOIN table2 ON table1.col1 = table2.col2 | ->addJoin(col1, col2, Criteria::RIGHT_JOIN)

TIP Le meilleur moyen de découvrir les méthodes disponibles dans les classes générées est de jeter un coup d'œil aux fichiers Base du répertoire lib/model/om/ après la génération. Les noms des méthodes sont suffisamment explicite, mais si vous avez besoin de plus de commentaires, passez le paramètre propel.builder.addComments à true dans le fichier config/propel.ini et générez le modèle.

Le listing 8-14 montre d'autres exemples de Criteria à conditions multiples. On récupère alors tous les commentaires de Steve comprenant le mot "enjoy" , triés par date.

Listing 8-14 - Autres exemples de récupération d'enregistrements par Criteria avec doSelect() -- Critère avec condition

[php]
$c = new Criteria();
$c->add(CommentPeer::AUTHOR, 'Steve');
$c->addJoin(CommentPeer::ARTICLE_ID, ArticlePeer::ID);
$c->add(ArticlePeer::CONTENT, '%enjoy%', Criteria::LIKE);
$c->addAscendingOrderByColumn(CommentPeer::CREATED_AT);
$comments = CommentPeer::doSelect($c);

// La requête correspondante est :
SELECT blog_comment.ID, blog_comment.ARTICLE_ID, blog_comment.AUTHOR,
       blog_comment.CONTENT, blog_comment.CREATED_AT
FROM   blog_comment, blog_article
WHERE  blog_comment.AUTHOR = 'Steve'
       AND blog_article.CONTENT LIKE '%enjoy%'
       AND blog_comment.ARTICLE_ID = blog_article.ID
ORDER BY blog_comment.CREATED_AT ASC

Tout comme SQL est un langage simple qui vous permet de construire des requêtes très complexes, l'objet Criteria permet de manipuler des conditions de différents niveaux de complexité. Beaucoup de développeurs réfléchissent en SQL avant de penser orienté objet, donc pour ceux qui sont habitué à SQL plutôt qu'à la logique objet, le concept de Criteria risque d'être difficile à comprendre au premier abord. La meilleure façon l'appréhender est d'apprendre par l'exemple. Le site de Symfony regorge d'exemples d'utilisation de Criteria qui pourront vous éclairer de bien des manières.

En plus de la méthode doSelect(), chaque classe Peer possède une méthode doCount(), qui compte le nombre d'enregistrements correspondant à une requête. Le résultat est un entier. Etant donné que dans ce cas précis aucun objets n'est traité, l'hydrating n'intervient pas. La méthode doCount() est donc plus rapide que doSelect()

Les classes Peer possèdent aussi les méthodes doDelete(), doInsert() et doUpdate() qui acceptent toute un paramètre Criteria Ces méthodes vous permette d'exécuter les ordres DELETE, INSERT et UPDATE sur vos bases. Pour en savoir plus à propos de ces méthodes Propel, regardez les classes Peer générées.

Enfin, si vous ne souhaitez récuperer qu'un seul objet et que vous êtes certain que le Criteria ne correspond qu'à un seul résultat, utilisez la méthode doSelectOne() plutôt que doSelect(). L'avantage est que cette méthode renvoie un objet à la place d'un tableau d'objets.

TIP Lorsqu'un doSelect() renvoie un très grand nombre de résultats, vous pouvez préférer n'afficher qu'une partie de ces résultats. Pour ce faire, Symfony propose une classe de pagination sfPropelPager, qui automatise la pagination des résultats. Pour en apprendre plus à ce propos, consultez la documentation http://www.symfony-project.org/cookbook/trunk/pager

Utilisation de requêtes SQL brut.

Vous aurez parfois besoin de résultats particuliers, calculés par la base de données plutôt que d'objets. Par exemple, pour récupérer le dernier article créé (en foction de la date de création), il serait stupide de récupérer tous les articles et de boucler sur le tableau résultat. Il serait préférable que la base puisse vous fournir immédiatement ce résultat, car cela éviterait tout le processus d'hydrating

De plus, il serait préférable de ne pas faire appel aux commandes PHP pour ce faire, parce que vous perdriez tout le bénéfice de l'abstraction de données. Ceci veut donc dire que vous avez besoin de vous substituer à l'ORM (Propel) mais pas à l'abstraction de données (Creole)

Interroger la base avec Creole nécessite qui vous suiviez les étapes suivantes :

  1. Se connecter à la base.
  2. Construire une requête.
  3. Créer une déclaration.
  4. Boucler sur le résultat produit par la requête de la déclaration.

C'est surement du charabia pour vous, mais le listing 8-15 devrait être un peu plus explicite.

Listing 8-15 - Une requête personnalisée avec Creole

[php]
$connection = Propel::getConnection();
$query = 'SELECT MAX(%s) AS max FROM %s';
$query = sprintf($query, ArticlePeer::CREATED_AT, ArticlePeer::TABLE_NAME);
$statement = $connection->prepareStatement($query);
$resultset = $statement->executeQuery();
$resultset->next();
$max = $resultset->getInt('max');

Comme pour Propel, les requêtes Creole ne sont pas évidentes à appréhender au départ. Une fois de plus, les exemples des applications existantes et les tutoriels vous permettront de mieux comprendre le principe.

CAUTION Si vous êtes tenté d'accéder directement à la base sans passer par ce procédé, vous risquez de perdre la sécurité et l'abstraction offertes par Creole. Utiliser Creole est certes plus long, mais garantit les performances, la portabilité et la sécurité de votre application. Ceci est d'autant plus vrai lorsque les requêtes contiennent des informations provenant d’une source non sûre (comme un utilisateur internet lambda). Creole effectue les échappements et autres opérations de sécurité nécessaires à votre base. Se passer de Creole vous expose aux attaques par injection de SQL.

Utilisation des colonnes de dates spéciales

Habituellement, lorsque table possède une colonne nommée created_at, elle est utilisé pour stocker la date de création de l'enregistrement au format timestamp. Il en est de même pour la colonne updated_at qui est mise à jour à chaque fois que l'enregistrement est modifié.

La bonne nouvelle est que Symfony reconnait les noms de ces colonnes et gère leur mise à jour pour vous. Vous n'avez pas à vous soucier de la mise à jour des colonnes created_at ni updated_at. Leur mise à jour devient transparente comme le montre le listing 8-16. Il en est de même pour les colonnes created_on et updated_on

Listing 8-16 - Les colonnes created_at et updated_at sont gérées automatiquement.

[php]
$comment = new Comment();
$comment->setAuthor('Steve');
$comment->save();

// Affiche la date de création
echo $comment->getCreatedAt();
  => [date of the database INSERT operation]

De plus, les accesseurs des colonnes date acceptent le format date comme argument:

[php]
echo $comment->getCreatedAt('Y-m-d');

SIDEBAR

Refactorisation du modèle de données

Quand on développe un projet avec Symfony, on commence souvent par coder la logique au sein des actions. Mais les requêtes à la base et la manipulation du modèle ne doivent pas être stockées au niveau du contrôleur. Ainsi toute la logique concernant les données doit être déplacée dans le modèle. Si vous êtes amené à écrire plusieurs fois la même requête à différents endroits, vous devriez envisager de transférer ce code au niveau du modèle. Cela aide à conserver un minimum de code lisible pour les actions.

Par exemple, imaginez le code nécessaire à la récupération des dix articles les plus populaires pour un tag donné (passé en paramètre à la requête). Ce code ne devrait pas se trouver dans une action mais dans le modèle. En fait, vous devriez afficher cette liste via un gabarit et l'action correspondante devrait ressembler à ça :

[php]
public function executeShowPopularArticlesForTag()
{
  $tag = TagPeer::retrieveByName($this->getRequestParameter('tag'));
  $this->foward404Unless($tag);
  $this->articles = $tag->getPopularArticles(10);
}

L'action crée une classe d'objet Tag. Ainsi tout le code nécessaire à l'interrogation de la base est localisé dans la méthode getPopularArticles() de cette classe. Cela rend le code plus lisible et ce code peut facilement être réutilisé par une autre action.

Déplacer du code dans un endroit plus approprié est une des techniques de refactorisation. Si vous l'adoptez comme habitude, le code sera plus compréhensible et plus facile à maintenir pour un autre développeur. Nous pouvons admettre qu'une bonne règle pour savoir quand refactoriser la couche de donnée, est de ne pas dépasser dix lignes de code PHP par action.

Connexion à la base de données

Le modèle de données est indépendant de la base utilisée, mais vous êtes obligé d'utiliser une base de données. Les informations minimums nécessaires à Symfony pour interroger les bases de données sont le nom, le code d'accès et le type de base de données. Ces informations doivent être précisées dans le fichier databases.yml du répertoire config. Le listing 8-17 monte l'exemple d'un tel fichier.

Listing 8-17 - Exemple de paramétrage d'une connexion à une base de données, dans myproject/config/databases.yml

prod:
  propel:
    param:
      hostspec:           mydataserver
      username:           myusername
      password:           xxxxxxxxxx

all:
  propel:
    class:                sfPropelDatabase
    param:
      phptype:            mysql     # Database vendor
      hostspec:           localhost
      database:           blog
      username:           login
      password:           passwd
      port:               80
      encoding:           utf8      # Default charset for table creation
      persistent:         true      # Use persistent connections

Les paramètres de connexion sont dépendants de l'environnement. Vous pouvez définir des paramétrages différents pour les environnements prod, dev et test ou tout autre environnement. De même ce paramétrage peut être définit par application en renseignant des valeurs particulières dans des fichiers de configuration spécifiques aux applications comme apps/myapp/config/databases.yml. Par exemple, vous pouvez utiliser cette approche pour définir des politiques de sécurité différentes pour vos applications de front-end ou back-end, et définir différents utilisateurs avec des privilèges différents pour gérer tout ça.

Vous pouvez définir plusieurs connexions pour chaque environnement. Chaque connexion pointe vers un schéma identifié par le même nom. Dans l'exemple 8-17, la connexion Propel pointe vers le schéma propel du listing 8-3.

Les valeurs autorisées pour le paramètre phptype sont les noms des bases supportées par Creole :

  • mysql
  • sqlserver
  • pgsql
  • sqlite
  • oracle

hostspec, database, username et password sont les paramètres traditionnels d'une connexion. Le paramétrage peut aussi être écrit d'une manière plus rapide via DSN (Data Source Name). Le listing 8-18 est équivalent à la section all: du listing 8-17.

Listing 8-18 - Syntaxe DSN du paramétrage de la connexion

all:
  propel:
    class:          sfPropelDatabase
    param:
      dsn:          mysql://login:passwd@localhost/blog

Si vous utilisez la base SQLite, le paramètre hostspec doit correspondre au chemin des fichiers de la base de données. Par exemple, si vous conservez la base de votre blog dans data/blog.db, alors le fichier databases.yml devrait ressembler au listing 8-19.

Listing 8-19 - Le paramétrage d'une connexion sur SQLite utilise un chemin comme host

    all:
      propel:
        class:          sfPropelDatabase
        param:
          phptype:  sqlite
          database: %SF_DATA_DIR%/blog.db

Modèle étendu

Le modèle généré est bien mais pas suffisant. Dès que vous implémenterez votre propre logique de métier, vous aurez besoin d'étendre le modèle en ajoutant des méthodes et/ou en remplaçant d'autres.

Ajouter des nouvelles méthodes.

Vous pouvez ajouter des méthodes aux classes vides du modèle générées dans le répertoire lib/model/. Utilisez $thispour appeler une méthode de l'objet courant et self:: pour appeler des méthodes statique de la classe courante. Souvenez-vous que les classes particulières héritent des méthodes des classes Base correspondantes du répertoire lib/model/om/.

Par exemple, pour l'objet Article généré comme le montre le listing 8-3, vous pouvez ajouter la méthode magique __toString() pour que l'affichage d'un objet Article affiche aussi sont titre comme le montre le listing 8-20.

Listing 8-20 - Personnaliser le modèle dans lib/model/Article.php

[php]
<?php

class Article extends BaseArticle
{
  public function __toString()
  {
    return $this->getTitle();  // getTitle() est hérité de BaseArticle
  }
}

Vous pouvez aussi étendre les classe Peer. Par exemple, ajouter une méthode qui récupère tous les articles triés par date de création comme le montre le listing 8-21.

Listing 8-21 - Personnaliser le modèle dans lib/model/ArticlePeer.php

[php]
<?php

class ArticlePeer extends BaseArticlePeer
{
  public static function getAllOrderedByDate()
  {
    $c = new Criteria();
    $c->addAscendingOrderByColumn(self::CREATED_AT);
    return self::doSelect($c);

  }
}

Les nouvelles méthodes sont accessibles de la même manière que la méthode générée comme vous pouvez le voir sur le listing 8-22

Listing 8-22 - Utiliser les méthodes personnalisées comme on utilise les méthodes générées.

[php]
foreach (ArticlePeer::getAllOrderedByDate() as $article)
{
  echo $article;      // Appellera la méthode magique __toString()
}

Se substituer aux méthodes existantes.

Si certaines méthodes des classes Base ne correspondent pas exactement à vos besoins, vous pouvez y le substituer par des classes personnaliser. Vous devez vous assurez qu'elles aient la même signature (c'est à dire le même nombre de paramètres).

Par exemple, la méthode $article->getComments() renvoie un tableau d'objets Comment sans ordre particulier. Si vous préférez que ces résultats soient classés par date de création par ordre décroissantes alors vous devrez remplacer la méthode getComments() comme indiqué dans le listing 8-23. Etant donné que la méthode originale getComments() (de lib/model/om/BaseArticle.php) attends un critère et une connexion comme paramètres, votre méthode personnalisée doit en faire de même.

Listing 8-23 - Substituer les méthodes existantes du modèle dans lib/model/Article.php

[php]
public function getComments($criteria = null, $con = null)
{
  if (is_null($criteria))
  {
    $criteria = new Criteria();
  }
  else
  {
    // Les objets sont passé comme références dans PHP5, par conséquent et afin d'éviter de modifier l'original
    // vous devez la cloner
    $criteria = clone $criteria;
  }
  $criteria->addDescendingOrderByColumn(CommentPeer::CREATED_AT);

  return parent::getComments($criteria, $con);
}

Même s'il est bien qu'une méthode personnalisée puisse éventuellement appeler une méthode de la classe Base parente, vous pouvez entièrement court-circuiter la classe et renvoyer le résultat que vous souhaitez.

Utiliser les behaviors (comportements) du modèle

Certaines modifications du modèle sont génériques et peuvent être réutilisées, comme des méthodes permettant le tri d'objets et optimisant le verrouillage prévenant ainsi la collision d'objets. Il s'agit alors d’extensions génériques pouvant être ajoutés à une multitude de classes.

Symfony regroupe ces extensions dans des behaviors qui sont alors considérés comme des classes externes ajoutant des méthodes aux classes du modèle. Ces dernières contiennent des balises pour que Symfony sache où intégrer ces méthodes externes : sfMixer. (Rendez vous au chapitre 17 pour plus d'informations)

Pour activer les behaviors dans les classes du modèle, vous devez modifier le fichier config/propel.ini comme suit :

propel.builder.AddBehaviors = true     // Valeur false par défaut

Il n'y a as pas de behaviors par défaut dans Symfony, mais vous pouvez les installer vie les plug-ins. Un fois fait, vous pouvez assigner un behavior à une classe grâce à une simple ligne. Si vous souhaitez par exemple installer sfPropelParanoidBehaviorPlugin dans votre application, vous pouvez étendre la classe Article avec ce behavior en ajoutant ce qui suit à la fin de Article.class.php :

[php]
sfPropelBehavior::add('Article', array(
  'paranoid' => array('column' => 'deleted_at')
));

Après avoir reconstruit le modèle, les objets Article supprimés resteront dans la base, invisibles aux requêtes employant Propel, et jusqu'à ce que vous désactiviez le behavior avec sfPropelParanoidBehavior::disable().

Dans la liste des plug-ins de Symfony située sur le Wiki (http://trac.symfony-project.com/wiki/SymfonyPlugins#Propelbehaviorplugins). , vous trouverez des behaviors. Chacun possède sa propre documentation et son propre guide d'installation.

Syntaxe étendue du schéma

Le fichier schema.yml peut être aussi simple que le listing 8-3, mais comme le modèle relationnel peut devenir assez complexe, le schéma possède une syntaxe étendue pour gérer cette complexité.

Les attributs

Les connexions et les tables possèdent aussi des attributs spécifiques, comme le montre le listing 8-24. Ils sont défini par la clé _attributes.

Listing 8-24 - Attributs des connections et des tables

propel:
  _attributes:   { noXsd: false, defaultIdMethod: none, package: lib.model }
  blog_article:
    _attributes: { phpName: Article }

Vous pouvez souhaitez que votre schéma soit valider avant d'être générée. Pour ce faire, vous devez désactiver l'attribut noXSD de votre connexion. Votre connexion contient aussi l'attribut defaultIdMethod qui renseigné à native précise d'utiliser les méthodes natives du SGBD en cours pour définitions des ID. Par exemple pour MySQL autoincrement ou sequence pour PostegreSQL. L'autre valeur possible est none

L'attribut package agit comme un espace de nommage : il indique où, seront conservées les classes générées. Par défaut il est renseigné avec lib/model/, mais vous pouvez le modifier pour organiser votre modèle en sous package. Si par exemple vous souhaitiez séparer les classes métier des classes statistiques, vous pourriez définir les packages suivant dans le modèle :lib.model.business et lib.model.stats.

Vous savez déjà que l'attribut phpName est utilisé pour définir le nom des tables générées.

Les tables dont le contenu est localisé (c'est à dire, plusieurs versions d'un même contenu dans des tables liées pour internationalisation) possèdent deux attributs supplémentaires comme indiqué sur le listing 8-25 (voir le chapitre 13 pour plus d'informations) :

Listing 8-25 - Attributs pour les tables i18n (internationalisation)

propel:
  blog_article:
    _attributes: { isI18N: true, i18nTable: db_group_i18n }

SIDEBAR Gérer plusieurs schémas

Vous pouvez avoir plus d’un schéma par application. Symfony prend en compte tout fichier terminant par schema.yml ou schema.xml

trouver dans le répertoire config/. Vous trouverez ce système très utile si votre application possède plusieurs tables ou si vos tables ne partagent pas la même connexion. > >Considérez ces deux schémas :> > > // Dans config/business-schema.yml > propel: > blog_article: > attributes: { phpName: Article } > id: > title: varchar(50) > > // Dans config/stats-schema.yml > propel: > statshit: > attributes: { phpName: Hit } > id: > resource: varchar(100) > createdat: > > >Les deux schémas partagent la même connexion (propel) et les classes Article et Hit seront générées toutes les deux dans le répertoire lib/model/. Tout se déroule comme si vous n'aviez créé qu'un seul schéma. > >Vous pouvez aussi avoir des schémas distincts utilisant des connexions différentes (par exemple propel et propel_bis, devant être défini dans databases.yml) répartissant les classes générées comme suit : > > > // Dans config/business-schema.yml > propel: > blog_article: > attributes: { phpName: Article, package: lib.model.business } > id: > title: varchar(50) > > // Dans config/stats-schema.yml > propelbis: > stats_hit: > attributes: { phpName: Hit, package.lib.model.stat } > id: > resource: varchar(100) > createdat: > > >Beaucoup d'application utilisent plus d'un schéma et plus particulièrement certains plug-ins afin d'éviter les conflits avec vos propres classes (voir le chapitre 17 pour plus de détails)

Les attributs des colonnes

Pour définir les colonnes dans le fichier schema.yml la syntaxe de base vous laisse deux possibilités : laisser Symfony déduire les caractéristiques de la colonne à partir de son nom (en ne renseignant pas la valeur de la clé) ou définir le type de la colonne avec le mot-clé correspondant. Voir l'exemple du listing 8-26.

Listing 8-26 - Les attributs de base des colonnes

propel:
  blog_article:
    id:                 # Symfony s'en chargera
    title: varchar(50)  # Spécifiez le vous-même

Mais vous pouvez définir bien plus de choses pour une colonne. Dans ce cas, vous devrez considérer les paramètres des colonnes comme un tableau associatif, comme le montre le listing 8-27

Listing 8-27 - Les attributs complexes des colonnes

propel:
  blog_article:
    id:       { type: integer, required: true, primaryKey: true, autoIncrement: true }
    name:     { type: varchar(50), default: foobar, index: true }
    group_id: { type: integer, foreignTable: db_group, foreignReference: id, onDelete: cascade }

Les paramètres des colonnes sont les suivants :

  • type: Le type d'une colonne. Les valeurs possibles sont boolean, tinyint, smallint, integer, bigint, double, float, real, decimal, char, varchar(size), longvarchar, date, time, timestamp, bu_date, bu_timestamp, blob, et clob.
  • required: Booléen. A renseigner à true si vous souhaitez que la colonne soit obligatoire.
  • default: Valeur par défaut.
  • primaryKey: Booléen. A renseigner à true s'il s'agit d'une clé primaire.
  • autoIncrement: Booléen. A renseigner à true pour une colonne de type integer qui recevra une valeur par auto-incrément.
  • sequence: Nom de la séquence pour la base les employant pour les colonnes en autoIncrement (par exemple PostgreSQL ou Oracle).
  • index: Booléen. A renseigner à true si vous souhaitez créer un index simple sur cette colonne ou à unique pour un index unique.
  • foreignTable: Le nom d'un table utilisé pour créer une clé étrangère dans une autre table.
  • foreignReference: Le nom de la colonne concernée si une clé étrangère a été définie par foreignTable.
  • onDelete: Précise l'action à effectuer lorsque qu'un enregistrement d'une table liée est supprimé. S'il est renseigné à setnull, la clé étrangère est renseignée à null. Si la valeur est cascade, l'enregistrement est supprimé. Si le SGDB ne supporte pas ce type de comportement, l'ORM l'émulera. Ceci ne concerne bien sûr que les colonnes possédant les attributs foreignTable et foreignReference.
  • isCulture: Booléen. A renseigner à true pour les colonnes culture dans les tables a contenu localisé (voir le chapitre 13).

Les clés étrangères

A la place des attributs foreignTable et foreignReference d'une colonne, vous pouvez définir des clés étrangères dans une table via la clé _foreignKeys:. Le schéma du listing 8-28 créera une clé étrangère sur la colonne user_id correspondant à la colonne id de la table blog_user.

Listing 8-28 - Syntaxe alternative pour clé étrangère

propel:
  blog_article:
    id:
    title:   varchar(50)
    user_id: { type: integer }
    _foreignKeys:
      -
        foreignTable: blog_user
        onDelete:     cascade
        references:
          - { local: user_id, foreign: id }

Cette syntaxe alternative est pratique pour les clés étrangères à référence multiple et pour pouvoir nommer ces clés. Le listing 8-29 en montre un exemple.

Listing 8-29 - La syntaxe alternative des clés étrangères appliquée à une clé à références multiples

    _foreignKeys:
      my_foreign_key:
        foreignTable:  db_user
        onDelete:      cascade
        references:
          - { local: user_id, foreign: id }
          - { local: post_id, foreign: id }

Les index

Comme alternative à l'attribut index d’une colonne, vous pouvez définir les index d'une table via la clé _indexes:. Si vous souhaitez définir des index unique vous devez utiliser _uniques: à la place de _indexes:. Le listing 8-30 vous montre la syntaxe alternative des index.

Listing 8-30 - Syntaxe alternative des index et index uniques

propel:
  blog_article:
    id:
    title:            varchar(50)
    created_at:
    _indexes:
      my_index:       [title, user_id]
    _uniques:
      my_other_index: [created_at]

Cette syntaxe n'est utile que pour les index construit à partir de plusieurs colonnes.

Les colonnes vides

Symfony devient magicien lorsqu'il rencontre des colonnes sans valeurs, puisqu'il y ajoute une valeur de son cru . Le listing 8-31 vous montre ce qui est fait à cette occasion.

Listing 8-31 - Les caractéristiques de la colonne déduites à partir du nom de son nom

// Les colonnes vides nommées id sont considérées comme clés primaires id: { type: integer, required: true, primaryKey: true, autoIncrement: true }

// Les colonnes vides nommées XXX_id sont considérées comme clés étrangères
foobar_id:  { type: integer, foreignTable: db_foobar, foreignReference: id }

// Les colonnes vides nommées created_at, updated at, created_on et updated_on
// sont considérées comme des dates et reçoivent automatiquement le type timestamp
created_at: { type: timestamp }
updated_at: { type: timestamp }

Pour les clés étrangères, Symfony va rechercher une table dont le phpName est égale au début du nom de la colonne (le XXX du listing 8-31). S'il la trouve il prendra le nom de cette table comme foreignTable.

Les tables d'internationalisation (I18n)

Symfony accepte les contenus internationalisé au travers de tables liées. Cela signifie que si vous avez des contenus soumis à l'internationalisation, ils sont stockés dans deux tables distinctes. La première contient les colonnes invariables et la seconde les colonnes internationalisées.

Tout ceci est implicite dans votre fichier schema.yml lorsque vous nommé une table quelquechose_i18n. Le listing 8-32 vous montre un schéma qui sera automatiquement complété avec les colonnes et attributs nécessaires pour le bon fonctionnement du mécanisme d'internationalisation. En interne, Symfony le comprendra comme si ce schéma était écrit comme sur le listing 8-33. Vous en apprendrez plus au chapitre 13.

Listing 8-32 - Mécanisme I18n implicite

propel:
  db_group:
    id:
    created_at:

  db_group_i18n:
    name:        varchar(50)

Listing 8-33 - Mécanisme I18n explicite

propel:
  db_group:
    _attributes: { isI18N: true, i18nTable: db_group_i18n }
    id:
    created_at:

  db_group_i18n:
    id:       { type: integer, required: true, primaryKey: true,foreignTable: db_group, foreignReference: id, onDelete: cascade }
    culture:  { isCulture: true, type: varchar(7), required: true,primaryKey: true }
    name:     varchar(50)

Dans l'ombre du schema.yml: le schema.xml

En fait, le fichier schema.yml est propre à Symfony. Lorsque vous faites appelle à une commande Propel, Symfony traduit ce fichier en un fichier generated-schema.xml, lequel est réellement utilisé par Propel pour effectuer ses opérations sur le modèle.

Le fichier schema.xml contient les même informations que son homologue YAML. Par exemple, le listing 8-3 converti en fichier XML donne le listing 8-34

Listing 8-34 - Exemple de schema.xml, correspondant au listing 8-3

[xml]
<?xml version="1.0" encoding="UTF-8"?>
 <database name="propel" defaultIdMethod="native" noXsd="true" package="lib.model">
    <table name="blog_article" phpName="Article">
      <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
      <column name="title" type="varchar" size="255" />
      <column name="content" type="longvarchar" />
      <column name="created_at" type="timestamp" />
    </table>
    <table name="blog_comment" phpName="Comment">
      <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
      <column name="article_id" type="integer" />
      <foreign-key foreignTable="blog_article">
        <reference local="article_id" foreign="id"/>
      </foreign-key>
      <column name="author" type="varchar" size="255" />
      <column name="content" type="longvarchar" />
      <column name="created_at" type="timestamp" />
    </table>
 </database>

La description du schema.xml est disponible dans la documentation Propel et dans la section "Getting Started" du site du projet.(http://propel.phpdb.org/docs/user_guide/chapters/appendices/AppendixB-SchemaReference.html).

Le format YAML a été adopté pour que les schémas restent faciles à lire et écrire, mais la contrepartie est qu'il n'est pas adapté aux schémas complexes. D'autre part, le format XML peut-être utilisé pour tous les schémas quelques soit leurs complexités. De plus il accepte les paramètres spécifiques des bases, l'héritage de table etc.

Symfony est capable d'interpréter les schémas écrit en XML. Par conséquent, si votre schéma est trop compliqué pour la syntaxe YAML, si vous possédez déjà un schéma en XML ou si vous êtes à l'aise avec les syntaxe XML de Propel, vous n'avez pas besoin de passer à YAML. Placez votre schema.xml dans le répertoire config/, générez le modèle et tout est dit.

SIDEBAR Propel dans Symfony

Les informations données ici ne sont pas spécifiques à Symfony, mais plutôt à Propel. Propel est la couche d'abstraction objet/relationnel favorite de Symfony, mais vous pouvez en choisir une autre. Cependant Symfony et Propel se marient bien pour les raisons suivantes :

Tous les objets classes du modèle de données et la classe Criteria sont des classes à chargement automatique. Dès que vous les utiliser, Symfony inclus les fichiers adéquats ce qui vous économise d'ajouter vous même les ordres d'inclusions. Avec Symfony, Propel n'a pas besoin d'être démarré ou même initialisé. Lorsqu'un objet utilise Propel, la bibliothèque s'initialise elle-même. Certains helpers de Symfony acceptent des objets Propel comme paramètres, ce qui permet de réaliser des opérations de haut niveau (comme la pagination ou le filtrage). Propel permet un prototypage et une génération rapide du backend de votre application (Voir le chapitre 14 pour plus de détails). Le schéma s'écrit plus vite au travers grâce au fichier schema.yml.

Enfin, à l'image de Propel, Symfony est indépendant de la base de données utilisée.

Ne doublez pas la création du modèle

La contrepartie à l'utilisation d'un ORM est que vous devez définir deux fois la structure de donnée : une fois pour la base de données et la seconde pour le modèle objet. Heureusement, Symfony vous possède des outils en ligne de commande permettant la génération de l'un à partir de l'autre. Vous évitez ainsi de répéter votre travaille.

Générer une base SQL à partir d'un schéma existant

Si vous débutez le développement de votre application par l'écriture du fichier schema.yml, Symfony pourra générer une requête SQL qui créera les tables à partir du modèle de données YAML. Pour ce faire, placez-vous à la racine de votre projet et saisissez la commande suivante :

> symfony propel-build-sql

Un fichier lib.model.schema.sql sera crée dans le répertoire myproject/data/sql/. Il est à noter que le code SQL généré sera optimisé pour le SGBD définit par le paramètre phptype du fichier propel.ini.

Vous pouvez directement utiliser le fichier schema.sql pour construire vos tables. Par exemple pour MySQL vous entreriez :

> mysqladmin -u root -p create blog
> mysql -u root -p blog < data/sql/lib.model.schema.sql

Le SQL généré est aussi très pratique dans le cas où vous devez reconstruire votre base dans un autre environnement ou changer de SGBD. Si votre connexion est correctement paramétrée dans le fichier propel.ini, vous pouvez utiliser la commande symfony propel-insert-sql pour exécuter ces opérations automatiquement.

TIP La ligne de commande offre aussi la possibilité de peupler vos bases en se basant sur un fichier texte. Rendez-vous au chapitre 16 pour en apprendre plus sur la commande propel-load-data et les fichiers d'installation de YAML.

Générer un modèle de donnée YAML à partir d'une base existante

Grâce au mécanisme d'introspection, qui permet à une base de donnée de déterminer la structure des tables avec lesquelles elle travaille, Symfony peut générer le fichier schema.yml à partir d'une base existante en utilisant Creole. Ceci est particulièrement pratique si vous faites du reverse-engineering ou si vous préférez travailler sur les bases plutôt que sur le modèle de donnée.

Pour exploiter ce comportement, vous devez vous assurer que le fichier propel.ini du projet pointe sur les bonnes bases et que les connexions nécessaires soient correctement paramétrées. Vous pouvez alors exécuter la commande propel-build-schema:

> symfony propel-build-schema

Un tout nouveau fichier schema.yml sera alors généré dans le répertoire config/ à partir de la structure de la base de donnée. Vous pouvez construire votre modèle à partir de ce schéma

La commande de génération du schéma est suffisamment puissante pour permettre d'ajouter des informations spécifiques à la base dans votre schéma. Etant donné que YAML ne sait pas gérer ce genres d'informations particulières, vous devez générez un schéma XML pour profiter de ce comportement. Vous pouvez le faire facilement en ajoutant simplement l'argument xml à la commande build-schema:

> symfony propel-build-schema xml

A la place d'un fichier schema.yml, cette commande va créer un fichier schema.xml pleinement compatible avec Propel et contenant toutes les informations propriétaires. Attention, les schémas XML générés ont tendance à être très verbeux et difficile à lire.

SIDEBAR La configuration du propel.ini

Les commandes propel-build-sql et propel-build-schema n'utilisent pas le paramètres de connexions définit dans le fichier databases.yml. Elles emploient les paramètres de connexions du fichier propel.ini du répertoire config de votre projet:

 propel.database.createUrl = mysql://login:passwd@localhost
 propel.database.url       = mysql://login:passwd@localhost/blog

Ce fichier contient d'autres paramètres nécessaires à Propel pour rendre les classes générées du modèle compatible avec Symfony. La plupart d'entre-elles sont sans grand intérêt pour l'utilisateur, mis à part celles-ci:

 // Les classes de base sont chargées automatiquement par Symfony
 // A renseigner à `true` pour utiliser l'ordre `include_once` à la place
 // (Légère dégradation des performances)
 propel.builder.addIncludes = false

 // Par défaut, les classes générées ne sont pas commentées
 // A renseigner à `true` pour ajouter les commentaires dans les classes de base
 // (Légère dégradation des performances)
 propel.builder.addComments = false

 // Par défaut, les behaviors ne sont pas gérés 
 // A renseigner à `true` pour les gérer
 propel.builder.AddBehaviors = false

Après avoir apporté des modifications au fichier propel.ini n'oubliez pas de regénérer le modèle pour que les modifications prennent effet.

Résumé

Symfony utilise Propel comme ORM et Creole comme abstraction de donnée. Cela signifie que vous devez d'abord décrire le schéma relationnel de votre base en YAML avant de générer les classes du modèle. Ensuite, utilisez les méthodes des objets et les classes Peer pour récupérer les valeurs d'un enregistrement ou d'un ensemble d'enregistrements. Vous pouvez court-circuiter ces méthodes et étendre le modèle en ajoutant des méthodes à des classes personnalisées. Les paramètres de connexions sont définis dans le fichier databases.yml qui peut accepter plusieurs connexions. Enfin la ligne de commande possède plusieurs ordres permettant d'éviter la duplication de la définition de structure.

La couche modèle est la partie la plus complexe du framework Symfony. Une des raisons de cette complexité est que la manipulation de donnée est justement une question complexe. A cela s'ajoute que les questions de sécurité sont d'une importance cruciale pour un site Web et ne doivent pas être ignorées. Une autre raison est que Symfony prévu pour le monde de l'entreprise donc mieux adapté à des applications de moyennes ou grandes tailles. Pour de telles applications, les automatismes pourvus par le modèle de Symfony représentent un véritable gain de temps qui justifie à lui seul l'investissement de son apprentissage.

N'hésitez donc pas à prendre le temps de tester les objets du modèle pour bien le comprendre. La solidité et la capacité d'évolution de vos applications seront vos meilleures récompenses.