Development

sfBookFRmodel

You must first sign up to be able to contribute.

Le Modèle dans symfony

Présentation

Symfony utilise une couche d'abstraction objet/relationnel basée sur Propel pour se connecter aux bases de données. Propel permet d'accéder et de modifier facilement les données stockées dans une base en utilisant des objets à la place de requêtes SQL. Ce chapitre détaille l'utilisation du modèle de données objet dans Propel et illustre son utilisation au sein de symfony.

Pourquoi utiliser une couche d'abstraction ?

Les bases de données sont relationnelles, alors que PHP5 est orienté objet. Afin d'accéder à une base de données de manière objet, il nous faut utiliser une interface qui convertie la logique objet en logique relationnelle. Cette méthode se nomme ORM (Object Relationnal Mapping ou mappage objet relationnel).

C'est la couche Modèle de l'architecture MVC utilisée dans symfony. Elle est constituée d'objets qui donnent accès aux données, tout en conservant la logique métier.

Un des bénéfices d'une telle implémentation, c'est que vous n'avez pas à utiliser une syntaxe propre à une base de données particulière. Elle traduit automatique les appels du modèle objet en requêtes SQL, optimisées pour la base utilisée.

Il est ainsi facile de changer de système de données en cours de projet. Imaginé que vous ayez à écrire rapidement un prototype pour une application, mais que client ne s'est pas encore décidé sur le système de base de données. Vous pouvez commencé le projet avec SQLite et migrer vers MySQL, PostgreSQL ou Oracle quand le client le souhaite. Il vous suffit juste pour cela de changer une ligne dans un fichier de configuration.

Une couche d'abstraction gère l'extraction des données. Le reste de l'application ne nécessite pas de requêtes SQL et le SQL qui accède à la base est facile à trouver. Les développeurs spécialisés dans la programmation de base de données s'y retrouvent également.

Le fait d'utiliser des objets à la place d'enregistrements et des classes à la place de tables a un autre avantage : cela permet d'ajouter des accesseurs à vos tables. Si par exemple vous avez une table 'Client' avec deux champs 'Prénom' et 'Nom', vous pourriez être intéressé pour récupérer uniquement le nom complet. Dans un environnement orienté objet, il suffit d'ajouter un nouvelle méthode accesseur à la classe Client.

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

Les fonctions qui font des appels répétitifs aux données et la logique métier des données elles-mêmes peuvent être conservées dans de tels objets. Prenons l'exemple d'une classe 'ShoppingCart?' dans laquelle vous conservez les produits sous forme d'objets. Pour connaître le montant total du panier lorsque la commande est terminée, vous pouvez utiliser la méthode suivante :

 public function getTotal()
 {
   $total = 0;
   foreach ($this->getItems() as $item)
   {
     $total += $item->getPrice();
   }
   return $total;
 }

C'est tout. Faire la même chose avec une requête SQL aurait été plus long.

Propel est actuellement l'une des meilleures couches d'abstraction pour PHP5. Plutôt que de la réécrire, symfony l'intègre de façon homogène au sein du framework.

Le modèle de données

But

Pour créer le modèle de données objet que symfony va utiliser, vous devez commencer par convertir le modèle relationnel de votre base de données. C'est le fichier schema.yml qui s'en charge. On y trouve les tables, leurs relations et le type de chaque colonne.

Le fichier schema.yml doit se trouver dans le répertoire myproject/config/. Il utilise la syntaxe YAML?, mais vous n'aurez aucun problème à le lire sans connaître ce format. Format qui, soit dit en passant, ne nécessite pas plus de 5 minutes d'apprentissage.

Exemple

Imaginons que vous ayez une base de données 'blog' avec deux tables 'blog_article' et 'blog_comment' qui contiennent des données. La structure est la suivante :

blog_article blog_comment
id id
title article_id
content author
created_at content
created_at

Le fichier schema.yml correspondant ressemblerait à ça :

 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 pouvez remarquer que le nom de la base de données (blog) n'apparaît pas directement dans le fichier schema.yml. A la place, la base est référencée sous un nom de connexion (propel dans notre exemple). Ceci parce que les paramètres de connexion actuels peuvent dépendre de l'environnement dans lequel votre application fonctionne. Par exemple, lorsque vous êtes en phase de développement, vous accédez à la base de développement, mais avec le même schéma que la base de production qui, elle, n'utilise pas les mêmes paramètres de connexion. Ces informations seront spécifiées dans le fichier databases.yml (voir ci-dessous).

La syntaxe de base pour le schéma

Dans un fichier schema.yml, la première clé correspond à une connexion. Plusieurs tables peuvent y figurer, chacune ayant un ensemble de colonnes. La syntaxe YAML définie que les clés se terminent par un point-virgule et que la structure est indiquée à l'aide d'une indentation (un ou plusieurs espaces, mais jamais de tabulations).

Une table peut avoir des attributs spéciaux, y compris l'attribut phpName qui indique le nom de la classe qui sera générée. Si cet attribut n'est pas mentionné, le nom de la table sera utilisé à la place, sans les tirets bas et avec une lettre majuscule pour chacun des mots, excepté pour le premier. Dans notre exemple, le nom phpName par défaut serait blogArticle et blogComment). Les colonnes ont elles aussi un nom phpName, qui est la version majuscule du nom (ID, TITLE, CONTENT, etc.) et qui, dans la majorité des cas, n'a pas a être réécrit.

La plupart du temps, les colonnes n'ont besoin que d'un seul attribut : le type. Il peut avoir pour valeur boolean, integer, float, date, varchar(taille), longvarchar, etc. Pour les champs texte de plus de 256 caractères, vous devez utiliser le type longvarchar qui n'a pas de taille. Selon les spécifications de MySQL, celui-ci ne peut excéder 65 Ko. Les types date et timestamp ont les mêmes limitations que les dates Unix et ne peuvent donc pas être définies avec une date antérieure au 1er janvier 1970 (1970-01-01). Pour définir des dates plus anciennes (par exemple des dates de naissance), un format de date 'before Unix' existe et s'utilise avec bu_date et bu_timestamp.

Bien sûr, si vous souhaitez spécifier vos propres clés primaires, clés étrangères, valeurs par défaut ou index, vous pouvez utiliser la syntaxe de schéma étendue (voir ci-dessous).

Conventions utilisées pour les schémas

Les colonnes qui se nomment id sont considérées comme étant des clés primaires.

Les colonnes se terminant par _id sont considérées comme étant des clés étrangères. La table qui s'y rattache est déterminée automatiquement, à partir de la première partie du nom de la colonne.

Les colonnes qui se nomment created_at sont automatiquement défini comme étant de type timestamp.

Pour toutes ces colonnes vous n'avez pas à spécifier leur type. Symfony déduit leur format à partir de leur nom. C'est pour cette raison que le fichier schema.yml est si facile à écrire.

Les fichiers du modèle de données objet

Génération du code

Maintenant que nous avons le schéma de données, les classes du modèle peuvent être générées. Dans le répertoire du projet, saisissez la commande suivante :

 $ symfony propel-build-model

Note: Propel utilise Phing. Vous aurez donc besoin de l'installer si cela n'a pas déjà été fait. Pour cela, tapez :

 $ pear install http://phing.info/pear/phing-current.tgz

Les classes de base d'accès aux données seront automatiquement créées dans le répertoire myproject/lib/model/om/ :

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

En outre, les classes réelles d'accès aux données seront créées dans myproject/lib/model :

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

Vous n'avez défini que deux tables et vous vous retrouver avec huit fichiers.

Le modèle de base et le modèle courant

Pourquoi avoir deux versions du modèle de données objet, l'un dans model/om/ et l'autre dans model/ ?

Vous aurez certainement besoin d'ajouter vos propres méthodes et attributs aux objets du modèle (pensez à la méthode ->getName() qui permet de joindre les champs Prénom et Nom). De même, au fil du développement de votre projet, vous aurez probablement à ajouter des tables et des colonnes. A chaque fois que vous modifiez le fichier schema.yml, vous devrez génerer à nouveau les classes du modèle objet, à l'aide de la commande symfony propel-build-model. Si vos méthodes ont été rajoutées à celles précédemment générées, elles seront alors effacées à chaque fois.

Les classes Base qui se trouvent dans le répertoire model/om/ sont celles générées par Propel. Vous ne devriez jamais les modifier, car à chaque fois que vous générez le modèle, ces fichiers sont effacés. Par contre, les classes qui se trouvent dans le répertoire model/, qui héritent des classes précédentes, ne sont pas réécrites. C'est donc là que vous devez ajouter vos propres méthodes.

Par exemple, voici le contenu du fichier model/Article.php nouvellement créé :

 require_once 'model/om/BaseArticle.php';
 class Article extends BaseArticle
 {
 }

Cette classe hérite de toutes les méthodes de la classe BaseArticle, mais elle ne sera pas affectée si vous touchez au modèle. Cette structure est à la fois personnalisable et évolutive.

Ce mécanisme permet de commencer à coder sans pour autant connaître le modèle relationnel final de votre base de données.

Les classes Peer

Les classes Article et Comment vont vous permettrent d'accéder aux attributs d'un enregistrement. Vous pouvez ainsi connaître le titre d'un article en appelant la méthode appropriée d'un objet Article :

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

Mais vous aurez également besoin de trouver des enregistrements. C'est là qu'interviennent les classes Peer qui offrent des méthodes pour cela. Celles-ci retournent un objet ou une liste d'objets correspondant à la classe sans la mention Peer.

 $articles = ArticlePeer::retrieveByPks(array(123, 124, 125));
 // $articles est un tableau d'objets de la classe Article

D'un point de vue du modèle de données, vous noterez qu'il ne peut pas y avoir d'objet Peer. C'est pourquoi les méthodes se trouvant dans les classes Peer seront appelées avec la syntaxe :: (pour un appel de méthode de classe) plutôt que l'habituel -> (pour un appel de méthode d'objet).

Accès aux données

Avec Propel, vos données sont accessibles à travers des objets. Si vous êtes rodé au modèle relationnel et au langage SQL, les méthodes Propel vous sembleront sans doute compliquées. Mais une fois que vous aurez goûté à la puissance du mappage objet, vous ne pourrez plus vous en passer. Jetez donc un oeil aux exemples donnés dans la documentation de Propel.

Accès aux champs

Propel fourni une classe pour chacune des tables définies dans le fichier schema.yml. Ces classes ont des créateurs, accesseurs et manipulateurs par défaut. Les méthodes new, get et set donnent accès aux colonnes de la table.

 $article = new Article();
 $article->setTitle('Mon premier article');
 $article->setContent('C\'est mon tout premier article.\n J'espère que vous l\'appréciez !');
 $title   = $article->getTitle();
 $content = $article->getContent();

Notez que l'attribut phpName de la table est utilisé pour le nom de la classe. Les accesseurs utilisent la notation CamelCase (la première lettre de chaque mot est en majuscule) en se basant sur le nom des colonnes et en supprimant les tirets bas.

Pour créer un enregistrement dans une table liée à une autre, il vous suffit de passer l'objet comme clé étrangère :

 $comment = new Comment();
 $comment->setAuthor('Steve');
 $comment->setContent('C'est le meilleur article que j'ai jamais lu !');
 $comment->setArticle($article);

La dernière ligne place automatiquement la valeur de l'attribut Id de l'objet $article à l'attribut ArticleId de l'objet Comment. Vous auriez également pu l'écrire manuellement :

 $comment->setArticleId($article->getId());

Notez qu'en appelant le constructeur new, vous avez créé un nouvel objet, mais pas un nouvel enregistrement dans la table Article. Pour enregistrer les données dans la base, vous devez appeler la méthode de votre objet :

 $article->save();

Grâce au principe en cascade de Propel, vous n'avez pas à enregistrer l'objet $comment. Étant donné que l'enregistrement est lié à celui que vous traitez, il sera sauvegardé automatiquement.

Astuce: Pour définir plusieurs champs d'un seul coup, vous pouvez utiliser les méthodes ->fromArray() des objets Propel :

 $article->fromArray(array(
    'title'   =>   'Mon premier article',
    'content' =>   'C\'est mon tout premier article.\n J'espère que vous l\'appréciez !',
 ));

Accès aux enregistrements liés

Vérifions tous les commentaires de notre nouvel article :

 $comments = $article->getComments();

Propel voit la colonne article_id de la table Comment et comprend qu'un article peut avoir plusieurs commentaires (relation un à plusieurs). Lors de la génération du code, une méthode getComments() est créée pour l'objet Article, afin de récupérer un tableau contenant les commentaires correspondants. Vous noterez ici l'utilisation du pluriel dans le nom de la méthode get. Ainsi, la variable $comments contient un tableau d'objets de la classe Comment. Vous pouvez afficher le premier élément de ce tableau en utilisant :

 print_r($comments[0]);

Si vous lisez les commentaires laissés sur vos articles, vous pourriez changer d'avis concernant l'intérêt de les voir publier sur Internet. De même, si vous n'appréciez pas l'ironie de leurs auteurs, vous pouvez facilement les supprimer à l'aide de la méthode delete() :

 foreach($comments as $comment)
 {
   $comment->delete();
 }

Pour les relations plusieurs à un, c'est encore plus simple :

 echo $comment->getArticle()->getTitle();

La méthode getArticle() renvoie un objet de la classe Article qui bénéficie de l'accesseur getTitle(). C'est bien mieux que de faire la jointure soi-même; ce qui prendrait quelques lignes de code (à partir de l'appel de la méthode $comment->getArticleId()).

Vous en saurez plus sur ces méthodes et leur syntaxe dans la documentation de Propel.

Accès aux enregistrements

Si vous connaissez la valeur de la clé primaire d'un enregistrement, vous pouvez utiliser la méthode retrieveByPk() de la classe Peer pour obtenir l'objet correspondant. Par exemple :

 $article = ArticlePeer::retrieveByPk(7);

Le fichier schema.yml défini le champ id comme étant la clé primaire de la table Article. La variable va ainsi retourner l'article qui correspond au champ id qui a la valeur 7. Comme nous utilisons la clé primaire, nous savons qu'un seul enregistrement sera retourné. La variable $article contient alors un objet de la classe Article.

Afin de permettre l'abstraction des bases de données, Propel ne nécessite pas de SQL pour les requêtes simples. A la place, il utilise un objet Criteria, qui offre la même puissance.

Par exemple, pour rapatrier tous les articles, il suffit d'écrire :

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

La méthode doSelect() applique comme filtre ce qu'elle reçoit en paramètre. Dans cette exemple, l'objet $c est vide. L'appel à doSelect() renverra donc tous les enregistrements de la table Article (ce qui est équivaut en SQL à 'SELECT *'). La variable $articles contiendra un tableau de tous les objets de la classe Article. Pour y accéder un par un, il vous suffit d'utiliser l'instruction foreach :

 foreach ($articles as $article)
 {
   echo $article->getTitle();
 }

Mais on peut aller plus loin. L'objet Criteria vous permet d'ajouter des règles, comme vous le feriez en SQL avec les instructions 'WHERE' ou 'ORDER BY'.

Pour récupérer tous les commentaires écrits par Steve, classés par date, utilisez :

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

Ou, à l'inverse, pour ne récupérer que les articles qui n'ont pas été écrits par Steve :

 $c = new Criteria();
 $c->add(CommentPeer::AUTHOR, 'Steve', Criteria::NOT_EQUAL);
 $c->addAscendingOrderByColumn(CommentPeer::DATE);
 $comments = CommentPeer::doSelect($c);

Notez les constantes utilisées ici : il s'agit des noms phpNames des colonnes correspondant au modèle de données objet. La méthode ::doSelect() génère la requête SQL la plus appropriée selon le type de votre base de données, l'exécute et retourne une liste d'objets; ce qui est bien mieux que de récupérer un jeu d'enregistrements.

Note: Pourquoi écrire CommentPeer::AUTHOR plutôt que comment.AUTHOR, que nous aurions utilisé avec la requête SQL ? Imaginez que vous devez remplacer dans la base de données le champ auteur par contributeur. Si vous utilisez comment.AUTHOR, vous devrez le modifier à chaque appel aux objets Propel. En utilisant CommentPeer::AUTHOR, il vous suffit juste de changer le nom de la colonne dans le fichier schema.yml, et de garder le nom phpName AUTHOR afin d'éviter de multiples modifications.

Référez-vous à la Documentation Propel pour plus de détails concernant l'objet Criteria et ces méthodes.

Les colonnes created_at et updated_at

Habituellement, lorsqu'une table possède une colonne created_at, celle-ci sert à stocker l'heure et la date à laquelle l'enregistrement a été créé. La même logique s'applique pour la colonne updated_at, qui est généralement modifiée, avec l'heure et la date courante, à chaque fois que l'enregistrement est mis à jour.

La bonne nouvelle est que symfony sait reconnaître le nom de ces tables. Vous n'avez pas à définir manuellement les valeurs des colonnes created_at et updated_at; elles seront automatiquement mise à jour.

 $comment = new Comment();
 $comment->setAuthor('Steve');
 $comment->save();
 // affiche la date de création
 echo $comment->getCreatedAt();

Ces colonnes accepte également un format date en argument :

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

Configuration de l'accès à la base de données

Le modèle de données est indépendant de la base de données utilisée, mais vous devrait impérativement en utiliser une. Les informations nécessaires à symfony pour gérer les requêtes d'une base sont : son nom, ses codes d'accès et son type. Ces données doivent être indiquées dans le fichier databases.yml qui se trouve dans le répertoire myproject/config/ :

 prod:
   propel:
     param:
       host:               mydataserver
       username:           myusername
       password:           xxxxxxxxxx  
 all:
   propel:
     class:                sfPropelDatabase
     param:
       phptype:            mysql
       host:               localhost
       database:           blog
       username:           root
       password:           
       compat_assoc_lower: true
       # datasource:       propel

L'accès à la base est lié à l'environnement dans lequel vous travaillez. Vous pouvez définir des paramètres différents pour les environnements prod, dev, test ou n'importe quel autre environnement que vous avez défini. L'entête all défini les paramètres pour tous les environnements. Cette configuration peut également être modifiée pour chaque application. On indique pour cela des valeurs différentes, dans un fichier spécifique à une application, par exemple dans myproject/apps/myapp/config/databases.yml.

Note: Le paramètre host peut également être défini sous le nom hostspec.

Vous pouvez définir plusieurs connexions pour chaque environnement. Chacune de ces connexions est liées au fichier schema.yml par le paramètre datasource. Il réfère à la première clé de ce fichier. Si vous ne spécifiez pas ce paramètre, il prend la valeur du label indiqué dans les paramètres de connexion.

Les valeurs autorisées pour le paramètre phptype correspondent aux systèmes de bases de données que supporte Propel :

  • mysql
  • sqlserver
  • pgsql
  • sqlite
  • oracle

Les valeurs host, database, username et password définissent les paramètres nécessaires pour se connecter à la base de données.

Dans l'exemple ci-dessous, les valeurs pour tous les environnements (all) peuvent également être présentées avec la syntaxe suivante, plus compacte :

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

Note: Si vous utilisez une base de données SQLite, le paramètre host doit être défini avec le chemin d'accès au fichier de la base. Par exemple, si vous stocker la base de données de votre blog dans myproject/data/blog.db, le fichier databases.yml sera :

 all:
   propel:
     class:          sfPropelDatabase
     param:
       dsn:          sqlite://./../data/blog.db

Syntaxe de schéma étendu

Attributs

La base de données et les tables peuvent avoir des attributs différents. Ils sont définis avec la clé _attribute:.

Si vous souhaitez que votre schéma soit validé avant de générer le code, il suffit de désactiver l'attribut noXSD.

 propel:
   _attributes:   { noXsd: false }

L'élément principal supporte l'attribut defaultIdMethod. Dans le cas où celui-ci n'est pas défini, la méthode native de génération des champs IDs sera utilisée -- ex. auto incrémentation pour MySQL, séquences pour PostgreSQL. L'autre valeur possible est none :

 propel:
   _attributes:   { defaultIdMethod: none }

Nous avons déjà vu l'attribut de table phpName, utilisé pour définir le nom des classes générées convertissant la table :

 propel:
   blog_article:
     _attributes: { phpName: Article }

Les tables ayant un contenu localisé (c'est-à-dire plusieurs versions d'un contenu pour l'internationalisation) se voient adjoindre deux attributs supplémentaires (voir le chapitre internationalisation? pour les détails) :

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

Détails des colonnes

La syntaxe de base vous laisse deux possibilités : laisser à symfony le soin de déduire les caractéristiques des colonnes à partir de leur nom (en n'indiquant pas de valeur) ou définir le type avec l'un des mots-clés autorisés :

 propel:
   blog_article:
     id:                 # laissons symfony faire le travail
     title: varchar(50)  # spécifions le type nous-même

Mais vous pouvez définir bien plus pour une colonne, et dans ce cas, vous aurez à définir les valeurs de la colonne sous forme d'un tableau associatif :

 propel:
   blog_article:
     id:       { type: integer, required: true, primaryKey: true, autoincrement: true }
     name:     { type: varchar , size: 50, defaultValue: foobar, index: true }
     group_id: { type: integer, foreignTable: db_group, foreignReference: id, onDelete: cascade }

Les paramètres de colonnes sont :

  • type: Type de colonne Propel. Voir Liste de types de Propel pour plus de détails.
  • size: Défini la longueur d'une chaîne pour les colonnes de type VARCHAR. Notez qu'une colonne définie comme varchar(50) dans la syntaxe de base correspond à { type: varchar , size: 50 } dans la syntaxe étendue.
  • required: false par défaut, définir à true si vous souhaitez que la colonne soit requise
  • defaultValue
  • primaryKey: booléen, définir à true pour les clés primaires
  • autoincrement: booléen, définir à true pour les colonnes de type integer qui nécessitent d'être auto incrémentées
  • index: booléen, définir à true si vous souhaitez créer un index sur cette colonne
  • foreignTable: pour créer une clé étrangère avec une autre table
  • foreignReference: le nom de la colonne liée si une clé étrangère est définie via foreignTable
  • onDelete: dans le cas d'une clé étrangère, avec la valeur cascade, les données de cette table seront effacées lorsqu'un enregistrement lié de la table étrangère sera supprimée.
  • isCulture: définir à true pour les colonnes culture dans les tables de contenus localisés (voir le chapitre internationalisation?)

Clés étrangères

Une alternative à l'utilisation des attributs de colonne foreignTable et foreignReference pour une table consiste à ajouter manuellement une ou plusieurs clés étrangères dans la section _foreign_keys :

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

Cela créera une clé étrangère pour la colonne user_id, correspondant à la colonne id de la table blog_user.

Comparer à l'utilisation des attributs de colonne, cela n'a d'intérêt que pour des clés étrangères avec de multiple références :

    _foreign_keys:
       db_user:
         _attributes: { onDelete: cascade }
         references:
           - { local: user_id, foreign: id }
           - { local: post_id, foreign: id }

Index

Pour une table, comme alternative à l'attribut de colonne index, vous pouvez ajouter manuellement un ou plusieurs index dans la section _indexes:

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

De même que la syntaxe pour les clés étrangères, cela ne se révèle utile que pour les index construits sur plus d'une colonne.

Automatismes liés aux colonnes

Lorsque symfony rencontre une colonne sans valeur, il ajoute ces propres informations, comme par magie :

 id:         { type: integer, required: true, primaryKey: true, autoincrement: true }
 foobar_id:  { type: integer, foreignTable: db_foobar, foreignReference: id }
 created_at: { type: timestamp }
 updated_at: { type: timestamp }

Pour les clés étrangères, symfony va chercher une table ayant le même nom phpName que le début du nom de la colonne. S'il en trouve une, il prendre le nom de la table comme valeur pour l'attribut foreignTable.

Automatismes liés aux tables (I18n)

Symfony supporte la [i18n.txt localisation de contenu] dans les tables liées. Cela signifie que lorsque vous avez du contenu sujet à localisation, celui est stocké dans deux tables séparées : l'une avec des colonnes invariables, et une seconde avec des colonnes propres à la localisation.

Dans un fichier schema.yml, cela est pris en compte lorsque vous nommez une table foobar_i18n, comme suit :

 propel:
   db_group:
     id:          -
     created_at:  -
   db_group_i18n: 
     name:        varchar(50)

Symfony va automatiquement ajouter les attributs de colonnes et de table pour permettre au mécanisme qui gère le contenu localisé de fonctionner. Par conséquent, le schéma précédent est équivalent à :

 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 }
     culture:     { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
     name:        varchar(50)

Au delà de schema.yml: Le fichier schema.xml

Le format schema.yml est interne à symfony. Lorsque vous exécutez une commande propel-, symfony génère en réalité un fichier generated-schema.xml, qui correspond au type de fichier attendu par Propel pour exécuter des tâches sur le modèle.

Le fichier schema.xml contient exactement les même informations que son équivalent YAML. Voici ce que donne au format XML le schéma Article/Comment défini précédemment :

 [xml]
 <?xml version="1.0" encoding="UTF-8"?>
  <database name="propel" defaultIdMethod="native" noxsd="true">
     <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 format schema.xml se trouve dans les sections documentation et bien démarrer du site du projet Propel.

Le format YAML a été conçu pour que les schémas demeurent facilent à lire et à écrire. Mais de fait, les schémas plus complexes ne peuvent pas être décrits dans un fichier schema.yml. Le format XML permet, par contre, une description complexe du schéma, quelque soit la complexité de celui-ci. Il intègre également les paramètres spécifiques aux différents types de données, la transmission des tables, etc.

Symfony peut lire les schémas écrits au format XML. Si votre schéma est trop complexe pour la syntaxe YAML, si vous avez déjà un schéma au format XML, ou si vous connaissez déjà Propel et que vous ne souhaitez pas réécrire votre schéma en YAML, il vous suffit de mettre votre fichier schema.xml dans le répertoire config/ de votre projet, puis de construire le modèle.

Ne créez pas deux fois le modèle

Construire une structure SQL de base de données basé sur un schéma existant

Si vous commencez votre application en écrivant le fichier schema.yml, vous n'aurez pas envie de le faire une seconde fois en SQL pour créer les tables dans la base de données. Symfony peut générer une requête SQL qui va directement créer les tables à partir du modèle de données défini dans le fichier YAML. Pour cela, placez-vous à la racine du répertoire de votre projet et tapez :

 $ symfony propel-build-sql

You can connexion that you want to use in the propel.ini.

Un fichier schema.sql sera créé dans le répertoire myproject/data/sql/. Le code SQL généré sera optimisé pour le système de base de données défini par le paramètre phptype du fichier databases.yml. Vous pouvez également précisez la connexion que vous souhaitez utiliser dans le fichier propel.ini.

Vous pouvez utiliser directement le fichier schema.sql pour construire les tables. Par exemple avec MySQL :

 $ mysqladmin -u root -p create blog
$ mysql -u root -p blog < data/sql/schema.sql

Cette commande est également utile pour reconstruire la base dans un autre environnement, ou pour changer de système de base de données.

Note: Si les paramètres de connexion sont correctement définis dans votre fichier propel.ini, vous pouvez utiliser la commande symfony propel-insert-sql pour effectuer cette tâche automatiquement.

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

Symfony peut utiliser la couche d'accès aux bases de données de Creole pour générer un fichier schema.xml à partir d'une base existante. Cela peut se révéler particulièrement utile lorsque vous faites de la rétro ingénierie (reverse engineering), ou si vous préférez travailler sur la base de données avant de vous attaquez au modèle objet.

Vous devez pour cela vous assurez que le fichier propel.ini de votre projet pointe sur la bonne base de données et contient tous les paramètres de connexion.

Vous pouvez ensuite appeler la commande propel-build-schema :

 $ symfony propel-build-schema

Un nouveau fichier schema.xml sera généré dans votre répertoire myproject/config/, basé sur la structure de votre base de données. Vous pouvez construire votre modèle à partir de cette structure.

La commande qui génère les schémas est assez puissante. Elle peut ajouter à votre schéma de nombreuses informations propres à la base de données. Parce que le format YAML ne gère pas les informations relatives aux différents types de bases de données du marché, vous devez générer un schéma XML pour bénéficier de ces informations. Pour cela, il suffit d'ajouter un argument à la commande build-schema :

 $ symfony propel-build-schema xml

Plutôt que de générer un fichier schema.yml, cette commande va créer un fichier schema.xml, compatible avec Propel, qui contiendra les informations relatives au type de la base. Soyez conscient que les schémas XML générés de la sorte tendent à être assez chargés et difficile à lire.

Bases de données multiples

Vous pouvez définir plusieurs bases de données pour un même projet et répartir vos données sur plusieurs bases. Il faut pour cela écrire plusieurs fichiers schema.yml en indiquant un préfixe spécifique à chaque nom, tout en conservent l'appellation schema.yml à la fin, afin qu'ils soient reconnus.

Par exemple, si vous souhaitez séparer les connexions pour les parties blog et dépôt d'images de votre site, créez deux fichiers dans votre répertoire config/:

 //dans le fichier blog_schema.yml
 blog:
   table1:
     column1: ...
     column2: ...
 //dans le fichier image_schema.yml
 imagerep:
   table1:
     column1: ...
     column2: ...

Refactoriser la couche de données

Lors du développement d'un projet symfony, vous démarrez souvent en écrivant le code du domaine logique dans les actions. Plus les projets deviennent complexes, plus les actions contiennent de code PHP et SQL. Ils deviennent alors moins lisible.

La logique liée aux données doit alors être déplacée vers la couche Modèle. Toutes les fois où vous avez à faire la même requête à plus d'un endroit au sein de vos actions, pensez à transférer le code correspondant au Modèle. Cela aide à garder les actions compactes et lisibles.

Imaginez, par exemple, le code nécessaire dans un weblog pour récupérer les 10 articles les plus populaires pour un tag donné (passé en paramètre de requête). Ce code ne devrait pas se trouver dans une action, mais dans le modèle. Si vous devez afficher cette liste dans un template, l'action devrait ressembler à ceci :

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

L'action crée un objet de la classe Tag à l'aide du paramètre de la requête. Le code nécessaire pour faire une requête à la base est situé dans la méthode ->getPopularArticles() de cette classe. Mettre le code de cette méthode dans l'action rendrait le code moins lisible.

Déplacer le code dans un endroit plus approprié est l'une des techniques de refactorisation. Si vous le faites souvent, votre code sera plus facile à maintenir et à comprendre des autres développeurs. Une bonne règle de conduite pour savoir quand effectuer une refactorisation vers la couche de données, c'est lorsque le code d'une action compte plus de dix lignes de code PHP.

Propel dans symfony

Tous les détails donnés ci-dessus ne sont pas spécifique à symfony. Vous n'êtes pas obligé d'utiliser Propel comme couche d'abstraction objet/relationnelle, mais symfony fonctionne de façon plus homogène avec Propel du fait que :

  • Toutes les classes du modèle de données objet et les classes Criteria sont des classes chargées automatiquement. Dès que vous les utiliserez, symfony inclura les bons fichiers, vous n'aurez pas à gérer vous même à la main les inclusions de fichiers.
  • Dans symfony, Propel n'a pas besoin d'être lancé ou initialisé. Lorsqu'un objet utilise Propel, la bibliothèque l'initialise d'elle-même.
  • Les résultats d'une requête peuvent être facilement paginées (pour plus de détails reportez-vous au chapitre pager?).
  • Les objets Propel permettent le prototypage rapide et la génération d'un backend pour votre application (pour plus de détails reportez vous au chapitre scaffolding?).
  • Le fichier schema.xml est plus rapide à écrire via le fichier schema.yml.

Enfin, parce que Propel est indépendant de la base de données utilisée, symfony l'est aussi.