Here’s an other tutorial about the Symfony form framework. Last time, we managed to make embedded i18n forms optionals. In today’s howto, we will talk about embedded forms again.
In the last tutorials, we created a form to edit news. Today we will create another form to write interviews. An interview is just an article about a celebrity. So our Interview class will inherit from our Article one, so we can reuse our last time work.
Here’s the complete schema we will work with :
Article:
actAs:
Timestampable: ~
I18n:
fields: [ title, body ]
actAs:
Sluggable: { fields: [ title ], uniqueBy: [ lang, title ] }
columns:
title: { type: string(255), notnull: true }
body: { type: clob, notnull: true }
author: { type: string(255), notnull: false }
News:
inheritance:
extends: Article
type: concrete
Celebrity:
actAs:
Sluggable:
fields: [ firstname, lastname ]
columns:
firstname: { type: string(255), notnull: true }
lastname: { type: string(255), notnull: true }
Interview:
inheritance:
extends: Article
type: concrete
columns:
celebrity_id: { type: integer, notnull: true }
relations:
Celebrity:
local: celebrity_id
foreign: id
type: one
foreignType: one
foreignAlias: InterviewsAnd the fixtures :
Interview:
i1:
Author: 'Thibault J.'
Celebrity:
firstname: Thibault
lastname: Jouannic
Translation:
fr:
title: Auto-interview
body: |
Bla bla bla question et réponsesAs usual…
php symfony build --all --and-load php symfony doctrine:generate-admin backend Interview
Before we start
Before we start, let’s stop and think a few seconds. How do we want to manage our celebrity relation into our form ? Take a look at the default form, without any modification :
Quite bad, isn’t it ? We could make a little better by adding a toString method.
// lib/model/doctrine/Celebrity.class.php class Celebrity extends BaseCelebrity { public function __toString() { return sprintf('%s %s', $this->getFirstname(), $this->getLastname()); } }
That’s a bit better. The problem with this form is that you cannot add a new celebrity. If you want to create a new interview, you’ll have to chose among the existing celebrities. Of course, you could create an other admin module to manage celebrities, but a good programmer is a lazy one. And that would be great if we could create a new celebrity directly in the interview form.
In fact, that’s quite easy, using the embedRelation function.
// lib/form/doctrine/InterviewForm.class.php class InterviewForm extends BaseInterviewForm { /** * @see ArticleForm */ public function configure() { parent::configure(); unset($this['celebrity_id']); $this->embedRelation('Celebrity'); } }
Here’s the result, which is quite relevant to what we wanted. But !? Wait ! There is a regression. What if we want to choose among existing celebrities, the way we used to do ? What we really need is a way to combine both methods actually. We need to be able to choose an existing celebrity OR to create a new one.
Pick or create
We will have to use an intermediate form.
// lib/form/doctrine/InterviewForm.class.php class InterviewForm extends BaseInterviewForm { /** * @see ArticleForm */ public function configure() { parent::configure(); unset($this['celebrity_id']); $celebrityId = $this->getObject()->isNew() ? NULL : $this->getObject()->getCelebrity()->getId(); $form = new InterviewCelebrityForm(array(), array('celebrity_id' => $celebrityId)); $this->embedForm('Celebrity', $form); } } // lib/form/doctrine/InterviewCelebrityForm.class.php class InterviewCelebrityForm extends sfForm { public function configure() { $this->widgetSchema['celebrity_id'] = new sfWidgetFormDoctrineChoice(array( 'model' => 'Celebrity', 'add_empty' => true )); $this->setDefault('celebrity_id', $this->getOption('celebrity_id')); $form = new CelebrityForm(); $this->embedForm('new_celebrity', $form); $this->widgetSchema->setLabel('celebrity_id', 'Choose one…'); $this->widgetSchema->setLabel('new_celebrity', 'or create a new.'); } }
Try to edit the existing interview, and look at the nice result. However, if you try to save the form, you’ll have a lot of validations errors. There is still some work to do before we can get rid of all those red alerts.
Form validation
The form validation strategy is quite simple. If an existing celebrity is chosen with the drop down list, the firstname and lastname should be ignored. If the list is empty, we must validate the firstname and lastname. At last, we must throw an error if all the fields are empty.
// lib/form/doctrine/InterviewCelebrityForm.class.php // add this at the end of the configure() function $this->validatorSchema['celebrity_id'] = new sfValidatorPass(); $celebrityValidatorSchema = clone $this->validatorSchema['new_celebrity']; $this->validatorSchema['new_celebrity'] = new sfValidatorPass(); $this->validatorSchema->setPostValidator( new sfValidatorAnd(array( new sfValidatorOr(array( new sfValidatorSchemaFilter('celebrity_id', new sfValidatorDoctrineChoice(array( 'model' => 'Celebrity', 'required' => true ))), new sfValidatorSchemaFilter('new_celebrity', $celebrityValidatorSchema) )), new sfValidatorCallback(array( 'callback' => array($this, 'checkCelebrity') )) )) ); // add this function into the class /** * Either who choose an existing celebrity, either we create a new one * * Unset values according to the choice we made **/ public function checkCelebrity($validator, $values, $argument) { if(!empty($values['celebrity_id'])) { unset( $this['new_celebrity'], $values['new_celebrity'] ); } else { unset( $this['celebrity_id'], $values['celebrity_id'] ); } return $values; }
Try differents combinations to submit your form. The validation should behave correctly now.
Saving your form
You may have noticed that our interview isn’t updated correctly after we submit the form. We will have to override the doUpdateObject method in order to update the object correctly on form validation.
// lib/form/doctrine/InterviewForm.class.php public function doUpdateObject($values) { if(!empty($values['Celebrity']['celebrity_id'])) { $this->getObject()->setCelebrityId($values['Celebrity']['celebrity_id']); } else { $celebrity = new Celebrity(); $celebrity->fromArray($values['Celebrity']['new_celebrity']); $this->getObject()->Celebrity = $celebrity; } unset( $values['Celebrity'], $this['Celebrity'] ); parent::doUpdateObject($values); }
That’s it ! Everything should be alright now. But just to make sure, here’s a few tests.
Test your forms
We are reusing some code from the last day, so I just will output the new part here.
// test/function/backend/interviewActions.class.php <?php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new sfBackendTestFunctional(new sfBrowser()); $browser->loadData(); $browser->setTester('doctrine', 'sfTesterDoctrine'); $interview = Doctrine::getTable('Interview')-> createQuery('i')-> leftJoin('i.Translation t')-> andWhere('t.lang = ?', 'fr')-> orderBy('i.id', 'asc')-> fetchOne(); $celebrity = new Celebrity(); $celebrity->setFirstname('toto'); $celebrity->setLastname('tutu'); $celebrity->save(); $editUrl = sprintf('/interview/%s/edit', $interview->getId()); $browser-> get('/interview')-> with('response')->begin()-> isStatusCode(200)-> end()-> info('1 - Interview list')-> with('response')->begin()-> checkElement('h1', '/Interview List/')-> checkElement('body', '/Thibault Jouannic/')-> end()-> // … info(' 2.8 - The current celebrity is already selected')-> get($editUrl)-> with('response')->begin()-> checkElement('#interview_Celebrity_celebrity_id option[selected="selected"]', sprintf('/%s/', $interview->getCelebrity()))-> end()-> info(' 2.9 - The current celebrity can be updated')-> get($editUrl)-> click('Save', array('interview' => array( 'Celebrity' => array( 'celebrity_id' => $celebrity->getId() ) )), array('_with_csrf' => true) )-> with('form')->begin()-> hasErrors(0)-> end()-> with('doctrine')->begin()-> check('Interview', array( 'id' => $interview->getId(), 'celebrity_id' => $celebrity->getId() ))-> info(' 2.10 - Updating the celebrity does not create empty records')-> check('Celebrity', array(), 4)-> end()-> info(' 2.11 - A new celebrity can be created')-> get($editUrl)-> click('Save', array('interview' => array( 'Celebrity' => array( 'celebrity_id' => '', 'NewCelebrity' => array( 'firstname' => 'oncle', 'lastname' => 'picsou' ) ) )), array('_with_csrf' => true) )-> with('doctrine')->begin()-> check('Celebrity', array(), 5)-> check('Interview', array( 'id' => $interview->getId(), 'celebrity_id' => $celebrity->getId() + 1 ))-> end() ;
That’s all folks ! See you soon.





7 Commentaires
Sweet, we even get functionals as a bonus
Excellent ! Thanks
i have a some exemple
but for me i would like to create a user with his messages
i explain :
user
i have a some exemple
but for me i would like to create a user with his messages
i explain :
user
id
name
message
id
i would like to create a user il the same time of his messages
user_id
Hi,
thank you for this tutorial, it’s very good.
My Celebrity table has one more field : photo. As compared to your example I modified CelebrityForm.class.php only by adding appropriate sfWidgetFileInput() and sfValidatorFile(). Then if I add a new celebrity a new object is created and all data are correct except that the the file and file name are not saved. It seems to me that it is some problem with passing the values in doUpdateObject().
Could you please advise me how to proceed in this case ?
Regards,
Goska
Merci pour ce petit tuto !
Mais j’ai beau essayer impossible d’imbriquer plus que deux formulaires.
Par exemple pour 3 formulaires Doctrine_Connection_Mysql_Exception me bloque au second en disant qu’il ne peut pas continuer car il manque l’id du 3ème formulaire (celui qui est en tout dernier).
Si quelqu’un a une solution ça m’aiderait beaucoup ^^ . Merci d’avance.
Thank you for this how to.
But I tried to overlap 3 forms and I fail. Doctrine_Connection_Mysql_Exception signal that he don’t have the id of the last form so the form’s save bug…
If anyone have a solution I’ll be happy to have it. Thank’s.
Bravo pour ce tutoriel absolument capital et essentiel, qui correspond tout à fait aux besoins réels du dev Symfony. Merci pour le InterviewCelebrityForm, auquel je n’aurais pas pensé tout seul. Bravo, bravo, bravo.
One Trackback
[...] create new if the option is not available. After googling on the internet, I found an interesting article how to overcome these problem. Inspired by Propel 1.5 features ( embedRelation and mergeRelation), [...]