Using embedFormForEach in Symfony

December 1st, 2008 by Carlos Barros

The 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 :)

Share this post...
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • Ma.gnolia
  • MySpace
  • Rec6
  • Reddit
  • StumbleUpon
  • Technorati

Comments

      JunhoNo Gravatar in
    • 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 BarrosNo Gravatar in
    • 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

      stuNo Gravatar in
    • 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 BarrosNo Gravatar in
    • 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:

      1
      2
      3
      4
      5
      6
      
      <?php foreach($form["cars"] as $k=>$wg): ?>
                      <tr>
                              <th>Cars <?php echo $k+1 ?></th>
                              <td><?php echo $wg; ?></td>
                      </tr>
      <?php endforeach; ?>

      Hope this helps

      Carlos

      Nei Rauni SantosNo Gravatar in
    • 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 SantosNo Gravatar in
    • 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:

      ArrigoNo Gravatar in
    • Also, how fast is this service comparable to Twitterfeed? I currently use Twitterfeed and it is pretty fast in publishing to Twitter.

      StephanieNo Gravatar in
    • Funny, your webpage is generated beautifuly on my new iPhone, didnt look quite right on my old peice of junk.

Leave a Reply

Powered by WP Hashcash