I’m going to show a way that I’ve found to easily edit N1 relationships with the admin generator of symfony 1.2 Surely it can be polished, and maybe it could be done better another way, if you know how please let me know.

We are going to use the next model schema:

# config/schema.yml
propel:
  lista:
    id:      ~
    name:    varchar(255)
    type:    varchar(255)

  lista_item:
    id:      ~
    lista_id: {type: integer, foreignTable: lista, foreignReference: id, required: true }
    name:    varchar(255)

There is a table of lists, and a table of list items related with the lists, so a list can contain n list items. The problem is to achieve the edition of the list properties and related list items on the same form.

I recommend to create the project to follow all the steps. So first of all…

$ mkdir listproject
$ cd listproject
$ symfony generate:project listproject

To test with some initial data we can add some fixtures:

# data/fixtures/lists.yml
Lista:
  list_1:
    name: List of fruits
    type: fruits
  list_2:
    name: List of books
    type: books

ListaItem:
  item_1:
    lista_id: list_1
    name: apple
  item_2:
    lista_id: list_1
    name: orange
  item_3:
    lista_id: list_1
    name: pear
  item_4:
    lista_id: list_2
    name: Treasure Island
  item_5:
    lista_id: list_2
    name: Moby Dick

Now we can configure database and load with some data:

$ symfony configure:database "mysql:host=localhost;dbname=listproject" root mYsEcret
$ mysqladmin -uroot -pmYsEcret create listproject
$ symfony propel:build-all-load

The next step is to create the backend application, and generate an admin module for the Lista model:

$ symfony generate:app backend
$ symfony propel:generate-admin backend Lista

Now we can view and edit the lists, as you can see on these screenshots:

We are ready for the main subject. The first step is to add an action to the edit template, which will add items to the list.

Edit the generator.yml of the module, and replace with this:

generator:
  class: sfPropelGenerator
  param:
    model_class:           Lista
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          lista
    with_propel_route:     1

    config:
      actions: ~
      fields:  ~
      list:    ~
      filter:  ~
      form:    ~
      edit:
        actions:
          _delete:
          _list:
          add_item:
          _save:
      new:     ~

This will add a new link at the bottom of the edit template, linking to the following action: ListAddItem. So we add the action to the actions.class.php of the module:

class listaActions extends autoListaActions
{
  public function executeListAddItem()
  {
    if($this->getUser()->hasAttribute('N1added'))
    {
      $N1added = $this->getUser()->getAttribute('N1added');
      $this->getUser()->setAttribute('N1added', $N1added + 1);
    }
    else
    {
      $this->getUser()->setAttribute('N1added', 1);
    }
    $this->forward('lista', 'edit');
  }
}

Here is the trick. We add an attribute to the user (N1added) that has the count of the added items to the list. Each time the user clicks on “Add item”, N1added will be increased. This attribute will allow us to embed the needed number of forms into the ListaForm, as we can see below:

class ListaForm extends BaseListaForm
{
  public function configure()
  {
    $n = 0;
    foreach($this->object->getListaItems() as $item)
    {
      $n++;
      $this->embedForm('Item '.$n, new ListaItemForm($item));
    }

    if(sfContext::getInstance()->getUser()->hasAttribute('N1added'))
    {
      for($i=0; $i < sfContext::getInstance()->getUser()->getAttribute('N1added'); $i++)
      {
        $it = new ListaItem();
        $it->setLista($this->object);
        $n++;
        $this->embedForm('Item_'.$n, new ListaItemForm($it));
      }
    }
  }
}

Also we have to set the ‘lista_id’ field to an input hidden on ListaItemForm:

class ListaItemForm extends BaseListaItemForm
{
 public function configure()
 {
   $this->widgetSchema['lista_id'] = new sfWidgetFormInputHidden();
 }
}

So, after this steps you can view, add and save items to the list as the next screenshot shows:

Now we have to reset the N1added counter in two cases: after save the list, and returning to the list, so we add the next two methods to the actions:

public function executeIndex(sfWebRequest $request)
{
 $this->getUser()->setAttribute('N1added', 0);
 parent::executeIndex($request);
}
protected function processForm(sfWebRequest $request, sfForm $form)
{
 $this->getUser()->setAttribute('N1added', 0);
 parent::processForm($request, $form);
}

The last thing to be done is the option to delete items. We are going to modify the _form_field.php template, that you can copy from the cache folder and save in the templates folder of lista’s module. The modified template is as follows (added code is in bold):

<?php if ($field->isPartial()): ?>
  <?php include_partial('lista/'.$name, array(
     'form' => $form, 'attributes' => $attributes instanceof sfOutputEscaper ?
       $attributes->getRawValue() : $attributes)) ?>
<?php elseif ($field->isComponent()): ?>
  <?php include_component('lista', $name, array(
     'form' => $form, 'attributes' => $attributes instanceof sfOutputEscaper ?
       $attributes->getRawValue() : $attributes)) ?>
<?php else: ?>
  <div class="<?php echo $class ?><?php $form[$name]->hasError() and print ' errors' ?>">
    <?php echo $form[$name]->renderError() ?>
    <div>
      <?php echo $form[$name]->renderLabel($label) ?>

      <?php echo $form[$name]->render($attributes instanceof sfOutputEscaper ?
             $attributes->getRawValue() : $attributes) ?>
      <?php if($form[$name] instanceof sfFormFieldSchema): ?>
        <ul class="sf_admin_actions">
          <li class="sf_admin_action_delete">
          <?php $value = $form[$name]->getValue();
          echo link_to(__('Delete'), 'lista/deleteItem?itemId='.$value['id'],
            array('confirm' => 'Are you sure?'));
          ?>
          </li>
        </ul>
      <?php endif; ?>
      <?php if ($help || $help = $form[$name]->renderHelp()): ?>
        <div class="help"><?php echo __($help, array(), 'messages') ?></div>
      <?php endif; ?>
    </div>
  </div>
<?php endif; ?>

We have added a delete link for each item. And finally, add the corresponding action in actions.class.php:

public function executeDeleteItem(sfWebRequest $request)
{
  $item = ListaItemPeer::retrieveByPk($request->getParameter('itemId'));
  $id = $item->getLista()->getId();
  $item->delete();

  $this->getUser()->setFlash('notice', 'The item was deleted successfully.');

  $this->redirect('lista/edit?id='.$id);
}

That’s all. You can now edit, add and delete every item of every list.

I don’t know if this is the best solution for this problem, so if you find a better one or have any suggestions to improve this, please tell me.


11 Comments on “A way to edit N-1 relationships in one form with symfony 1.2”

You can track this conversation through its atom feed.

  1. Murena says:

    Great idea. I’m having a problem though.
    When I add an item, fill it and then save the form, I receive an error. For examble if I save 2 items a third item appears and gives error: item_id: required.

    Also the processForm written like this may not be good if there are validation errors in the embedded forms.

  2. Murena says:

    (in my previous commet I meant list_id instead of item_id)

    To be more clear. If I allow list_id in the schema to be empty, when I save a form I always end up with one more lista_item in the database, with list_id empty.
    I’m using Doctrine and Symfony 1.2

    Any idea what am I doing wrong?

  3. Junho says:

    Good idea! I didn’t think about refreshing a page for each “add item” action. It may not be as smooth using JavaScript code, but I think it’s one of the most clear way to work with symfony.

    I have a suggestion to keep edited values for every “add item” request.

    1. Call getListaItems only once from ‘edit’ action of ‘lista’ module. Not inside the ListaForm.

    2. Move “add action” link into the form as another button beside “save”. I’m not sure whether symfony supports either http://articles.techrepublic.com.com/5100-10878_11-5242116.html or http://www.codeproject.com/KB/scripting/multiaction.aspx.

    3. Add $request parameter to executeListAddItem, and call EmbedForm for each $request->getParameter(‘lista’)[Item *].

    I’ll try tomorrow, and let you know if it works.

  4. Junho says:

    I didn’t succeed to implement my idea yet, but found an interesting post about the same issue.

    http://blog.barros.ws/2009/01/01/using-embedformforeach-in-symfony-part-ii/comment-page-1

  5. enrique says:

    @Murena
    sorry, but I have no experience with Doctrine.

  6. vojto says:

    Wouldn’t it be easier to embed all ListaItems and one extra?

    I’m doing it that way (but without form framework) and works.

  7. Enrique Martínez says:

    @vojto
    yes, that is another way, but you can add only one item each time you save.

  8. arnold says:

    Hello,

    Really a nice tutorial but has anyone succeeded making it work also for the new action?

  9. Erin says:

    If you are looking for a way to implement a similar system for your own forms, not those generated by the admin generator, I have written a tutorial explaining how to do so. If you are interested check out http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ and let me know your thoughts. Any feedback would be most welcome.

  10. sallyjupiter says:

    “So we add the action to the actions.”
    You can more about this?

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>