![]() |
|
symfony Forms in ActionChapter 11 - Doctrine Integration |
|
You are currently reading "symfony Forms in Action" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.
save() method save() method doSave() method updateObject() Method 
|
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. |
In a Web project, most forms are used to create or modify model objects. These objects are usually serialized in a database thanks to an ORM. Symfony's form system offers an additional layer for interfacing with Doctrine, symfony's built-in ORM, making the implementation of forms based on these model objects easier.
This chapter goes into detail about how to integrate forms with Doctrine object models. It is highly suggested to be already acquainted with Doctrine and its integration in symfony. If this is not the case, refer to the chapter Inside the Model Layer from the "The Definitive Guide to symfony" book.
In this chapter, we will create an article management system. Let's start with the database schema. It is made of five tables: article, author, category, tag, and article_tag, as Listing 4-1 shows.
Listing 4-1 - Database Schema
// 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
Here are the relations between the tables:
article table and the author table: an article is written by one and only one authorarticle table and the category table: an article belongs to one or zero categoryarticle and tag tablesWe want to edit the information of the article, author, category, and tag tables. To do so, we need to create forms linked to each of these tables and configure widgets and validators related to the database schema. Even if it is possible to create these forms manually, it is a long, tedious task, and overall, it forces repetition of the same kind of information in several files (column and field name, maximum size of column and fields, ...). Furthermore, each time we change the model, we will also have to change the related form class. Fortunately, the Doctrine plugin has a built-in task doctrine:build-forms that automates this process generating the forms related to the object model:
$ ./symfony doctrine:build-forms
During the form generation, the task creates one class per table with validators and widgets for each column using introspection of the model and taking into account relations between tables.
The
doctrine:build-allanddoctrine:build-all-loadalso updates form classes, automatically invoking thedoctrine:build-formstask.
After executing these tasks, a file structure is created in the lib/form/ directory. Here are the files created for our example schema:
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
The doctrine:build-forms task generates two classes for each table of the schema, one base class in the lib/form/base directory and one in the lib/form/ directory. For example, the author table, consists of BaseAuthorForm and AuthorForm classes that were generated in the files lib/form/base/BaseAuthorForm.class.php and lib/form/AuthorForm.class.php.
Table below sums up the hierarchy among the different classes involved in the AuthorForm form definition.
| Class | Package | For | Description |
|---|---|---|---|
| AuthorForm | project | developer | Overrides generated form |
| BaseAuthorForm | project | symfony | Based on the schema and overridden at each execution of the doctrine:build-forms task |
| BaseFormDoctrine | project | developer | Allows global Customization of Doctrine forms |
| sfFormDoctrine | Doctrine plugin | symfony | Base of Doctrine forms |
| sfForm | symfony | symfony | Base of symfony forms |
In order to create or edit an object from the Author class, we will use the AuthorForm class, described in Listing 4-2. As you can notice, this class does not contain any methods as it inherits from the BaseAuthorForm which is generated through the configuration. The AuthorForm class is the class we will use to Customize and override the form configuration.
Listing 4-2 - AuthorForm Class
class AuthorForm extends BaseAuthorForm { public function configure() { } }
Listing 4-3 shows the BaseAuthorForm class with the validators and widgets generated introspecting the model for the author table.
Listing 4-3 - BaseAuthorForm Class representing the Form for the author table
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'; } }
The generated class looks very similar to the forms we have already created in the previous chapters, except for a few things:
BaseFormDoctrine instead of sfFormsetup() method, rather than in the configure() methodgetModelName() method returns the Doctrine class related to this formGlobal Customization of Doctrine Forms
In addition to the classes generated for each table, the
doctrine:build-formsalso generates aBaseFormDoctrineclass. This empty class is the base class of every other generated class in thelib/form/base/directory and allows to configure the behavior of every Doctrine form globally. For example, it is possible to easily change the default formatter for all Doctrine forms:abstract class BaseFormDoctrine extends sfFormDoctrine { public function setup() { sfWidgetFormSchema::setDefaultFormFormatterName('div'); } }You'll notice that the
BaseFormDoctrineclass inherits from thesfFormDoctrineclass. This class incorporates functionality specific to Doctrine and among other things deals with the object serialization in database from the values submitted in the form.TIP Base classes use the
setup()method for the configuration instead of theconfigure()method. This allows the developer to override the configuration of empty generated classes without handling theparent::configure()call.
The form field names are identical to the column names we set in the schema: id, first_name, last_name, and email.
For each column of the author table, the doctrine:build-forms task generates a widget and a validator according to the schema definition. The task always generates the most secure validators possible. Let's consider the id field. We could just check if the value is a valid integer. Instead the validator generated here allows us to also validate that the identifier actually exists (to edit an existing object) or that the identifier is empty (so that we could create a new object). This is a stronger validation.
The generated forms can be used immediately. Add a <?php echo $form ?> statement, and this will allow to create functional forms with validation without writing a single line of code.
Beyond the ability to quickly make prototypes, generated forms are easy to extend without having to modify the generated classes. This is thanks to the inheritance mechanism of the base and form classes.
At last at each evolution of the database schema, the task allows to generate again the forms to take into account the schema modifications, without overriding the Customization you might have made.
Now that there are generated form classes, let's see how easy it is to create a symfony module to deal with the objects from a browser. We wish to create, modify, and delete objects from the Article, Author, Category, and Tag classes.
Let's start with the module creation for the Author class. Even if we can manually create a module, the Doctrine plugin provides the doctrine:generate-crud task which generates a CRUD module based on a Doctrine object model class. Using the form we generated in the previous section:
$ ./symfony doctrine:generate-crud frontend author Author
The doctrine:generate-crud takes three arguments:
frontend : name of the application you want to create the module inauthor : name of the module you want to createAuthor : name of the model class you want to create the module for CRUD stands for Creation / Retrieval / Update / Deletion and sums up the four basic operations we can carry out with the model datas.
In Listing 4-4, we see that the task generated five actions allowing us to list (index), create (create), modify (edit), save (update), and delete (delete) the objects of the Author class.
Listing 4-4 - The authorActions Class generated by the Task
// 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(); } } }
In this module, the form life cycle is handled by three methods: create, edit and, update. It is also possible to ask the doctrine:generate-crud task to generate only one method covering the three previous methods functionalities, with the option --non-atomic-actions:
$ ./symfony doctrine:generate-crud frontend author Author --non-atomic-actions
The generated code using --non-atomic-actions (Listing 4-5) is more concise and less verbose.
Listing 4-5 - The authorActions Class generated with the --non-atomic-actions option
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'); } }
The task also generated two templates, indexSuccess and editSuccess. The editSuccess template was generated without using the <?php echo $form ?> statement. We can modify this behavior, using the --non-verbose-templates:
$ ./symfony doctrine:generate-crud frontend author Author --non-verbose-templates
This option is helpful during prototyping phases, as Listing 4-6 shows.
Listing 4-6 - The editSuccess Template
// apps/frontend/modules/author/templates/editSuccess.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>
The
--with-showoption let us generate an action and a template we can use to view an object (read only).
You can now open the URL /frontend_dev.php/author in a browser to view the generated module (Figure 4-1 and Figure 4-2). Take time to play with the interface. Thanks to the generated module you can list the authors, add a new one, edit, modify, and even delete. You will also notice that the validation rules are also working.
Figure 4-1 - Authors List

Figure 4-2 - Editing an Author with Validation Errors

We can now repeat the operation with the Article class:
$ ./symfony doctrine:generate-crud frontend article Article --non-verbose-templates --non-atomic-actions
The generated code is quite similar to the code of the Author class. However, if you try to create a new article, the code throws a fatal error as you can see in Figure 4-3.
Figure 4-3 - Linked Tables must define the __toString() method

The ArticleForm form uses the sfWidgetFormDoctrineSelect widget to represent the relation between the Article object and the Author object. This widget creates a drop-down list with the authors. During the display, the authors objects are converted into a string of characters using the __toString() magic method, which must be defined in the Author class as shown in Listing 4-7.
Listing 4-7 - Implementing the __toString() method for the Author class
class Author extends BaseAuthor { public function __toString() { return $this->getFirstName().' '.$this->getLastName(); } }
Just like the Author class, you can create __toString() methods for the other classes of our model: Article, Category, and Tag.
sfDoctrineRecord will attempt to guess in the base __toString() if you do not specify your own. It checks for columns named title, name, subject, etc. to use as the string representation.
Tip The
methodoption of thesfWidgetFormDoctrineSelectwidget change the method used to represent an object in text format.
The Figure 4-4 Shows how to create an article after having implemented the __toString() method.
Figure 4-4 - Creating an Article

The doctrine:build-forms and doctrine:generate-crud tasks let us create functional symfony modules to list, create, edit, and delete model objects. These modules are taking into account not only the validation rules of the model but also the relationships between tables. All of this happens without writing a single line of code!
The time has now come to customize the generated code. If the form classes are already considering many elements, some aspects will need to be customized.
Let's start with configuring the validators and widgets generated by default.
The ArticleForm form has a slug field. The slug is a string of characters that uniquely representing the article in the URL. For instance, the slug of an article whose title is "Optimize the developments with symfony" is 12-optimize-the-developments-with-symfony, 12 being the article id. This field is usually automatically computed when the object is saved, depending on the title, but it has the potential to be explicitly overridden by the user. Even if this field is required in the schema, it can not be compulsory to the form. That is why we modify the validator and make it optional, as in Listing 4-8. We will also customize the content field increasing its size and forcing the user to type in at least five characters.
Listing 4-8 - Customizing Validators and 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)); } }
We use here the validatorSchema and widgetSchema objects as PHP arrays. These arrays are taking the name of a field as key and return respectively the validator object and the related widget object. We can then Customize individually fields and widgets.
In order to allow the use of objects as PHP arrays, the
sfValidatorSchemaandsfWidgetFormSchemaclasses implement theArrayAccessinterface, available in PHP since version 5.
To make sure two articles can not have the same slug, a uniqueness constraint has been added in the schema definition. This constraint on the database level is reflected in the ArticleForm form using the sfValidatorDoctrineUnique validator. This validator can check the uniqueness of any form field. It is helpful among other things to check the uniqueness of an email address of a login for instance. Listing 4-9 shows how to use it in the ArticleForm form.
Listing 4-9 - Using the sfValidatorDoctrineUnique validator to check the Uniqueness of a field
class BaseArticleForm extends BaseFormDoctrine { public function setup() { // ... $this->validatorSchema->setPostValidator( new sfValidatorDoctrineUnique(array('model' => 'Article', 'column' => array('slug'))) ); } }
The sfValidatorDoctrineUnique validator is a postValidator running on the whole data after the individual validation of each field. In order to validate the slug uniqueness, the validator must be able to access, not only the slug value, but also the value of the primary key(s). Validation rules are indeed different throughout the creation and the edition since the slug can stay the same during the update of an article.
Let's Customize now the active field of the author table, used to know if an author is active. Listing 4-10 shows how to exclude inactive authors from the ArticleForm form, modifying the query option of the `Chapter
Listing 4-10 - Customizing the sfWidgetFormDoctrineSelect widget
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); } }
Even if the widget customization can make us narrow down the list of available options, we must not forget to consider this narrowing on the validator level, as shown in Listing 4-11. Like the sfWidgetProperSelect widget, the sfValidatorDoctrineChoice validator accepts a query option to narrow down the options valid for a field.
Listing 4-11 - Customizing the sfValidatorDoctrineChoice validator
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); } }
In the previous example we defined the Query object directly in the configure() method. In our project, this query will certainly be helpful in other circumstances, so it is better to create a getActiveAuthorsQuery() method within the AuthorTable class and to call this method from ArticleForm as Listing 4-12 shows.
Listing 4-12 - Refactoring the Query in the Model
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); } }
Like the
sfWidgetFormDoctrineSelectwidget and thesfValidatorDoctrineChoicevalidator represent a 1-n relation between two tables, thesfWidgetDoctrineSelectManyand thesfValidatorDoctrineChoiceManyvalidator represent a n-n relation and accept the same options. In theArticleFormform, these classes are used to represent a relation between thearticletable and thetagtable.
The email being defined as a string(255) in the schema, symfony created a sfValidatorString() validator restraining the maximum length to 255 characters. This field is also supposed to receive a valid email, Listing 4-14 replaces the generated validator with a sfValidatorEmail validator.
Listing 4-13 - Changing the email field Validator of the AuthorForm class
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorEmail(); } }
We observed in the previous chapter how to modify the generated validator. But in the case of the email field, it would be useful to keep the maximum length validation. In Listing 4-14, we use the sfValidatorAnd validator to guarantee the email validity and check the maximum length allowed for the field.
Listing 4-14 - Using a multiple Validator
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( new sfValidatorString(array('max_length' => 255)), new sfValidatorEmail(), )); } }
The previous example is not perfect, because if we decide later to modify the length of the email field in the database schema, we will have to think about doing it also in the form. Instead of replacing the generated validator, it is better to add one, as shown in Listing 4-15.
Listing 4-15 - Adding a Validator
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
In the database schema, the status field of the article table stores the article status as a string of characters. The possible values were defined in the ArticeTable class, as shown in Listing 4-16.
Listing 4-16 - Defining available Statuses in the ArticleTable class
class ArticleTable extends Doctrine_Table { static protected $statuses = array('draft', 'online', 'offline'); static public function getStatuses() { return self::$statuses; } // ... }
When editing an article, the status field must be represented as a drop-down list instead of a text field. To do so, let's change the widget we used, as shown in Listing 4-17.
Listing 4-17 - Changing the Widget for the status field
class ArticleForm extends BaseArticleForm { public function configure() { $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => ArticleTable::getStatuses())); } }
To be thorough we must also change the validator to make sure the chosen status actually belongs to the list of possible options (Listing 4-18).
Listing 4-18 - Modifying the status Field Validator
class ArticleForm extends BaseArticleForm { public function configure() { $statuses = ArticleTable::getStatuses(); $this->widgetSchema['status'] = new sfWidgetFormSelect(array('choices' => $statuses)); $this->validatorSchema['status'] = new sfValidatorChoice(array('choices' => array_keys($statuses))); } }
The article table has three special columns, created_at, updated_at
and published_at. The first two are automatically handled by Doctrine as part
of the timestampable behaviour, the third we will handle at a later date in our
own code. We must delete them from the form as Listing 4-19 shows, to prevent
the user from modifying them.
Listing 4-19 - Deleting a Field
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']); } }
In order to delete a field, it is necessary to delete its validator and its widget. Listing 4-20 shows how it is also possible to delete both in one action, using the form as a PHP array.
Listing 4-20 - Deleting a Field using the Form as a PHP Array
class ArticleForm extends BaseArticleForm { public function configure() { unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
To sum up, Listing 4-21 and Listing 4-22 show the ArticleForm and AuthorForm forms as we customize them.
Listing 4-21 - ArticleForm Form
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' => ArticleTable::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(ArticleTable::getStatuses()))); $this->validatorSchema['author_id']->setOption('query', $authorQuery); unset($this['created_at'], $this['updated_at'], $this['published_at']); } }
Listing 4-22 - AuthorForm Form
class AuthorForm extends BaseAuthorForm { public function configure() { $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); } }
Using the doctrine:build-forms allows to automatically generate most of the elements letting forms introspect the object model. This automatization is helpful for several reasons:
It makes the developer's life easier, saving him from a repetitive and redundant work. He can then focus on the validators and widget Customization according to the project's specific business rules .
Besides, when the database schema is updated, the generated forms will be automatically updated. The developer will just have to tune the customization they made.
The next section will describe the customization of actions and templates generated by the doctrine:generate-crud task.
The previous section show us how to customize forms generated by the task doctrine:build-forms. In the current section, we will customize the life cycle of forms, starting from the code generated by the doctrine:generate-crud task.
A Doctrine form instance is always connected to a Doctrine object. The linked Doctrine object always belongs to the class returned by the getModelName() method. For instance, the AuthorForm form can only be linked to objects belonging to the Author class. This object is either an empty object (a blank instance of the Author class), or the object sent to the constructor as first argument. Whereas the constructor of an "average" form takes an array of values as first argument, the constructor of a Doctrine form takes a Doctrine object. This object is used to define each form field default value. The getObject() method returns the object related to the current instance and the isNew() method allows to know if the object was sent via the constructor:
// creating a new object $authorForm = new AuthorForm(); print $authorForm->getObject()->getId(); // outputs null print $authorForm->isNew(); // outputs true // modifying an existing object $author = Doctrine::getTable('Author')->find(1); $authorForm = new AuthorForm($author); print $authorForm->getObject()->getId(); // outputs 1 print $authorForm->isNew(); // outputs false
As we observed at the beginning of the chapter, the edit action, shown in Listing 4-23, handles the form life cycle.
Listing 4-23 - The executeEdit Method of the author Module
// 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()); } } } }
Even if the edit action looks like the actions we might have describe in the previous chapters, we can point a few differences:
A Doctrine object from the Author class is sent as first argument to the form constructor:
$author = Doctrine::getTable('Author')->find($request->getParameter('id')); $this->form = new AuthorForm($author);
The widgets name attribute format is automatically customized to allow the retrieval of the input data in a PHP array named after the related table (author):
$this->form->bind($request->getParameter('author'));
When the form is valid, a mere call to the save() method creates or updates the Doctrine object related to the form:
$author = $this->form->save();
Listing 4-23 code handles with a single method the creation and modification of objects from the Author class:
Creation of a new Author object:
The index action is called with no id parameter ($request->getParameter('id') is null)
The call to the find() therefore sends null
The form object is then linked to an empty Author Doctrine object
The $this->form->save() call creates consequently a new Author object when a valid form is submitted
Modification of an existing Author object:
The index action is called with an id parameter ($request->getParameter('id') standing for the primary key the Author object is to modify)
The call to the find() method returns the Author object related to the primary key
The form object is therefore linked to the previously found object
The $this->form->save() call updates the Author object when a valid form is submitted
save() methodWhen a Doctrine form is valid, the save() method updates the related object and stores it in the database. This method actually stores not only the main object but also the potentially related objects. For instance, the ArticleForm form updates the tags connected to an article. The relation between the article table and the tag table being a n-n relation, the tags related to an article are saved in the article_tag table (using the saveArticleTagList() generated method).
In order to certify a consistent serialization, the save() method includes every updates in one transaction.
We will see in Chapter 9 that the
save()method also automatically updates the internationalized tables.
Using the
bindAndSave()methodThe
bindAndSave()method binds the input data the user submitted to the form, validates this form and updates the related object in the database, all in one operation:class articleActions extends sfActions { public function executeCreate(sfWebRequest $request) { $this->form = new ArticleForm(); if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('article'))) { $this->redirect('article/created'); } } }
The save() method automatically updates the Doctrine objects but can not handle the side elements as managing the file upload.
Let's see how to attach a file to each article. Files are stored in the web/uploads directory and a reference to the file path is kept in the file field of the article table, as shown in Listing 4-24.
Listing 4-24 - Schema for the article Table with associated File
// config/schema.yml
doctrine:
article:
// ...
file: string(255)
After every schema update, you need to update the object model, the database and the related forms:
$ ./symfony doctrine:build-all
Do mind that the
doctrine:build-alltask deletes every schema tables to re-create them. The data inside the tables are therefore overwritten. That is why it is important to create test data (fixtures) you can download again at each model modification.
Listing 4-25 shows how to modify the ArticleForm class in order to link a widget and a validator to the file field.
Listing 4-25 - Modifying the file Field of the ArticleForm form.
class ArticleForm extends BaseArticleForm { public function configure() { // ... $this->widgetSchema['file'] = new sfWidgetFormInputFile(); $this->validatorSchema['file'] = new sfValidatorFile(); } }
As for every form allowing to upload a file, does not forget to add also the enctype attribute to the form tag of the template (see Chapter 2 for further informations concerning file upload management).
Listing 4-26 shows the modifications to apply when saving the form to upload the file onto the server and store its path in the article object.
Listing 4-26 - Saving the article Object and the File uploaded in the 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()); } } }
Saving the uploaded file on the filesystem allows the sfValidatedFile object to know the absolute path to the file. During the call to the save() method, the fields values are used to update the related object and, as for the file field, the sfValidatedFile object is converted in a character string thanks to the __toString() method, sending back the absolute path to the file. The file column of the article table will store this absolute path.
If you wish to store the path relative to the
sfConfig::get('sf_upload_dir')directory, you can create a class inheriting fromsfValidatedFileand use thevalidated_file_classoption to send to thesfValidatorFilevalidator the name of the new class. The validator will then return an instance of your class. We will see in the rest of this chapter another approach, consisting in modifying the value of thefilecolumn before saving the object in database.
save() methodWe observed in the previous section how to save the uploaded file in the edit action. One of the principles of the object oriented programming is the reusability of the code, thanks to its encapsulation in classes. Instead of duplicating the code used to save the file in each action using the ArticleForm form, it is better to move it in the ArticleForm class. Listing 4-27 shows how to override the save() method in order to also save the file and possibly to delete of an existing file.
Listing 4-27 - Overriding the save() Method of the ArticleForm Class
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); } }
After moving the code to the form, the edit action is identical to the code initially generated by the doctrine:generate-crud task.
Refactoring the Code in the Model of in the Form
The actions generated by the
doctrine:generate-crudtask shouldn't usually be modified.The logic you could add in the
editaction, especially during the form serialization, must usually be moved in the model classes or in the form class.We just went over an example of refactoring in the form class in order to consider a uploaded file storing. Let's take another example related to the model. The
ArticleFormform has aslugfield. We observed that this field should be automatically computed from thetitlefield name that it should be potentially overridden by the user. This logic does not depend on the form. It belongs therefore to the model, as shown the following code:class Article extends BaseArticle { public function save($con = null) { if (!$this->getSlug()) { $this->setSlugFromTitle(); } return parent::save($con); } protected function setSlugFromTitle() { // ... } }The main goal of those refactorings is to respect the separation in applicative layers, and especially the reusability of the developments.
doSave() methodWe observed that the saving of an object was made within a transaction in order to guarantee that each operation related to the saving is processed correctly. When overriding the save()method as we did in the previous section in order to save the uploaded file, the executed code is independent from this transaction.
Listing 4-28 shows how to use the doSave() method to insert in the global transaction our code saving the uploaded file.
Listing 4-28 - Overriding the doSave() Method in the ArticleForm Form
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); } }
The doSave() method being called in the transaction created by the save() method, if the call to the save() method of the file() object throws an exception, the object will not be saved.
updateObject() MethodIt is sometimes necessary to modify the object connected to the form between the update and the saving in database.
In our file upload example, instead of storing the absolute path to the uploaded file in the file column, we wish to store the path relative to the sfConfig::get('sf_upload_dir') directory.
Listing 4-29 shows how to override the updateObject() method of the ArticleForm form in order to change the value of the file column after the automatic update object but before it is saved.
Listing 4-29 - Overriding the updateObject() Method and the ArticleForm Class
class ArticleForm extends BaseFormDoctrine { // ... public function updateObject($values = null) { $object = parent::updateObject($values); $object->setFile(str_replace(sfConfig::get('sf_upload_dir').'/', '', $object->getFile())); return $object; } }
The updateObject() method is called by the doSave() method before saving the object in database.
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the official user mailing-list.