Using embedFormForEach in Symfony
December 1st, 2008 by Carlos BarrosThe new Form sub-framework, introduced in Symfony 1.1, is really great, and the more I work with it, the more I like it. One of the nicest feature of form sub-framework is the ability to embed a form inside another one, using the embedForm method. Also, symfony provides another method, called embedFormForEach that will embed a form inside another one N times. At first glance this method doesn’t seem to be very useful, but I found it really useful in a project I worked a few weeks ago. Consider the following scenario: you want a form to ask users to input information about all cars they have (make and model). In this form you have a select box (this can be an input box too, but I’m using a select box in this example) where users specify how many cars they have and then it will show X fields where users can input cars model and make (X = number of cars user selected), using ajax to load fields. You can see an example here.
For this example, we will need two forms: one containing the number of cars, and another one containing car details (that will be embedded into first one).
lib/CarsForm.class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class CarsForm extends sfForm { public function __construct($defaults = array(), $options = array(), $CSRFSecret = null) { parent::__construct($defaults,$options,$CSRFSecret); $this->embedFormForEach('cars',new CarForm(),in_array($defaults['cars_count'],array(1,2,3,4,5))?$defaults['cars_count']:1); } public function configure() { $this->setWidgets(array( 'cars_count' => new sfWidgetFormSelect(array('choices'=>array(1=>1,2=>2,3=>3,4=>4,5=>5))), )); $this->setValidators(array( 'cars_count' => new sfValidatorChoice(array('choices'=>array(1,2,3,4,5))), )); $this->widgetSchema->setLabels(array( 'cars_count' => 'Number of Cars', )); $this->widgetSchema->setNameFormat('cars[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); } } |
This is the main form, where we ask for number of cars and where we will embed details form. All the trick is done in the form constructor, specifically in line 7. embedFormForEach expect three parameters:
- name
- form to embed
- number of times to embed
Our form expect to receive the number of cars in $defaults array, that stores default values of fields in the form, but before assigning it, we check if the number is within our list of allowed value. Note that list of values is hardcoded into the form. I just did it for the sake of simplicity, but I recommend creating a static method somewhere that will return these values. Ok, if our test succeed, we use received value and create requested number of CarForm form, otherwise we just create one (default).
lib/CarForm.class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class CarForm extends sfForm { public function configure() { $this->setWidgets(array( 'make' => new sfWidgetFormSelect(array('choices'=>array('ferrari'=>'Ferrari','bmw'=>'BMW','porsche'=>'Porsche'))), 'model' => new sfWidgetFormInput(), )); $this->setValidators(array( 'make' => new sfValidatorChoice(array('choices'=>array('ferrari','bmw','porsche'))), 'model' => new sfValidatorString(array('required'=>true),array('required'=>'Please enter model')), )); $this->widgetSchema->setLabels(array( 'make' => 'Make:', 'model' => 'Model:', )); $this->widgetSchema->setNameFormat('%s'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); } } |
This is the form with car details, called CarForm. This is a simple for, with nothing special, but there’s just one detail. At line 20, we set NameFormat to “%s”. Again, allowed values are hardcoded in the form, this should be avoided in real applications.
Ok, now that we already have the forms, we need to create the action and templates to handle the forms.
apps/frontend/modules/cars/actions/actions.class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** * Executes index action * * @param sfRequest $request A request object */ public function executeIndex($request) { // prepare form if($request->isMethod('post')) { $this->form = new CarsForm($request->getParameter('cars')); $this->form->bind($request->getParameter('cars')); if($this->form->isValid()) { $this->cars = $this->form->getValue("cars"); return $this->setTemplate('result'); } } else $this->form = new CarsForm(array('cars_count'=>1)); } |
This looks like a common action that handles form submission, but with one little change: we have to send default values to form constructor. If we are handling a GET request, we just set it to 1 (line 19), as we want just 1 form to be displayed at the beginning, but if we are handling a POST, we send entire form values to constructor (line 11), as our constructor will need to know beforehand how many cars are being submitted.
In the template, we will add a small piece of JavaScript to update the form, using AJAX, based on the number of cars the user selected:
apps/frontend/modules/cars/templates/indexSuccess.php
<form name="cars_frm" action="<?=url_for('cars/index') ?>" method="post"> <table> <?=$form["cars_count"]->renderRow(array('onchange'=>"new Ajax.Updater("cars_table", '".url_for('cars/refresh')."', { parameters: { cars_count: $F('cars_cars_count') } } )")); ?> <tr> <td> <table id="cars_table"> <?php include_partial('car_form',array('form'=>$form)) ?> </table> </td> </tr> </table> <p><input type="submit" value="Submit" /></p> </form>
apps/frontend/modules/cars/templates/_car_form.php:
<?=$form["cars"]->renderRow() ?>
In template, I added an listener to the onchange event of number of cars select box that will send a request to the refresh action, sending selected number of cars as parameter, and finally update cars_table container (all using prototype library). refresh action will receive the number of cars, instantiate the form, sending the number to the constructor, and then render _car_form partial.
apps/frontend/modules/actions/actions.class.php:
/** * Update cars form * * @param sfWebRequest $request Web request object */ public function executeRefresh($request) { $this->form = new CarsForm(array('cars_count'=>$request->getParameter('cars_count'))); return $this->renderPartial('car_form'); }
That’s it, the form is complete. After you validate it, getField(‘cars’) will return an associative array containing cars details submitted by the user. In this example I just write it back to the user, but you can do anything you want with this data, like serializing and then saving into DB.
apps/frontend/moduled/cars/templates/resultSuccess.php
<p>Selected cars:</p> <?php foreach($cars as $car): ?> <p><b>Make:</b> <?=$car["make"] ?><br/><b>Model:</b> <?=$car["model"] ?></p> <?php endforeach; ?>
Well, this example is a very simple one, but I think it’s a good start to understand how embedFormForEach can be used in your Symfony applications. Say, what if you want to remove the number of cars select box and rather have controls to dynamically add, delete and even sort car details?? Yes, that’s possible too, but I’ll leave this for a next article, that I should write as soon as I have some free time, for now I’ll leave this idea as an challenge




rpsblog.com » A week of symfony #101 (1->7 december 2008) in December 7th, 2008 at 22:07-
[...] Using embedFormForEach in Symfony [...]
Junho
in
December 20th, 2008 at 21:15
-
Great article! I subscribed to your blog feed.
I’m very curious to see the next one about dynamically updating the form. I failed to search that example on the web so far.
On your next article, would you show how to bind CarForm for each form added dynamically, if it’s possible?
Carlos Barros
in
December 22nd, 2008 at 9:48
-
Hi Junho…
Yes, next article is on it’s way and I’ll try to show how to bind CarForm dynamically.. I just need a little bit of time to finish it up!!
Carlos
blog.barros.ws » Using embedFormForEach in Symfony, Part II in January 1st, 2009 at 18:58-
[...] my last post I talked about embedFormForEach method, that will embed a form inside another N times, and in the [...]
stu
in
January 31st, 2009 at 17:59
-
Great example. How do I modified the formatter so that instead of Cars (0) it says Car 1? Also, how do I get rid of the "Cars" on the side?
Carlos Barros
in
February 4th, 2009 at 20:36
-
Hi stu….
I’m not sure how to do it using a new formatter (even though I’m 99% sure it’s possible).. One way I found to do it was by customizing the the _car_form.php template to look like this:
Hope this helps
Carlos
Nei Rauni Santos
in
February 12th, 2009 at 8:39
-
Olá, seu post foi muito útil para mim, estava com um problema da seguinte forma:
tinha 2 forms:
-> formulário de reserva de hotel
-> formulário de quarto
dependendo da escolha do cliente carregava o form de reserva com mais + x RoomForm.
Até neste ponto deu tudo certo, agora vem a questão:
no meu form principal BookForm eu já possuo a lista dos quartos que eu quero carregar, com id e nome, eu precisava inserir os forms de quarto já com alguns valores preenchidos no formulario de reserva. como fazer isso?
olhando a classe eu não vi essa opção.
tem alguma sugestão???
meu code pode ser acessado http://pastebin.com/f701d10f
Aguardo idéias…
Nei Rauni Santos
in
February 13th, 2009 at 8:09
-
I found the solution of my problem here:
[1] http://trac.symfony-project.org/ticket/4906
Compartilhando a solução com vcs, ficou assim:
No método construtor do form principal:
# adiciona os formulários de quarto no formulário de reserva
if( count( $book['rooms'] ) ){
$form = new sfForm();
$i = 0;
foreach( $book['rooms'] as $room ){
$form->embedForm($i, new BookRoomForm( $room ));
$i++;
}
$this->embedForm(‘rooms’, $form);
}
instancia o form sfForm e adiciona dentro dele cada quarto.
assim o html gerado fica assim:
Arrigo
in
February 25th, 2010 at 4:48
-
Also, how fast is this service comparable to Twitterfeed? I currently use Twitterfeed and it is pretty fast in publishing to Twitter.
Stephanie
in
March 7th, 2010 at 18:20
-
Funny, your webpage is generated beautifuly on my new iPhone, didnt look quite right on my old peice of junk.