Posts Tagged ‘symfony’

rto: sfYaml complies for 43% with Yaml 1.2 spec

Monday, February 8th, 2010

For a recent project I needed to write a small domain specific language. I decided to use a language based on YML, as the DSL was also array based.

While reading the spec of of Yaml 1.2, I was surprised some things don’t work in the current sfYaml implementation. The most important one to me is that the short syntax for lists of mappings is not supported (yet), see also example 2.12:

- item    : Super Hoop
  quantity: 1
- item    : Basketball
  quantity: 4
- item    : Big Shoes
  quantity: 1

Below is a concluding table of my findings. Currently only 12 out of 28 examples are parsed by sfYaml. Some errors are easy to fix (the ‘—‘ document separator for instance). Hopefully together we can make the sfYaml 100% compliant to spec 1.2!

NB. Tests were run on a SF1.3 trunk library, link to the used script

2.1. Sequence of Scalars
- Mark McGwire
- Sammy Sosa
- Ken Griffey
sfYaml
spec
1.2
Array
(
    [0] => Mark McGwire
    [1] => Sammy Sosa
    [2] => Ken Griffey
)
2.2. Mapping Scalars to Scalars
hr:  65    # Home runs
avg: 0.278 # Batting average
rbi: 147   # Runs Batted In
sfYaml
spec
1.2
Array
(
    [hr] => 65
    [avg] => 0.278
    [rbi] => 147
)
2.3. Mapping Scalars to Sequences
american:
  - Boston Red Sox
  - Detroit Tigers
  - New York Yankees
national:
  - New York Mets
  - Chicago Cubs
  - Atlanta Braves
sfYaml
spec
1.2
Array
(
    [american] => Array
        (
            [0] => Boston Red Sox
            [1] => Detroit Tigers
            [2] => New York Yankees
        )

    [national] => Array
        (
            [0] => New York Mets
            [1] => Chicago Cubs
            [2] => Atlanta Braves
        )

)
2.4. Sequence of Mappings
-
  name: Mark McGwire
  hr:   65
  avg:  0.278
-
  name: Sammy Sosa
  hr:   63
  avg:  0.288
sfYaml
spec
1.2
Array
(
    [0] => Array
        (
            [name] => Mark McGwire
            [hr] => 65
            [avg] => 0.278
        )

    [1] => Array
        (
            [name] => Sammy Sosa
            [hr] => 63
            [avg] => 0.288
        )

)
2.5. Sequence of Sequences
- [name        , hr, avg  ]
- [Mark McGwire, 65, 0.278]
- [Sammy Sosa  , 63, 0.288]
sfYaml
spec
1.2
Array
(
    [0] => Array
        (
            [0] => name
            [1] => hr
            [2] => avg
        )

    [1] => Array
        (
            [0] => Mark McGwire
            [1] => 65
            [2] => 0.278
        )

    [2] => Array
        (
            [0] => Sammy Sosa
            [1] => 63
            [2] => 0.288
        )

)
2.6. Mapping of Mappings
Mark McGwire: {hr: 65, avg: 0.278}
Sammy Sosa: {
    hr: 63,
    avg: 0.288
  }
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Malformed inline YAML string {
2.7. Two Documents in a Stream
# Ranking of 1998 home runs
---
- Mark McGwire
- Sammy Sosa
- Ken Griffey

# Team ranking
---
- Chicago Cubs
- St Louis Cardinals
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 2 (—).
2.8. Play by Play Feed
---
time: 20:03:20
player: Sammy Sosa
action: strike (miss)
...
---
time: 20:03:47
player: Sammy Sosa
action: grand slam
...
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 4 (…).
2.9. Single Document with Two Comments
---
hr: # 1998 hr ranking
  - Mark McGwire
  - Sammy Sosa
rbi:
  # 1998 rbi ranking
  - Sammy Sosa
  - Ken Griffey
sfYaml
spec
1.2
Array
(
    [hr] => Array
        (
            [0] => Mark McGwire
            [1] => Sammy Sosa
        )

    [rbi] => Array
        (
            [0] => Sammy Sosa
            [1] => Ken Griffey
        )

)
2.10. Node for ‘Sammy Sosa’ appears twice in this document
---
hr:
  - Mark McGwire
  # Following node labeled SS
  - &SS Sammy Sosa
rbi:
  - *SS # Subsequent occurrence
  - Ken Griffey
sfYaml
spec
1.2
Array
(
    [hr] => Array
        (
            [0] => Mark McGwire
            [1] => Sammy Sosa
        )

    [rbi] => Array
        (
            [0] => Sammy Sosa
            [1] => Ken Griffey
        )

)
2.11. Mapping between Sequences
? - Detroit Tigers
  - Chicago cubs
:
  - 2001-07-23

? [ New York Yankees,
    Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 1 (? - Detroit Tigers).
2.12. Compact Nested Mapping
---
# Products purchased
- item    : Super Hoop
  quantity: 1
- item    : Basketball
  quantity: 4
- item    : Big Shoes
  quantity: 1
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 3 ( quantity: 1).
2.13. In literals, newlines are preserved
# ASCII Art
--- |
  \//||\/||
  // ||  ||__
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 2 (— |).
2.14. In the folded scalars, newlines become spaces
--- >
  Mark McGwire's
  year was crippled
  by a knee injury.
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 1 ( Mark McGwire’s).
2.15. Folded newlines are preserved for ‘more indented’ and blank lines
>
 Sammy Sosa completed another
 fine season with great stats.

   63 Home Runs
   0.288 Batting Average

 What a year!
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 1 (>).
2.16. Indentation determines scope
name: Mark McGwire
accomplishment: >
  Mark set a major league
  home run record in 1998.
stats: |
  65 Home Runs
  0.278 Batting Average
sfYaml
spec
1.2
Array
(
    [name] => Mark McGwire
    [accomplishment] => Mark set a major league home run record in 1998.

    [stats] => 65 Home Runs
0.278 Batting Average

)
2.17. Quoted Scalars
unicode: "Sosa did fine.\u263A"
control: "\b1998   1999    2000
"
hex esc: "
 is 
"

single: '"Howdy!" he cried.'
quoted: ' # Not a ''comment''.'
tie-fighter: '|\-*-/|'
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Malformed inline YAML string ("\b1998 1999 2000).
2.18. Multi-line Flow Scalars
plain:
  This unquoted scalar
  spans many lines.

quoted: "So does this
  quoted scalar.
"
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 2 (This unquoted scalar).
2.19. Integers
canonical: 12345
decimal: +12345
octal: 0o14
hexadecimal: 0xC
sfYaml
spec
1.2
Array
(
    [canonical] => 12345
    [decimal] => 12345
    [octal] => 0o14
    [hexadecimal] => 12
)
2.20. Floating Point
canonical: 1.23015e+3
exponential: 12.3015e+02
fixed: 1230.15
negative infinity: -.inf
not a number: .NaN
sfYaml
spec
1.2
Array
(
    [canonical] => 1230.15
    [exponential] => 1230.15
    [fixed] => 1230.15
    [negative infinity] => -INF
    [not a number] => INF
)
2.21. Miscellaneous
null:
booleans: [ true, false ]
string: '012345'
sfYaml
spec
1.2
Array
(
    [] => 
    [booleans] => Array
        (
            [0] => 1
            [1] => 
        )

    [string] => 012345
)
2.22. Timestamps
canonical: 2001-12-15T02:59:43.1Z
iso8601: 2001-12-14t21:59:43.10-05:00
spaced: 2001-12-14 21:59:43.10 -5
date: 2002-12-14
sfYaml
spec
1.2
Array
(
    [canonical] => 1008385183
    [iso8601] => 1008385183
    [spaced] => 1008385183
    [date] => 1039824000
)
2.23. Various Explicit Tags
---
not-date: !!str 2002-04-28

picture: !!binary |
 R0lGODlhDAAMAIQAAP//9/X
 17unp5WZmZgAAAOfn515eXv
 Pz7Y6OjuDg4J+fn5OTk6enp
 56enmleECcgggoBADs=

application specific tag: !something |
 The semantics of the tag
 above may be different for
 different documents.
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 4 ( R0lGODlhDAAMAIQAAP//9/X).
2.24. Global Tags
%TAG ! tag:clarkevans.com,2002:
--- !shape
  # Use the ! handle for presenting
  # tag:clarkevans.com,2002:circle
- !circle
  center: &ORIGIN {x: 73, y: 129}
  radius: 7
- !line
  start: *ORIGIN
  finish: { x: 89, y: 102 }
- !label
  start: *ORIGIN
  color: 0xFFEEBB
  text: Pretty vector drawing.
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 2 (— !shape).
2.25. Unordered Sets
# Sets are represented as a
# Mapping where each key is
# associated with a null value
--- !!set
? Mark McGwire
? Sammy Sosa
? Ken Griff
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 4 (— !!set).
2.26. Ordered Mappings
# Ordered maps are represented as
# A sequence of mappings, with
# each mapping having one key
--- !!omap
- Mark McGwire: 65
- Sammy Sosa: 63
- Ken Griffy: 58
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 4 (— !!omap).
2.27. Invoice
--- !<tag:clarkevans.com,2002:invoice>
invoice: 34843
date   : 2001-01-23
bill-to: &id001
  given  : Chris
  family : Dumars
  address:
    lines: |
      458 Walkman Dr.
      Suite #292
    city    : Royal Oak
    state   : MI
    postal  : 48046
ship-to: *id001
product:
  - sku         : BL394D
    quantity    : 4
    description : Basketball
    price       : 450.00
  - sku         : BL4438H
    quantity    : 1
    description : Super Hoop
    price       : 2392.00
tax  : 251.42
total: 4443.52
comments:
  Late afternoon is best.
  Backup contact is Nancy
  Billsmer @ 338-4338.
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 16 ( quantity : 4).
2.28. Log File
---
Time: 2001-11-23 15:01:42 -5
User: ed
Warning:
  This is an error message
  for the log file
---
Time: 2001-11-23 15:02:31 -5
User: ed
Warning:
  A slightly different error
  message.
---
Date: 2001-11-23 15:03:17 -5
User: ed
Fatal:
  Unknown variable "bar"
Stack:
  - file: TopClass.py
    line: 23
    code: |
      x = MoreObject("345
")
  - file: MoreClass.py
    line: 58
    code: |-
      foo = bar
sfYaml
spec
1.2
Error, unable to load YML: Unable to parse string: Unable to parse line 4 (This is an error message).

rto: Learn coding from the symfony core team!

Friday, December 4th, 2009

Interactive embedded forms

Tuesday, November 11th, 2008

Today I want to demonstrate how to make interactive forms with sfForm and Doctrine. The target is to create an interface which edits an object, edits related objects and allows you to add or remove related objects. See the screenshot below for the end-result:

firefoxscr007

Preparation

  • To illustrate my way of installing I added a full history of what I did. The tutorial will start in the next paragraph. * I started with a fresh 1.2beta project with something like symfony generate:project sandbox. After the project setup I ran:

1) ./symfony generate:app frontend to create the frontend app

2) Enable Doctrine: config/ProjectConfiguration.class.php

<?php
 
require_once dirname(__FILE__).'/../../lib/symfony/1.2/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();
 
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins(array('sfDoctrinePlugin'));
    $this->disablePlugins(array('sfPropelPlugin'));
  }
}

3) I installed a SQLite db: config/databases.yml

all:
  doctrine:
    class:          sfDoctrineDatabase
    param:
      dsn:          sqlite:///<?php echo dirname(__FILE__); ?>/../log/sqlite.db

4) I made the schema.yml

---
Author:
  columns:
    name: string(255)
    description: string
 
Book:
  columns:
    author_id: integer(20)
    title: string(255)
    released: integer(20)
  relations:
    Author:
      foreignAlias: Books

5) I put some sample data in /data/fixtures/fixtures.yml. There’s a link to the file at the end of this post.

6) Run some fun stuff at the command line: ./symfony doctrine:build-all-load followed by ./symfony cc and to setup the right permissions sudo ./symfony project:permissions.

7) Now I create a default doctrine module to edit the Author table: ./symfony doctrine:generate-module frontend author Author. Everything should be up and running now, visiting http://your_host/frontend_dev.php/author should give you something like:

finderscr0011

The Author form is still very basic:

firefoxscr0011

Automatic templates

First step is to remove the strict formatting in the template and enable automattic formatting. As I’m lazy I want sfForm to do the layout work for me. Open up the /apps/frontend/modules/author/templates/_form.php partial and remove some code. The following template will display sfForm using it’s own __toString() rendering system:

<?php include_stylesheets_for_form($form) ?>
<?php include_javascripts_for_form($form) ?>
 
<form action="<?php echo url_for('author/'.($form->getObject()->isNew() ? 'create' : 'update').(!$form->getObject()->isNew() ? '?id='.$form->getObject()->getid() : '')) ?>" method="POST" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>
<?php if (!$form->getObject()->isNew()): ?>
<input type="hidden" name="sf_method" value="PUT" />
<?php endif; ?>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          &nbsp;<a href="<?php echo url_for('author/index') ?>">Cancel</a>
          <?php if (!$form->getObject()->isNew()): ?>
            &nbsp;<?php echo link_to('Delete', 'author/delete?id='.$form->getObject()->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>

Don’t be impressed here; I only removed the code in the <tbody></tbody> section and removed the call to $form->renderHiddenFields(). Your form should still look exactly the same as in the screenshot supplied before.

Embed the forms

Now we can add the forms for all the books an Author has written to the form. Open /lib/form/doctrine/AuthorForm.class.php and add some intelligence:

class AuthorForm extends BaseAuthorForm
{
  public function configure()
  {
    foreach ($this->object['Books'] as $index=>$book)
    {
      $this->embedForm('book'.$index, new BookForm($book));
    }
  }
}

This works, now you can edit all books:

firefoxscr002

Adding interactivity

The form works well and saves all values, but it is not possible to remove a book or add a new one yet. To achieve this we can add some more submit buttons, for each book one. This will inform the server what the user wants to delete or insert. I also restructured the code a bit to make it easier to understand.

  public function configure()
  {
    foreach ($this->object['Books'] as $index=>$book)
    {
      $fieldName = 'book_'.$book['id'];
 
      $form = new BookForm($book);
      unset($form['author_id']);
      $this->embedForm($fieldName, $form);
 
      $label = '<input type="submit" name="submit" value="delete_'.$book['id'].'">';
      if (count($this->object['Books']) -1 == $index)
      {
        // this is the last book
        $label.= '<input type="submit" name="submit" value="insert">';
      }
      $label.= ' Book '.($index+1);
 
      $this->widgetSchema->setLabel($fieldName, $label);
    }
  }

firefoxscr005

I admit, it’s quite ugly on the screen. But in the action we will now have a parameter submit which is set to insert when the insert-button is clicked. It’s set to delete_* when one of the delete-buttons is clicked (* is the id of the related book), or it’s not set, then the normal ‘Save’ button is clicked.

Let’s work on /apps/frontend/modules/author/actions/actions.class.php to enable the form to react to these new parameters:

  public function executeUpdate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post') || $request->isMethod('put'));
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
 
    //get the submit value from the request, and split it into an array of command, id
    $submit = ($request->hasParameter('submit') ? $request->getParameter('submit') : '_');
    $submit = explode('_',$submit);
 
    switch ($submit[0])
    {
      case 'insert':
        $author['Books'][] = new Book();
        break;
      case 'delete':
        $this->forward404Unless($book = Doctrine::getTable('Book')->find($submit[1]));
        $book->delete();
        break;
    }
    $this->form = new AuthorForm($author);
 
    $this->processForm($request, $this->form);
 
    $this->setTemplate('edit');
  }

This code works! After pressing one time on the insert button we can see that a new book is added to the form. Thanks to Doctrine the newly created book know’s it’s written by the Author. This is because the record defaults are automatically used as form defaults. According to the image below the only thing we need to do is some styling:

firefoxscr006

Styling the submit buttons

Luckily styling will be very easy. The old man at the wc3 will help a little, as they have once defined an <input type='image'>. This input looks like an image, but clicking it will submit the form. Let’s update the AuthorForm.class.php again:

  public function configure()
  {
    foreach ($this->object['Books'] as $index=>$book)
    {
      $fieldName = 'book_'.$book['id'];
 
      $form = new BookForm($book);
      unset($form['author_id']);
      $this->embedForm($fieldName, $form);
 
      $label = '<input type="image" src="/images/delete.gif" name="submit" value="delete_'.$book['id'].'">';
      if (count($this->object['Books']) -1 == $index)
      {
        // this is the last book
        $label.= '<input type="image" src="/images/add.gif" name="submit" value="insert">';
      }
      else
      {
        $label.= '<img src="/images/empty.gif">';
      }
      $label.= ' Book '.($index+1);
 
      $this->widgetSchema->setLabel($fieldName, $label);
    }
  }

Now our form looks and works the way we want:

firefoxscr007

Some more things to consider

Deleting a book will give you an Unexpected extra form field named "book2". error. This can be undone if you unset this data from the request. It needs some more work as the default generated processForm function has to be changed:

  protected function processForm($data, sfForm $form)
  {
    $form->bind($data);
    if ($form->isValid())
    {
      $author = $form->save();
 
      $this->redirect('author/edit?id='.$author['id']);
    }
  }
 
  public function executeUpdate(sfWebRequest $request)
  {
    $this->forward404Unless($request->isMethod('post') || $request->isMethod('put'));
    $this->forward404Unless($author = Doctrine::getTable('Author')->find($request->getParameter('id')), sprintf('Object author does not exist (%s).', $request->getParameter('id')));
    $data = $request->getParameter('author');
 
    //get the submit value from the request, and split it into an array of command, id
    $submit = ($request->hasParameter('submit') ? $request->getParameter('submit') : '_');
    $submit = explode('_',$submit);
 
    switch ($submit[0])
    {
      case 'insert':
        $author['Books'][] = new Book();
        break;
      case 'delete':
        $this->forward404Unless($book = Doctrine::getTable('Book')->find($submit[1]));
        unset($data['book'.$book['id']]);
        $book->delete();
        break;
    }
    $this->form = new AuthorForm($author);
 
    $this->processForm($data, $this->form);
 
    $this->setTemplate('edit');
  }

Also make sure to always check the $request->getParameter('submit') for validity. It’s not secured by the sfForm security system so you have to use it cautiously.

That’s it for today, thanks for reading!

The fixtures.yml file used

Full source of this project (Make sure you change /config/ProjectConfiguration.class.php to point to the right location of the Symfony 1.2 Library)

Nesting Doctrine’s relations in sfForm

Wednesday, November 5th, 2008

I regularly need a form to edit more than one object at once. To explain this more clearly: imagine an Author - Book relation. Doctrine builds you standard forms for the Author and Book objects, but you can only use them separate. Why not combine the forms using code like in the following lines?

$form = new AuthorForm($author);
$form->embedRelation('Books');

Such code extends functionality of your forms with the knowledge already defined in the Doctrine-schema of your project. It will reuse your already available forms into a really smart editor. Very DRY to my opinion…

summing-forms

This article explains how to extend the sfDoctrineForm and make the above code work.


Example schema and definition

Let’s take the typical Author-Book relation as an example. An author has written books, each book has an author. This gives the following schema & fixtures: (config/doctrine/schema.yml, data/fixture/data.yml)

---
Author:
  columns:
    name: string(255)
    description: string
 
Book:
  columns:
    author_id: integer(20)
    title: string(255)
    released: integer(20)
  relations:
    Author:
      foreignAlias: Author
data/fixtures/data.yml:
Author:
  kluun:
    name: Kluun
    description: Author living in Amsterdam
 
Book:
  komt:
    title: Komt een vrouw bij de dokter
    released: 2003
    Author: kluun
  weduw:
    title: De weduwnaar
    released: 2006
    Author: kluun

After doing doctrine:build-all-load for setting up the model&forms we can create a CRUD by executing something like doctrine:generate-module frontend author Author. This gives a familiar CRUD interface:

basic crud list

Steps

So far so good. The steps we need to do to get the right result are:

  1. add a BookForm form for each $author['Books'] object

  2. when data comes back, save the data, also for the nested objects.

Array structure

We have to start with the end in mind. After submitting and validating a form, the returned data should match the form Doctrine uses. So we start with step #2. Each Doctrine record object has a method ->fromArray(boolean $deep), which populates the object. When $deep is true it also populates nested records, this is the functionality we need! If we can create a sfForm which returns data in the exact array structure needed by fromArray, then we can directly populate the record object with the $form->getValues(). (Actually this is the way sfDoctrineForm populates records, but $deep is set to false so it only populates the current object)

Now we need to know which structure this array has. Luckily Doctrine supplies the ‘inverse’ method of fromArray, named ->toArray(boolean $deep). This method gives us the array needed to populate the current object. A sample output of $author->toArray() is below:

Array
(
    [id] => 3
    [name] => Kluun
    [description] => Author living in Amsterdam
    [Books] => Array
        (
            [0] => Array
                (
                    [id] => 5
                    [author_id] => 3
                    [title] => Komt een vrouw bij de dokter
                    [released] => 2003
                )
            [1] => Array
                (
                    [id] => 6
                    [author_id] => 3
                    [title] => De weduwnaar
                    [released] => 2006
                )
        )
)

Let’s give it a first try:

$author = Doctrine::getTable('Author')->find('3');
$book1 = Doctrine::getTable('Book')->find('5');
$book2 = Doctrine::getTable('Book')->find('6');
 
$form = new AuthorForm($author);
$form->embedForm('book1', new BookForm($book1));
$form->embedForm('book2', new BookForm($book2));
 
echo "<table>".$form."</table>";

After doing a $form->bind($request->getParameter('author')); the form returns values: (`$form->getValues()’)

Array
(
    [id] => 3
    [name] => Kluun
    [description] => Author living in Amsterdam
    [book1] => Array
        (
            [id] => 5
            [author_id] => 3
            [title] => Komt een vrouw bij de dokter
            [released] => 2003
        )
    [book2] => Array
        (
            [id] => 6
            [author_id] => 3
            [title] => De weduwnaar
            [released] => 2006
        )
)

Getting close, but actually we need a temporary form for embedding all book-objects first. When all forms are added we can embed this temporaryform with the name of the relation into the form.

The following code gives the right $form->getValues() array structure:

$form = new AuthorForm($author);
 
$tempform = new sfForm();
$tempform->embedForm('0', new BookForm($book1));
$tempform->embedForm('1', new BookForm($book2));
 
$form->embedForm('Books',$tempform);
Array
(
    [id] => 3
    [name] => Kluun
    [description] => Author living in Amsterdam
    [Books] => Array
        (
            [0] => Array
                (
                    [id] => 5
                    [author_id] => 3
                    [title] => Komt een vrouw bij de dokter
                    [released] => 2003
                )
            [1] => Array
                (
                    [id] => 6
                    [author_id] => 3
                    [title] => De weduwnaar
                    [released] => 2006
                )
        )
)

Bingo, this response exactly matches the array we got from the $author->toArray(true) method!

Hooking things up

Now we have to implement this in a new function. The doctrine:build-forms command also creates a BaseFormDoctrine.class.php where we can put our code. It inherits from sfDoctrineForm and is extended by all generated forms. BaseFormDoctrine.class.php is located in the lib/form/doctrine/base directory.

The following function will do the trick:

  public function embedRelation($relationName)
  {    
    $formClass = $this->object->getTable()->getRelation($relationName)->getClass()."Form";
 
    $subForm = new sfForm();
    foreach ($this->object[$relationName] as $index => $childObject)
    {
      $form = new $formClass($childObject);
    }      
    $this->embedForm($relationName, $subForm);
  }

Saving the values

Using the above code, the form will display right, but only the values of the Author are stored. Now we need to override some default behaviour. In sfDoctrineForm the method updateDefaultsFromObject merges arrays only one level deep; all array_merge calls should be changed into sfForm::deepArrayUnion calls. The function updateObject should also be changed to enable ‘deep’ setting of values, change the last line into $this->object->fromArray($values, true);. See the attached code for my full version of BaseFormDoctrine.class.php.

Reflection

Using relations to extend your forms is a powerful feature. Real-life usage demands a bit more than written in this article:

  1. There’s a danger of recursion, so the system has to detect this.

  2. The article only spoke about one-to-many relations, but one-to-one should also be handled.

  3. One-to-many lists (in our example the dropdown in a Book form for selecting the Author) should be removed from the form, as it creates an unintuitive interface.

  4. The developer should be able to override the classname of the embedded form and should also be able to change the options of the embedded forms

All these features are implemented in the attached code. Have a look and have fun! Thanks for reading

Last note: Today the sfForm was updated and now has a a new method ->getEmbeddedForms(). This enables other approaches to the problem, I’ll update when the sfDoctrinePlugin is up to date with this new feature.

baseformdoctrineclass