Dans un projet web, la plupart des formulaires sont utilisés pour créer ou modifier des objets du modèle. Ces objets sont généralement sérialisés dans une base de données grâce à un ORM. Le système de formulaires de Symfony offre une couche supplémentaire pour l'interfaçage avec Doctrine, l'ORM intégré à symfony, rendant l'implémentation de formulaires basés sur ces objets modèles plus aisée.
Ce chapitre détaille comment intégrer des formulaires avec les modèles objet Doctrine. Il est hautement recommandé d'être déjà familier avec Doctrine et son intégration dans symfony. Si ce n'est pas le cas, référez-vous au chapitre Inside the Model Layer du livre "The Definitive Guide to symfony".
Avant de commencer :
Dans ce chapitre, nous allons créer un système de gestion d'articles. Commençons avec le schéma de base de données. Il est composé de cinq tables : article
, author
, category
, tag
, et article_tag
, comme le montre le Listing 4-1 :
Listing 4-1 - Schéma de base de données
// config/doctrine/schema.yml Article: actAs: [Sluggable, Timestampable] columns: title: type: string(255) notnull: true content: type: clob status: string(255) author_id: integer category_id: integer published_at: timestamp relations: Author: foreignAlias: Articles Category: foreignAlias: Articles Tags: class: Tag refClass: ArticleTag foreignAlias: Articles Author: columns: first_name: string(20) last_name: string(20) email: string(255) active: boolean Category: columns: name: string(255) Tag: columns: name: string(255) ArticleTag: columns: article_id: type: integer primary: true tag_id: type: integer primary: true relations: Article: onDelete: CASCADE Tag: onDelete: CASCADE
Voici les relations entre les tables :
- relation 1-n entre les tables
article
etauthor
: un article est écrit par un et un seul auteur - relation 1-n entre les tables
article
etcategory
: un article appartient à zéro ou plusieurs catégories - relation n-n entre les tables
article
ettag
Générer les classes de formulaires
Nous voulons éditer les informations des tables article
, author
, category
, et tag
. Pour cela, nous devons créer des formulaires liés à chacune de ces tables et configurer les widgets et les validateurs en relation avec le schéma de base de données. Même s'il est possible de créer ces formulaires manuellement, c'est une tâche longue et ennuyeuse, et de plus, cela force à répéter le même genre d'informations dans plusieurs fichiers (nom de colonne et de champ, taille maximale, ...). De plus, chaque fois que nous changerons le modèle, nous devrons également changer les formulaires adéquats. Heureusement, le plugin Doctrine a une tâche intégrée, doctrine:build-forms
, qui automatise le processus de génération des formulaires liés au modèle :
$ ./symfony doctrine:build-forms
Durant la génération des formulaires, la tâche crée une classe par table avec des validateurs et des widgets pour chaque colonne en utilisant l'introspection du modèle, et en prenant en compte les relations entre les tables.
note
Les tâches doctrine:build-all
et doctrine:build-all-load
mettent également à jour les classes des formulaires, en exécutant automatiquement la tâche doctrine:build-forms
.
Après l'exécution de ces tâches, une arborescence de fichiers est créée dans le répertoire lib/form/
. Voici les fichiers créés pour notre modèle d'exemple :
lib/ form/ doctrine/ ArticleForm.class.php ArticleTagForm.class.php AuthorForm.class.php CategoryForm.class.php TagForm.class.php base/ BaseArticleForm.class.php BaseArticleTagForm.class.php BaseAuthorForm.class.php BaseCategoryForm.class.php BaseFormDoctrine.class.php BaseTagForm.class.php
La tâche doctrine:build-forms
génère deux classes pour chaque table du schéma, une classe de base dans le répertoire lib/form/base
et une autre dans lib/form/
. Par exemple, pour la table author
, deux classes BaseAuthorForm
et AuthorForm
ont été créées dans les fichiers lib/form/base/BaseAuthorForm.class.php
et lib/form/AuthorForm.class.php
.
La table ci-dessous résume la hiérarchie entre les différentes classes impliquées dans la définition du formulaire AuthorForm
.
Classe | Package | Pour | Description |
---|---|---|---|
AuthorForm | projet | developpeur | Redéfinit le formulaire généré |
BaseAuthorForm | projet | symfony | Basée sur le schéma et réécrite à chaque exécution de doctrine:build-forms |
BaseFormDoctrine | projet | developpeur | Permet la personnalisation globale des formulaires Doctrine |
sfFormDoctrine | plugin Doctrine | symfony | Base des formulaires Doctrine |
sfForm | symfony | symfony | Base des formulaires symfony |
Afin de créer ou d'éditer un objet de la classe Author
, nous allons utiliser la classe AuthorForm
, décrite dans le Listing 4-2. Comme vous pouvez le constater, cette classe ne contient aucune méthode, car elle hérite de BaseAuthorForm
qui est générée d'après la configuration. La classe AuthorForm
est celle que nous allons utiliser pour personnaliser et surcharger la configuration du formulaire.
Listing 4-2 - La classe AuthorForm
class AuthorForm extends BaseAuthorForm { public function configure() { } }
Le Listing 4-3 montre la classe BaseAuthorForm
avec les validateurs et les widgets générés par introspection du modèle pour la table author
.
Listing 4-3 - La classe BaseAuthorForm
représentant le formulaire de la table author
class BaseAuthorForm extends BaseFormDoctrine { public function setup() { $this->setWidgets(array( 'id' => new sfWidgetFormInputHidden(), 'first_name' => new sfWidgetFormInput(), 'last_name' => new sfWidgetFormInput(), 'email' => new sfWidgetFormInput(), )); $this->setValidators(array( 'id' => new sfValidatorDoctrineChoice(array('model' => 'Author', 'column' => 'id', 'required' => false)), 'first_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)), 'last_name' => new sfValidatorString(array('max_length' => 20, 'required' => false)), 'email' => new sfValidatorString(array('max_length' => 255)), )); $this->widgetSchema->setNameFormat('author[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); parent::setup(); } public function getModelName() { return 'Author'; } }
La classe générée ressemble beaucoup aux formulaires que nous avons déjà créés dans les précédents chapitres, hormis le fait que :
- La classe de base est
BaseFormDoctrine
au lieu desfForm
- La configuration des validateurs et des widgets se fait dans la méthode
setup()
plutôt que dansconfigure()
- La méthode
getModelName()
retourne la classe Doctrine associée à ce formulaire
Le nom des champs du formulaire sont identiques aux noms de colonnes spécifiés dans le schéma : id
, first_name
, last_name
, et email
.
Pour chaque colonne de la tableau author
, la tâche doctrine:build-forms
génère un widget et un validateur en accord avec la définition du schéma. La tâche génère toujours le validateur le plus sécurisé possible. Prenons par exemple le champ id
. Nous pourrions simplement vérifier que sa valeur est un entier valide. Au lieu de ça, le validateur généré ici nous permet de valider aussi le fait que l'identifiant existe bel et bien (pour éditer un objet existant), ou bien qu'il soit vide (pour pouvoir créer un nouvel objet). C'est une validation plus forte.
Les formulaires générés peuvent être utilisés immédiatement. Ajoutez une déclaration <?php echo $form ?>
dans votre template, et vous obtiendrez un formulaire fonctionnel avec validation sans avoir écrit une seule ligne de code.
Outre la possibilité de créer rapidement des prototypes, les formulaires générés sont faciles à étendre sans avoir à modifier les classes générées. Ceci est possible grâce au mécanisme d'héritage des classes de base et de formulaires.
Enfin, à chaque évolution du schéma de base de données, la tâche permet de générer à nouveau les formulaires pour prendre en compte les modifications du schéma, sans surcharger les personnalisations que vous auriez pu faire.
Le générateur CRUD
Maintenant qu'il y a des classes de formulaires générées, voyons comment il est facilement possible de créer un module symfony pour gérer les objets depuis un navigateur. Nous souhaitons créer, modifier et supprimer des objets des classes Article
, Author
, Category
, et Tag
.
Commençons par la création pour la classe Author
. Même si nous pouvons créer manuellement un module, le plugin Doctrine fournit la tâche doctrine:generate-crud
qui génère un module CRUD basé sur une classe du modèle objet Doctrine, utilisant le formulaire généré dans la section précédente :
$ ./symfony doctrine:generate-crud frontend author Author
doctrine:generate-crud
prend trois arguments :
frontend
: nom de l'application où vous voulez créer un moduleauthor
: nom du module à créerAuthor
: nom de la classe du modèle pour laquelle vous voulez créer un module
note
CRUD signifie Creation / Retrieval / Update / Deletion (Création / Récupération / Mise à jour / Suppression) et résume les quatre opérations basiques que l'on peut effectuer sur les données du modèle.
Dans le Listing 4-4, nous voyons que la tâche a généré cinq actions nous permettant de lister (index
), créer (create
), modifier (edit
), enregistrer (update
), et supprimer (delete
) les objets de la classe Author
.
Listing 4-4 - La classe authorActions
générée par la tâche
// apps/frontend/modules/author/actions/actions.class.php class authorActions extends sfActions { public function executeIndex() { $this->authorList = $this->getAuthorTable()->findAll(); } public function executeCreate() { $this->form = new AuthorForm(); $this->setTemplate('edit'); } public function executeEdit($request) { $this->form = $this->getAuthorForm($request->getParameter('id')); } public function executeUpdate($request) { $this->forward404Unless($request->isMethod('post')); $this->form = $this->getAuthorForm($request->getParameter('id')); $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->get('id')); } $this->setTemplate('edit'); } public function executeDelete($request) { $this->forward404Unless($author = $this->getAuthorById($request->getParameter('id'))); $author->delete(); $this->redirect('author/index'); } private function getAuthorTable() { return Doctrine::getTable('Author'); } private function getAuthorById($id) { return $this->getAuthorTable()->find($id); } private function getAuthorForm($id) { $author = $this->getAuthorById($id); if ($author instanceof Author) { return new AuthorForm($author); } else { return new AuthorForm(); } } }
Dans ce module, le cycle de vie du formulaire est géré par trois méthodes : create
, edit
et update
. Il est aussi possible de demander à doctrine:generate-crud
de générer une seule méthode couvrant les fonctionnalités des trois précédentes méthodes avec l'option --non-atomic-actions
:
$ ./symfony doctrine:generate-crud frontend author Author --non-atomic-actions
Le code généré en utilisant --non-atomic-actions
(Listing 4-5) est plus concis et moins verbeux.
Listing 4-5 - La classe authorActions
générée avec l'otion --non-atomic-actions
class authorActions extends sfActions { public function executeIndex() { $this->authorList = $this->getAuthorTable()->findAll(); } public function executeEdit($request) { $this->form = new AuthorForm(Doctrine::getTable('Author')->find($request->getParameter('id'))); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->getId()); } } } public function executeDelete($request) { $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id'))); $author->delete(); $this->redirect('author/index'); } }
La tâche génère également deux templates, indexSuccess
et editSuccess
. Le template editSuccess
a été généré sans la déclaration <?php echo $form ?>
. Nous pouvons modifier ce comportement en utilisant --non-verbose-templates
:
$ ./symfony doctrine:generate-crud frontend author Author --non-verbose-templates
Cette option est utile lors de la phase de prototypage, comme le montre le Listing 4-6.
Listing 4-6 - Le template editSuccess
// apps/frontend/modules/author/templates/editSuccess.class.php <?php $author = $form->getObject() ?> <h1><?php echo $author->isNew() ? 'New' : 'Edit' ?> Author</h1> <form action="<?php echo url_for('author/edit'.(!$author->isNew() ? '?id='.$author->getId() : '')) ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>> <table> <tfoot> <tr> <td colspan="2"> <a href="<?php echo url_for('author/index') ?>">Cancel</a> <?php if (!$author->isNew()): ?> <?php echo link_to('Delete', 'author/delete?id='.$author->getId(), array('post' => true, 'confirm' => 'Are you sure?')) ?> <?php endif; ?> <input type="submit" value="Save" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>
tip
L'option --with-show
nous permet de générer une action et un template utilisables pour voir un objet (en lecture seule).
Vous pouvez maintenant ouvrir l'URL /frontend_dev.php/author
dans un navigateur pour voir le module généré (Figure 4-1 et Figure 4-2). Grâce au module généré, vous pouvez lister les auteurs, en ajouter de nouveaux, les éditer, les modifier et même les supprimer. Vous noterez également que les règles de validation fonctionnent.
Figure 4-1 - Liste d'auteurs
Figure 4-2 - Édition d'un auteur avec erreurs de validation
Nous pouvons maintenant répéter l'opération avec la classe Article
:
$ ./symfony doctrine:generate-crud frontend article Article --non-verbose-templates --non-atomic-actions
Le code généré est assez similaire à celui de la classe Author
. Cependant, si vous essayez de créer un nouvel article, le code va lancer une erreur fatale comme vous pouvez le voir en Figure 4-3.
Figure 4-3 - Les tables liées doivent définir la méthode __toString()
Le formulaire ArticleForm
utilise le widget sfWidgetFormDoctrineSelect
pour représenter la relation entre l'objet Article
et l'objet Author
. Ce widget crée une liste déroulante avec les auteurs. Durant l'affichage, les objets auteur sont convertis en chaîne de caractères grâce à la méthode magique __toString()
, qui doit être définie dans la classe Author
, comme montré en Listing 4-7.
Listing 4-7 - Implementation de __toString()
pour la classe Author
class Author extends BaseAuthor { public function __toString() { return $this->getFirstName().' '.$this->getLastName(); } }
Tout comme pour la classe Author
, vous pouvez créer la méthode __toString()
pour les autres classes du modèle : Article
, Category
, et Tag
.
note
sfDoctrineRecord va essayer de deviner la valeur de __toString() si vous ne la spécifiez pas vous-même. Elle regarde les colonnes nommées titre, nom, sujet, etc. pour les utiliser comme représentation de chaîne.
Tip
L'option method
du widget sfWidgetFormDoctrineSelect
change le nom de la méthode utilisée pour représenter un objet au format texte.
La Figure 4-4 montre comment créer un article après avoir implémenté la méthode __toString()
.
Figure 4-4 - Création d'un article
Personnalisation des formulaires générés
Les tâches doctrine:build-forms
et doctrine:generate-crud
permettent de créer des modules symfony fonctionnels pour lister, créer, éditer et supprimer des objets modèle. Ces modules prennent en compte non seulement les règles de validation du modèle, mais également les relations entre les tables. Tout ceci se produit sans écrire la moindre ligne de code !
Le temps est maintenant venu de personnaliser le code généré. Si les classes de formulaires prennent déjà en considération beaucoup d'éléments, certains aspects devront être personnalisés.
Configuration des validateurs et des widgets
Commençons par configurer les validateurs et les widgets générés par défaut.
Le formulaire ArticleForm
a un champ slug
. Le slug est une chaine de caractères qui représente de manière unique l'article dans l'URL. Par exemple, le slug d'un article dont le titre est "Optimiser le développement avec symfony" est 12-optimiser-le-developpement-avec-symfony
, 12
étant l'id
de l'article. Ce champ est généralement calculé automatiquement lorsque l'objet est enregistré, en fonction de title
, mais il a le potentiel d'être explicitement redéfini par l'utilisateur. Même si ce champ est requis dans le schéma, il ne peut pas être obligatoire dans le formulaire. C'est pourquoi nous modifions le validateur et le rendons optionnel, comme dans le Listing 4-8. Nous allons aussi modifier le champ content
en augmentant sa taille et en forçant l'utilisateur à entrer au moins cinq caractères.
Listing 4-8 - Personnalisation des validateurs et des widgets
class ArticleForm extends BaseArticleForm { public function configure() { // ... $this->validatorSchema['slug']->setOption('required', false); $this->validatorSchema['content']->setOption('min_length', 5); $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40)); } }
Nous utilisons ici les objets validatorSchema
et widgetSchema
comme des tableaux PHP. Ces tableaux prennent un nom de champ en clé et retournent respectivement les objets validateur et widget asociés. Nous pouvons alors personnaliser individuellement les champs et les widgets.
note
Afin de permettre l'utilisation d'objets en tant que tableaux PHP, les classes sfValidatorSchema
et sfWidgetFormSchema
implémentent l'interface ArrayAccess
, disponible depuis la version 5 de PHP.
Pour être sûr que deux articles ne peuvent avoir le même slug
, une contrainte d'unicité a été ajoutée dans la définition du schéma. La contrainte au niveau de la base de données est utilisée dans le formulaire ArticleForm
en utilisant le validateur sfValidatorDoctrineUnique
. Ce validateur peut vérifier l'unicité de n'importe quel champ du formulaire. Il est utile entre autres choses pour vérifier l'unicité d'une adresse courriel ou d'un login par exemple. Le Listing 4-9 montre comment l'utiliser dans ArticleForm
.
Listing 4-9 - Utilisation du validateur sfValidatorDoctrineUnique
pour vérifier l'unicité d'un champ
class BaseArticleForm extends BaseFormDoctrine { public function setup() { // ... $this->validatorSchema->setPostValidator( new sfValidatorDoctrineUnique(array('model' => 'Article', 'column' => array('slug'))) ); } }
Le validateur sfValidatorDoctrineUnique
est un postValidator
s'exécutant sur toutes les données après les validations individuelles des champs. Afin de valider l'unicité de slug
, le validateur doit être capable d'accéder non seulement à la valeur de slug
, mais également à la valeur de(s) clé(s) primaire(s). Les règles de validation sont ainsi différentes entre la création et l'édition, puisque le slug peut rester le même lors de la mise à jour d'un article.
Personnalisons maintenant le champ active
de la table author
, utilisé pour savoir si un auteur est actif. Le Listing 4-10 montre comment exclure les auteurs inactifs du formulaire AuthorForm
, en modifiant l'option query
du widget Chapitreck php">
class ArticleForm extends BaseArticleForm
{
public function configure()
{
// ...
$query = Doctrine_Query::create()
->from('Author a')
->where('a.active = ?', true);
$this->widgetSchema['author_id']->setOption('query', $query);
}
}
Même si la personnalisation du widget peut nous permettre de réduire la liste des options disponibles, nous ne devons pas oublier de reporter cette réduction au niveau du validateur, comme montré dans le Listing 4-11. Comme le widget sfWidgetProperSelect
, le validateur sfValidatorDoctrineChoice
accepte une option query
pour cibler les options valides pour un champ.
Listing 4-11 - Personnalisation du validateur sfValidatorDoctrineChoice
class ArticleForm extends BaseArticleForm { public function configure() { // ... $query = Doctrine_Query::create() ->from('Author a') ->where('a.active = ?', true); $this->widgetSchema['author_id']->setOption('query', $query); $this->validatorSchema['author_id']->setOption('query', $query); } }
Dans le précédent exemple, nous avons défini un objet Query
directement dans la méthode configure()
. Dans notre projet, cette requête sera certainement utile en d'autres circonstances, il est donc préférable de créer une méthode getActiveAuthorsQuery()
dans la classe AuthorPeer
et d'appeler cette méthode depuis ArticleForm
comme le montre le Listing 4-12.
Listing 4-12 - Refactorisation de Query
dans le modèle
class AuthorTable extends Doctrine_Table { public function getActiveAuthorsQuery() { $query = Doctrine_Query::create() ->from('Author a') ->where('a.active = ?', true); return $query; } } class ArticleForm extends BaseArticleForm { public function configure() { $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery(); $this->widgetSchema['author_id']->setOption('query', $authorQuery); $this->validatorSchema['author_id']->setOption('query', $authorQuery); } }
tip
Tout comme le widget sfWidgetFormDoctrineSelect
et le validateur sfValidatorDoctrineChoice
représentent une relation 1-n entre deux tables, le widget sfWidgetDoctrineSelectMany
et le validateur sfValidatorDoctrineChoiceMany
représentent une relation n-n et acceptent les mêmes options. Dans le formulaire ArticleForm
, ces classes sont utilisées pour représenter une relation entre la table article
et la table tag
.
Changer les validateurs
L'email
étant défini comme un string(255)
dans le schéma, symfony a créé un validateur sfValidatorString()
restreignant la longueur maximale à 255 caractères. Ce champ est aussi supposé contenir des adresses email valides, le Listing 4-13 remplace le validateur généré par un sfValidatorEmail
.
Listing 4-13 - Changement du validateur du champ email
de la classe AuthorForm
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorEmail(); } }
Ajouter un validateur
Nous avons vu dans la précédente partie comment modifier le validateur généré. Mais dans le cas du champ email
, il serait utile de garder la validation de longueur maximale. Dans le Listing 4-14, nous utilisons le validateur sfValidatorAnd
pour garantir la validité de l'email et vérifier la taille maximale possible pour le champ.
Listing 4-14 - Utilisation d'un validateur multiple
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( new sfValidatorString(array('max_length' => 255)), new sfValidatorEmail(), )); } }
L'exemple précédent n'est pas parfait, car si nous décidons de modifier par la suite la longueur du champ email
dans le schéma de base de données, nous devrons penser à le faire également dans le formulaire. Au lieu de remplacer le validateur généré, il est préférable d'en ajouter un, comme le montre le Listing 4-15.
Listing 4-15 - Ajout d'un validateur
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
Changer un widget
Dans le schéma de base de données, le champ status
de la table article
stocke les statuts d'articles dans une chaine de caractères. Les valeurs possibles ont été définies dans la classe ArticlePeer
, comme montré dans le Listing 4-16.
Listing 4-16 - Définition des statuts possibles dans la classe ArticlePeer
class ArticlePeer extends BaseArticlePeer { static protected $statuses = array('draft', 'online', 'offline'); static public function getStatuses() { return self::$statuses; } // ... }
Lors de l'édition d'un article, le champ status
doit être représenté comme une liste déroulante au lieu d'un champ texte. Pour ce faire, changeons le widget utilisé, comme le montre le Listing 4-17.
Listing 4-17 - Changement du widget pour le champ status
class ArticleForm extends BaseArticleForm { public function configure() { $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticlePeer::getStatuses())); } }
Pour être exhaustifs, nous devons également changer le validateur pour être sûrs que le statut choisi est bien présent dans la liste des options possibles (Listing 4-18).
Listing 4-18 - Modification du validateur du champ status
class ArticleForm extends BaseArticleForm { public function configure() { $statuses = ArticlePeer::getStatuses(); $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => $statuses)); $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys($statuses))); } }
Supprimer un champ
La table article
a tres colonnes spéciales, created_at
, updated_at
et published_at
, dont la mise à jour est automatiquement gérée par Doctrine. Nous devons donc les supprimer du formulaire comme le montre le Listing 4-19, pour éviter que les utilisateurs les modifient.
Listing 4-19 - Suppression d'un champ
class ArticleForm extends BaseArticleForm { public function configure() { // ... unset($this->validatorSchema['created_at']); unset($this->widgetSchema['created_at']); unset($this->validatorSchema['updated_at']); unset($this->widgetSchema['updated_at']); unset($this->validatorSchema['published_at']); unset($this->widgetSchema['published_at']); } }
Afin de supprimer un champ, il est nécessaire de supprimer son validateur et son widget. Le Listing 4-20 montre comment il est possible de les supprimer tous les deux en une action, en utilisant le formulaire comme un tableau PHP.
Listing 4-20 - Suppression d'un champ en utilisant le formulaire comme un tableau PHP
class ArticleForm extends BaseArticleForm { public function configure() { unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
Résumé
Pour résumer, le Listing 4-21 et le Listing 4-22 montrent les formulaires ArticleForm
et AuthorForm
personnalisés.
Listing 4-21 - Formulaire ArticleForm
class ArticleForm extends BaseArticleForm { public function configure() { $authorQuery = Doctrine::getTable('Author')->getActiveAuthorsQuery(); // widgets $this->widgetSchema['content']->setAttributes(array('rows' => 10, 'cols' => 40)); $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticlePeer::getStatuses())); $this->widgetSchema['author_id']->setOption('query', $authorQuery); // validators $this->validatorSchema['slug']->setOption('required', false); $this->validatorSchema['content']->setOption('min_length', 5); $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys(ArticlePeer::getStatuses()))); $this->validatorSchema['author_id']->setOption('query', $authorQuery); unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
Listing 4-22 - Formulaire AuthorForm
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
Utiliser doctrine:build-forms
permet de générer la plupart des éléments en introspectant le modèle objet. Cette automatisation est utile pour plusieurs raisons :
Cela rend la vie plus facile au développeur, lui épargnant un travail répétitif et redondant. Il peut alors se concentrer sur la personnalisation des validateurs et des widgets en fonction des règles métier spécifiques au projet.
De plus, lorsque le schéma de base de données est mis à jour, les formulaires générés seront automatiquement mis à jour. Le développeur aura juste à adapter ses personnalisations.
La prochaine section décrira la personnalisation des actions et des templates générés par la tâche doctrine:generate-crud
.
Sérialisation des formulaires
La précédente section nous montre comment personnaliser les formulaires générés par la tâche doctrine:build-forms
. Dans la section courante, nous allons personnaliser le cycle de vie des formulaires, en commençant par le code généré par doctrine:generate-crud
.
Valeurs par défaut
Une instance de formulaire Doctrine est toujours reliée à un objet Doctrine. L'objet Doctrine lié appartient toujours à la classe retournée par la méthode getModelName()
. Par exemple, le formulaire AuthorForm
peut seulement être lié à des objets appartenant à la classe Author
. Ces objets sont soit des objets vides (une instance vide de la classe Author
), soit les objets passés en premier argument au constructeur. Alors que le constructeur d'un formulaire « normal » prend un tableau de valeurs en premier argument, le constructeur d'un formulaire Doctrine prend un objet Doctrine. Cet objet est utilisé pour définir la valeur par défaut de chaque champ du fomulaire. La méthode getObject()
retourne l'objet relié à l'instance courante, et la méthode isNew()
permet de savoir si l'objet a été créé par le constructeur :
// création d'un nouvel objet $authorForm = new AuthorForm(); print $authorForm->getObject()->getId(); // affiche null print $authorForm->isNew(); // affiche true // modification d'un objet existant $author = Doctrine::getTable('Author')->find(1); $authorForm = new AuthorForm($author); print $authorForm->getObject()->getId(); // affiche 1 print $authorForm->isNew(); // affiche false
Gestion du cycle de vie
Comme nous l'avons observé au début de ce chapitre, l'action edit
, montrée dans le listing 4-23, gère le cycle de vie.
Listing 4-23 - La méthode executeEdit
du module author
// apps/frontend/modules/author/actions/actions.class.php class authorActions extends sfActions { // ... public function executeEdit($request) { $author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new AuthorForm($author); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('author')); if ($this->form->isValid()) { $author = $this->form->save(); $this->redirect('author/edit?id='.$author->getId()); } } } }
Même si l'action edit
ressemble aux actions que nous avons pu décrire dans les chapitres précédents, nous pouvons remarquer quelques différences :
Un objet Doctrine de la classe
Author
est passé en premier paramètre du constructeur du formulaire :$author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new AuthorForm($author);
Le format des attributs
name
des widgets est automatiquement défini de manière à récupérer les données soumises dans un tableau PHP nommé d'après la table liée (author
) :$this->form->bind($request->getParameter('author'));
Lorsque le formulaire est valide, un appel à la méthode
save()
crée ou met à jour les objets Doctrine reliés au formulaire :$author = $this->form->save();
Créer et modifier un objet Doctrine
Le code du Listing 4-23 gère en une seule méthode la création et la modification d'objets de la classe Author
:
Création d'un nouvel objet
Author
:L'action
index
est appelée sans paramètreid
($request->getParameter('id')
vautnull
)L'appel à la méthode
find()
renvoie dontnull
L'objet
form
est alors lié à un objet DoctrineAuthor
videL'appel à
$this->form->save()
crée un nouvel objetAuthor
en conséquence quand un formulaire valide est soumis
Modification d'un objet Author` existant :
L'action
index
est appelée avec un paramètreid
($request->getParameter('id')
représentant la clé primaire de l'objetAuthor
à modifier)L'appel à la méthode
find()
retourne l'objetAuthor
associé à la clé primaireL'objet
form
est donc lié à l'objet précédemment trouvéL'appel à
$this->form->save()
met à jour l'objetAuthor
quand un formulaire valide est soumis
La méthode save()
Quand un formulaire Doctrine est valide, la méthode save()
met à jour l'objet associé et l'enregistre dans la base de données. Cette méthode enregistre en fait non seulement l'objet principal, mais aussi les objets potentiellement reliés. Par exemple, le formulaire ArticleForm
met à jour les tags connectés à un article. La relation entre les tables article
et tag
étant n-n, les tags reliés à un article sont stockés dans la table article_tag
(en utilisant la méthode saveArticleTagList()
générée).
Afin de certifier une sérialisation consistante, la méthode save()
inclut toutes les mises à jour dans une transaction.
note
Nous verrons dans le chapitre 9 que la méthode save()
met aussi automatiquement à jour les tables internationalisées.
Gestion des envois de fichiers
La méthode save()
met automatiquement à jour les objets Doctrine, mais ne peut pas gérer les autres éléments tels que les envois de fichiers.
Voyons comment attacher un fichier à chaque article. Les fichiers sont enregistrés dans le répertoire web/uploads
et une référence vers le fichier est gardée dans le champ file
de la table article
, comme le montre le Listing 4-24.
Listing 4-24 - Schéma pour la table article
avec le fichier associé
// config/schema.yml doctrine: article: // ... file: string(255)
Après chaque mise à jour du schéma, vous devez mettre à jour le modèle objet, la base de données et les formulaires associés :
$ ./symfony doctrine:build-all
Attention Soyez conscients que la tâche
doctrine:build-all
supprime toutes les tables du schéma et les recrée. Les données dans les tables sont donc réécrites. C'est pourquoi il est important de créer des données de test (fixtures
) qui pourront être rechargées à chaque modification du modèle.
Le Listing 4-25 montre comment modifier la classe ArticleForm
afin de relier un widget et un validateur au champ file
.
Listing 4-25 - Modification du champ file
du formulaire ArticleForm
class ArticleForm extends BaseArticleForm { public function configure() { // ... $this->widgetSchema['file'] = new sfWidgetFormInputFile(); $this->validatorSchema['file'] = new sfValidatorFile(); } }
Comme pour chaque formulaire permettant l'envoi d'un fichier, n'oubliez pas d'ajouter l'attribut enctype
à la balise form
dans le template (voyez le Chapitre 2 pour plus d'informations concernant la gestion d'envois de fichiers).
Le Listing 4-26 montre les modifications à appliquer lors de l'enregistrement du formulaire pour enregistrer le fichier sur le serveur et stocker son chemin dans l'objet article
.
Listing 4-26 - Enregistrement de l'objet article
et du fichier envoyé dans une action
public function executeEdit($request) { $author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new ArticleForm($author); if ($request->isMethod('post')) { $this->form->bind($request->getParameter('article'), $request->getFiles('article')); if ($this->form->isValid()) { $file = $this->form->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); $article = $this->form->save(); $this->redirect('article/edit?id='.$article->getId()); } } }
Enregistrer le fichier envoyé sur le système de fichiers permet à l'objet sfValidatedFile
de connaître le chemin absolu du fichier. Pendant l'appel à save()
, les valeurs des champs sont utilisées pour mettre à jour l'objet associé et, comme pour le champ file
, l'objet sfValidatedFile
est converti en chaine de caractères grâce à la méthode __toString()
, renvoyant le chemin absolu du fichier. La colonne file
de la table article
stocke ce chemin absolu.
tip
Si vous désirez stocker le chemin relatif au répertoire sfConfig::get('sf_upload_dir')
, vous devez créer une classe héritant de sfValidatedFile
et utiliser l'option validated_file_class
pour passer au validateur sfValidatorFile
le nom de la nouvelle classe. Le validateur retournera alors une instance de votre classe. Nous verrons dans le reste de ce chapitre une autre approche, consistant à modifier la valeur de la colonne file
avant d'enregistrer l'objet dans la base de données.
Personnalisation de la méthode save()
Nous avons vu dans la section précédente comment enregistrer le fichier envoyé dans l'action edit
. Un des principes de la programmation orientée objet est la réutilisabilité du code, grâce à son encapsulation dans des classes. Au lieu de dupliquer le code utilisé pour enregistrer le fichier dans chaque action utilisant le formulaire ArticleForm
, il est préférable de le déplacer dans la classe ArticleForm
. Le Listing 4-27 montre comment surcharger la méthode save()
afin d'enregistrer aussi le fichier et éventuellement de supprimer un fichier existant.
Listing 4-27 - Surcharge de la méthode save()
de la classe ArticleForm
class ArticleForm extends BaseFormDoctrine { // ... public function save($con = null) { if (file_exists($this->getObject()->getFile())) { unlink($this->getObject()->getFile()); } $file = $this->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); return parent::save($con); } }
Après avoir déplacé le code dans le formulaire, l'action edit
est identique au code généré par doctrine:generate-crud
.
Personnalisation de la méthode doSave()
Nous avons vu que l'enregistrement d'un objet était fait dans une transaction afin de garantir que chaque opération liée à l'enregistrement soit exécutée correctement. Lorsque la méthode save()
est surchargée comme nous l'avons fait dans la section précédente pour enregistrer le fichier envoyé, le code exécuté est indépendant de cette transaction.
Le Listing 4-28 nous montre comment utiliser la méthode doSave()
pour insérer dans la transaction globale le code enregistrant le fichier envoyé.
Listing 4-28 - Surcharge de la méthode doSave()
dans le formulaire ArticleForm
class ArticleForm extends BaseFormDoctrine { // ... protected function doSave($con = null) { if (file_exists($this->getObject()->getFile())) { unlink($this->getObject()->getFile()); } $file = $this->getValue('file'); $filename = sha1($file->getOriginalName()).$file->getExtension($file->getOriginalExtension()); $file->save(sfConfig::get('sf_upload_dir').'/'.$filename); return parent::doSave($con); } }
La méthode doSave()
étant appelée dans la transaction créé par la méthode save()
, si l'appel à la méthode save()
de l'objet file()
lance une exception, l'objet ne sera pas enregistré.
Personnalisation de la méthode updateObject()
Il est parfois nécessaire de modifier l'objet connecté au formulaire entre la mise à jour et l'enregistrement en base de données.
Dans l'exemple d'envoi de fichier, au lieu de stocker le chemin absolu du fichier envoyé dans la colonne file
, nous voulons enregistrer le chemin relatif au répertoire sfConfig::get('sf_upload_dir')
.
Le Listing 4-29 montre comment redéfinir la méthode updateObject()
du formulaire ArticleForm
afin de changer la valeur de la colonne file
après la mise à jour automatique, mais avant qu'elle soit enregistrée.
Listing 4-29 - Redéfinition de la méthode updateObject()
de la classe ArticleForm
class ArticleForm extends BaseFormDoctrine { // ... public function updateObject() { $object = parent::updateObject(); $object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile())); return $object; } }
La méthode updateObject()
est appelée par la méthode doSave()
avant d'enregistrer l'objet dans la base de données.