Using embedFormForEach in Symfony, Part II
January 1st, 2009 by Carlos BarrosIn my last post I talked about embedFormForEach method, that will embed a form inside another N times, and in the end of that article, I promised a new post on this subject to show how to use it to dynamically add/remove/sort cars, and here it is. As usual, I put live an example here (I integrated this new form on the old city picker form). In this example, when you create (or edit) an user, you will see a new control called Cars, and a green plus icon. If you click that icon, it will add a new “car form” into the user form. You can add as many cars as you want. Each new form has a set of controls that can be used to sort and remove cars from the form. Let’s take a look on how it all works.
Before we start, as this example is integrated into previous city picker system, I recommend that you read it before, and also the previous article on embedFormForEach.
Ok, let’s start with database model:
config/schema.yml:
1 2 3 4 5 6 7 8 9 10 | cp_users:
_attributes: { phpName: User, idMethod: native }
id: ~
name: { type: varchar(100), required: true }
country_id: { type: integer, required: true, foreignTable: cp_countries, foreignReference: id }
state_id: { type: integer, required: true, foreignTable: cp_states, foreignReference: id }
city_id: { type: integer, required: true, foreignTable: cp_cities, foreignReference: id }
cars: { type: longvarchar }
created_at: ~
updated_at: ~ |
Only thing I added here was at line 8, a new column where we’ll store cars information. Notice that it’s a single column of longvarchar type, and we’ll save car information as a serialized array. As we’re going to store car information as a serialized array, we need to overwrite getCars and setCars methods to make it do the conversion for us:
lib/model/User.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 | public function setCars($cars,$raw=false) { if(!$raw) $cars = array_values($cars); parent::setCars(serialize($cars)); } public function getCars() { return @unserialize(parent::getCars()); } public function getNumberOfCars() { $cars = $this->getCars(); $ncars = 0; if(is_array($cars)) { foreach($cars as $k=>$v) $ncars = max($ncars,intval($k)+1); } return $ncars; } |
setCars will receive an array as parameter and serialize it, and getCars will do the opposite, get a string from database and unserialize it into an array. Notice that setCars expects another parameter, called $raw. If this value is false (default), it will rebuild the array keys, to ensure a sequential value. But as you will see later on, there will be one place that it’s not desired, so we need to instruct setCars to not rebuild keys, passing $raw = true. getNumberOfCars, as the name suggests, returns the number of cars the object has. You might wonder why didn’t I just use return count($this->getCars()) for this… This method won’t return the actual number of cars, but the highest key + 1 in the array, and the reason for this will be explained later on.
To receive input from the user, we’re gonna use the CarForm we created in previous article. Notice that it’s exactly the same form:
lib/form/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('required'=>true,'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); } } |
Again, if you look into previous example, you’ll find another form called CarsForm (plural), that was responsible to get the number of cars user wanted to input. This form won’t be used in this new example, rather, we’ll embed CarForm inside CPUserForm:
lib/form/CPUserForm.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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | class CPUserForm extends sfFormPropel { private $ncars = 0; public function __construct($object = null, $options = array(), $CSRFSecret = null) { parent::__construct($object,$options,$CSRFSecret); // calculate number of cars $this->ncars = $object->getNumberOfCars(); // embed cars form if($this->ncars) { $cars_form = new CarForm(); $this->embedFormForEach('cars',$cars_form,$this->ncars,'%content%','%content%'); // set default $this->setDefault('cars',$object->getCars()); } else $this->embedForm("cars",new sfForm(),"%content%"); } public function configure() { // build state criteria $stateC = new Criteria(); $stateC->add(StatePeer::COUNTRY_ID,$this->getObject()->getCountryId()); // build city criteria $cityC = new Criteria(); $cityC->add(CityPeer::STATE_ID,$this->getObject()->getStateId()); $this->setWidgets(array( 'name' => new sfWidgetFormInput(), 'country_id' => new sfWidgetFormPropelSelect(array('model'=>'Country','add_empty'=>'-- Country --','order_by'=>array('Name','asc'))), 'state_id' => new sfWidgetFormPropelSelect(array('model'=>'State','add_empty'=>'-- State/Province --','order_by'=>array('Name','asc'),'criteria'=>$stateC)), 'city_id' => new sfWidgetFormPropelSelect(array('model'=>'City','add_empty'=>'-- City --','order_by'=>array('Name','asc'),'criteria'=>$cityC)), 'id' => new sfWidgetFormInputHidden(), )); $this->setValidators(array( 'name' => new sfValidatorString( array( 'trim' => true, 'required' => true, ),array( 'required' => '- Please enter name', ) ), 'country_id' => new sfValidatorPropelChoice( array( 'model' => 'Country', 'column' => 'id', ),array( 'required' => '- Please choose country', 'invalid' => '- Invalid country', ) ), 'state_id' => new sfValidatorPropelChoice( array( 'model' => 'State', 'column' => 'id', 'criteria' => clone $stateC, ),array( 'required' => '- Please choose state', 'invalid' => '- Invalid state', ) ), 'city_id' => new sfValidatorPropelChoice( array( 'model' => 'City', 'column' => 'id', 'criteria' => clone $cityC, ),array( 'required' => '- Please choose city', 'invalid' => '- Invalid city', ) ), 'id' => new sfValidatorNumber(array('required'=>false)), )); $this->widgetSchema->setLabels(array( 'name' => 'Name', )); $this->widgetSchema->setNameFormat('user[%s]'); $this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema); } public function bind(array $taintedValues = null, array $taintedFiles = null) { for($i=0;$i<$this->ncars;$i++) { if(!isset($taintedValues["cars"][$i])) { unset($this->widgetSchema["cars"][$i],$this->validatorSchema["cars"][$i]); } } return parent::bind($taintedValues,$taintedFiles); } public function getModelName() { return 'User'; } } |
This form is the same we used in the city picker article, with some new things added in order to handle car form. In fact, configure method is exactly the same. Only __constructor and bind methods were added. Let’s talk about each method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | private $ncars = 0; public function __construct($object = null, $options = array(), $CSRFSecret = null) { parent::__construct($object,$options,$CSRFSecret); // calculate number of cars $this->ncars = $object->getNumberOfCars(); // embed cars form if($this->ncars) { $cars_form = new CarForm(); $this->embedFormForEach('cars',$cars_form,$this->ncars,'%content%','%content%'); // set default $this->setDefault('cars',$object->getCars()); } else $this->embedForm("cars",new sfForm(),"%content%"); } |
__constructor is where we actually embed the CarForm into CPUserForm. As we’ll use embedFormForEach, we need to know, beforehand, the number of times we’ll embed the form. To determine this number, we use the getNumberOfCars method, and also, we store resulting value in a private variable ncars, that will be used inside bind method. If any car was defined, we embed CarForm $ncars times, and then set default values, otherwise, we just embed a generic sForm form.
As I talked before, getNumberOfCars is a bit different and the reason is not hard to explain. Every time you use the green plus icon to add a new car form, it will add a new user[cars][N] element, where N is a sequential number, starting from 0. Suppose you add 4 car forms, and then you remove the third one. You would end up with: user[cars][0], user[cars][1], users[cars][3]. You see the gap? We don’t have [2] anymore. In this case, if getNumberOfCars returns 3 (the actuall number of cars), CPUserForm would embed only three CarForm, and user[cars][3] would become invalid, as it would be the forth form. That’s the reason we must return the highest key (plus 1), in ths case, 4. And now is where bind method takes place:
1 2 3 4 5 6 7 8 9 10 11 12 | public function bind(array $taintedValues = null, array $taintedFiles = null) { for($i=0;$i<$this->ncars;$i++) { if(!isset($taintedValues["cars"][$i])) { unset($this->widgetSchema["cars"][$i],$this->validatorSchema["cars"][$i]); } } return parent::bind($taintedValues,$taintedFiles); } |
Still using the same scenario (added 4 cars, then removed the third). As I said, we’ll still need to embed four forms in order to make it work, but you might think: in CarForm, make and model are required, wouldn’t it throw an error?? The answer is YES. We embed 4 forms, but supply data only for 3, so it will throw an validation error. To avoid this, we need to implement a conditional validation. Remember on constructor we saved the variable ncars? We’ll use it here to remove removed (uh?) cars. For this, we traverse through cars values, and if we don’t find it, we unset both widget and validator for that index. Doing this, the form won’t try to validate a removed field, and also, will remove the widget from the form, preventing it from appear again.
Ok, what about the actions?
apps/frontend/modules/citypicker/actions/actions.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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public function executeEdit($request) { if($request->getParameter('id')) $user = UserPeer::retrieveByPK($request->getParameter("id")); else $user = new User(); $this->forward404Unless($user); $this->errors = array(); if($request->isMethod('post')) { // pre-populate country, state and city in order to filter select boxes $data = $request->getParameter('user'); $user->setCountryId(@$data['country_id']); $user->setStateId(@$data['state_id']); $user->setCityId(@$data['city_id']); // check validity if(!$user->getCity() || ($user->getCity()->getStateId() != $user->getStateId())) $user->setCityId(0); if(!$user->getState() || ($user->getState()->getCountryId() != $user->getCountryId())) $user->setStateId(0); if(!$user->getCountry()) $user->setCountryId(0); // pre-populate cars so we can get number of cars entered $user->setCars(@$data['cars'],true); $this->form = new CPUserForm($user); $this->form->bind($request->getParameter('user')); if($request->getParameter('refresh') != "Y") { if($this->form->isValid()) { $this->form->save(); // if we are handling a Soap request, do NOT redirect if(!$this->isSoapRequest()) return $this->redirect('citypicker/index'); } // get errors into array foreach($this->form->getFormFieldSchema()->getError() as $e) $this->errors[] = $e; } else { // render city/state/country return $this->renderPartial('address_edit',array('form'=>$this->form)); } } else { $this->form = new CPUserForm($user); } } |
Again, most code on this action is to handle city picker and was detailed in another article. Only thing that’s related to CarForm is at line 22. Remember we use getNumberOfCars on CPUserForm constructor? So, we need to pre-populate user object we’ll send to the form with received car information, so it can determine beforehand the number of forms it need to embed. Also, we set $raw=true when calling setCars method, in order to preserve keys, or we’ll break the system.
Now only thing that’s missing is the template, where we’ll add all the javascripts needed to make it work.
apps/frontend/modules/citypicker/templates/editSuccess.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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <form method="post" action="<?=url_for('citypicker/edit?id='.$form->getObject()->getId()) ?>"> <?=$form["id"]->render() ?> <table> <?php if ($errors): ?> <tr> <td colspan="2"> <ul> <?php foreach ($errors as $error): ?> <li><?=$error ?></li> <?php endforeach; ?> </ul> </td> </tr> <?php endif; ?> <tr> <th><?=$form["name"]->renderLabel() ?></th> <td><?=$form["name"]->render() ?></td> </tr> <tr> <th>Address</th> <td> <div id="address"> <?=include_partial('address_edit',array('form'=>$form)) ?> </div> </td> </tr> <tr> <th> Cars <a href="#" title="Add new car" onclick="new Ajax.Updater('fieldset_cars', '<?=url_for('citypicker/addCar') ?>', {parameters: {number: ncars}, asynchronous:true, evalScripts:false, insertion:Insertion.Bottom, onSuccess:function(request, json){ncars++;}});; return false;"> <?=image_tag(sfConfig::get('sf_admin_web_dir').'/images/add.png',array('alt'=>'Add new car','border'=>'0')) ?> </a> </th> <td id="fieldset_cars"> <?$form["cars"]->getWidget()->setFormFormatterName('options') ?> <?=$form["cars"] ?> </td> </tr> <tr> <td colspan="2"> <input type="submit" name="submit" value="save"/> <?=button_to('cancel','citypicker/index') ?> </td> </tr> </table> </form> <script type="text/javascript"> var ncars = <?=count($form["cars"]->getWidget()->getWidget()->getFields()) ?>; </script> |
Ok, you just need to pay attention on code from line 27 on, everything above it is to handle citypicker (once again)…. In this template, we place the gren plus icon and attach a JavaScript event to it. This JavaScript will request an action using AJAX and will append the result in the fieldset_cars container (the action will result in HTML code). Notice that I didn’t use symfony helper to build this ajax call. The reason is that we need to send to the action current number of cars we have in the form, so it can return correct HTML code (field index), and I didn’t find any means to make it work with the helper, so I wrote the Ajax.Updater by myself. It will request the addCar action, and send value stored in ncars variable as a parameter, and then will increment it on success (so we keep track of how many cars we have). ncars is initialized at line 48, retrieving the number of embedded forms.
Another important detail is at line 35. Here we change the FormatterName for CarForm (I could have set it inside CarForm configure method, but this would break previous example). This new formatter will add sort/remove controls to the form:
lib/widget/sfWidgetFormSchemaFormatterOptions.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class sfWidgetFormSchemaFormatterOptions extends sfWidgetFormSchemaFormatter { protected $rowFormat = ""; public function __construct(sfWidgetFormSchema $widgetSchema) { $this->rowFormat = " <table> <tr> <th> Car <a title="Move up" href="#" onclick="if($(this).up('table').previous('table')) $(this).up('table').previous('table').insert({before: $(this).up('table')}); return false">".image_tag('sf_admin/uparrow.png',array('border'=>0,'alt'=>'Move up'))." <a title="Move down" href="#" onclick="if($(this).up('table').next('table')) $(this).up('table').next('table').insert({after: $(this).up('table')}); return false">".image_tag('sf_admin/downarrow.png',array('border'=>0,'alt'=>'Move down'))." <a title="'Remove this option" href="#" onclick="if(window.confirm('Are you sure you want to remove this option?')) $(this.parentNode.parentNode.parentNode).remove(); return false;">".image_tag(sfConfig::get('sf_admin_web_dir').'/images/delete.png',array('border'=>0,'alt'=>'Remove this option'))."</a> </th> </tr> %field% </table>"; parent::__construct($widgetSchema); } } |
This formatter is a little bit different from normal formatters. Usually we only define a few variables inside the class, but we can’t do this here (well, we can, but we would need to write the <img>s by hand). The reason I defined the variable inside class __constructor is because I’m using image_tag helper to build the string, and we can’t to this when defining a variable in a class. Other than that, it’s a normal formatter, that wraps the form into a set of controls. Remember in setCars method, we rebuild array keys? The reason is explained here.. If you look at the javascript that sort the form, it simply swap the <table> elements, but field indexes will be kept the same. For instance, we have two cars added: user[cars][0] and user[cars][1]. If we swap it, we’ll have: user[cars][1] and user[cars][0]. Also, we can have gaps on the keys, so we need to rebuild the keys.
Ok, the last step now is the addCar action, that’s responsible to add a new form into the CPUserForm when u click the green plus icon:
apps/frontend/modules/citypicker/actions/actions.class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public function executeAddCar($request) { $this->forward404unless($request->isXmlHttpRequest()); // get next number $number = intval($request->getParameter("number")); // build widget $form = new CarForm(); $widgetSchema = new sfWidgetFormSchemaDecorator(new sfWidgetFormSchema(array( 'user[cars]['.$number.']' => new sfWidgetFormSchemaDecorator($form->getWidgetSchema(), "%content%") ),array(),array(),array()), "%content%"); $widgetSchema->setFormFormatterName('options'); $this->form = $widgetSchema; } |
At line 6 we get the number of cars we already have, in order to build the next field index. Then, we need to build the form object that will be rendered by the template and sent back. One important point is: we need to build this widget as so the fields name is prefixed by: user[cars][$number]. For this, we wrap CarForm into a sfWidgetFormSchemaDecorator. Also, we need to set FormatterName to add the controls to the new form. Finally, we render it in the template:
apps/frontend/modules/citypicker/templates/addCarSuccess.php:
1 | <?=$form->render(null) ?> |
And that’s it, our new form is ready to work. For this example I didn’t put attention to the design, but you can make it look nicer but changing the options formatter, and by using other formatter for the forms than the table one I’m using.
I’m not sure If I explained very well all the details of this implementation, so if you have any doubts, or comments, feel free to drop me a line on the comments section.




Murena
in
January 2nd, 2009 at 6:19
-
Brilliant post.
I’ve a suggestion for a third part.
Inserting images (file upload) on embedded forms, saved in a table in 1-n relationship. That would be a hit.
dereck
in
January 2nd, 2009 at 18:52
-
Great work, man!
thanks for this post
Carlos Barros
in
January 3rd, 2009 at 18:09
-
Hi Murena, thanks for the suggestion…. I believe it won’t need too much changes, maybe some tweaks on getNumberOfCars (and of course, getCars and setCars), and probably will need to overwrite save method from CPUserForm (I still didn’t go deep on version 1.2, but seems it now automatically saves embeded forms, not sure though). Anyways, I your suggestion is nice, I’ll try to update this article (or maybe make a new one) to handle this situation when I got some time…..
Kevin
in
January 6th, 2009 at 17:43
-
Great post.
1.2 does automatically save embedded forms. There is also a function called mergeForm() (not sure if it was in 1.1). I haven’t used it but I think this would be useful for a 1-1 relationship.
Jordi Hernandez
in
January 26th, 2009 at 9:26
-
Dear Carlos,
It would be possible do the same functionality but using foreign keys for "cars" instead of field with array "cars"?
I tried to do it by myself but I encountered some problems when saving existing records. The question is: "How to create new entries for new table Cars and remove the old ones when I save User?"
Thanks in advance and best regards,
Jordi
Carlos Barros
in
January 29th, 2009 at 13:50
-
Hi Jordi…
Yes, that would be possible to do this using foreign keys…. I didn’t do it myself, so I don’t have a sample code to show, but I guess I would do this way:
* Create a table and give object name = Car;
* Create a new method setCars, that will just save input array somewhere.
* Override getCars method. This new method will retrieve Cars objects (using parent::getCars()), then convert then into arrays (just like this example). This will also save this array in the same place you save in setCars method.
* Override save method (of user object). this method should delete all attached cars, then use getCars to get cars array and re-build all cars object;
Well, I don’t know if this will work, probably will need some more work here and there, but basically that’s the idea.. You should "emulate" the field cars….
Hope this helps
Carlos
Jordi Hernández
in
January 29th, 2009 at 18:26
-
Hello Carlos,
It has been very helpful. I’ll try to programm it and if it works I’ll post my solution.
I really apreciate your response.
Jordi
Jordi Hernández
in
January 29th, 2009 at 21:18
-
Hello Carlos.
I’ve done the changes following your suggestions and it works.
I’ve done the following changes:
Schema:
*************************************
*** Delete "cars" field in cp_users *
*************************************
*************************************
* Insert cp_cars table:
*************************************
cp_cars: _attributes: { phpName: Car, idMethod: native } id: ~ user_id: { type: integer, required: true, foreignTable: cp_users, foreignReference: id, onDelete: Cascade } make: { type: varchar(100), required: true } model: { type: varchar(100), required: true }**************************
*** Change User Class ****
**************************
******************************************+
* Change former getCars and setCars
******************************************
I’ve change the former setCars() and getCars() calls to the new methods setCarsTemp() and getCarsTemp()
*******************************************
* Change bind method in CPUserForm
*******************************************
Without this line the class sfFormPrope throw a "notice" at method updateObjectEmbeddedForms
I hope it will be helpful.
Jordi
Jordi Hernández
in
January 29th, 2009 at 21:20
-
Sorry the full User class is the following:
Carlos Barros
in
January 30th, 2009 at 8:07
-
Hi Jordi.. This looks great!!!
Thank you very much for sharing… I edited your comment to add syntax highlight to the code….
Carlos
mppfiles
in
February 10th, 2009 at 9:40
-
@Carlos
Great article, really.
@Jordi
(on method save of your User class)
Maybe you could delete the cars attached using a Criteria rather than a foreach.
Anyway, excellent contribution guys
Jordi Hernández
in
February 10th, 2009 at 15:22
-
Dear mppfiels,
You’re right. Definitely it would be better use a Criteria instead my solution but it was only a quick solution.
Nevertheless thank you for your appreciation.
Jordi
mc_maho
in
March 5th, 2009 at 8:43
-
Hi, grate work.
I would know if i can do the same work but only with javascript not Ajax. If it is how could I ?
Carlos Barros
in
March 6th, 2009 at 9:05
-
Hi mc_maho… Yes, it’s possible. I did it sometime ago using javascript.. All you need to do is convert the AJAX call into a normal jscript function call. This function will generate the HTML for your fields and add back to the page. Here’s one example:
The above code is the fields templates. In this case I have 4 fields. Below is the jscript function that will receive a number of "forms to embed", and will use the template to generate the fields and place into the page:
I hope this helps
Carlos
mc_maho
in
March 7th, 2009 at 10:46
-
thank you Carlos for your time (really I mean it), but how about the server side ?
thank you again
Volker
in
March 10th, 2009 at 8:50
-
I figured out the way how to use a symfony helper for displaying the "add" button. Just use the following code, it works perfectly when I adapted it to my needs:
[CODE]
‘Add new car’,'border’=>’0′)), array(
‘url’ => ‘citypicker/addCar’,
‘update’ => ‘fieldset_members’,
’success’ => ‘nb_members++;’,
‘position’ => ‘bottom’,
‘with’ => ‘{number: nb_members}’
)); ?>
[/CODE]
Volker
in
March 10th, 2009 at 9:56
-
Actually I had a typo in my previous post:
[CODE]
echo link_to_remote(image_tag(sfConfig::get(’sf_admin_web_dir’).’/images/add.png’, array(‘alt’=>’Add new car’,'border’=>’0′)), array(
‘url’ => ‘citypicker/addCar’,
‘update’ => ‘fieldset_members’,
’success’ => ‘ncars++;’,
‘position’ => ‘bottom’,
‘with’ => ‘{number: ncars}’
));
[/CODE]
A question, if I may: Can this be modified so I don’t save serialized strings in my database but full propel objects? I want to use this to save members to for a registration. Here’s an example: A user signs up for an event and is able to add invitations in advance. He needs at least two, so it would be helpful to have two empty embedded objects when the form is initially called.
Carlos Barros
in
March 14th, 2009 at 14:06
-
mc_maho: The server side is pretty much the same… No changes at all is required, as long as you keep the same fieldnames structure as before…
Volker: thanks for you contributions… Regarding your question, this was discussed before in this post entry and a solution proposed by Jordi Hernández (in the comments above), but I didn’t test it…
Carlos
mc_maho
in
March 18th, 2009 at 5:09
-
Hi Carlos, now I think am understanding you.
First you embed forms in the configure method (you know exactly how many forms you get) and then you create them by javascript.
In my case I do not know previously how many forms to embed,
I let user decide (ex: by adding button "add form" -car in you example- but only in js manner), so like that I don’t have to embed forms in the configure method.
My really interest is to know the exact scenario to cache those data and bind them to there corresponding forms.
Carlos Barros
in
March 19th, 2009 at 22:08
-
Hi mc_maho….
Actually, in the example shown in this post, I don’t need too know how many forms to embed. This number will be calculated based on the number of "fields" received from the form.. The technique I used is a bit tricky and not very obvious.. If you look at CPUserForm form constructor, at line 10, you notice we get the number of forms to embed using a getNumberOfCars method. This method will count the number of cars received and return this value. So, if you want to use only Java Script, you just need to add new form elements and then send back to server, and it will handle it the same way..
Carlos
mc_maho
in
March 23rd, 2009 at 5:11
-
Hi Carlos it’s me again. Thank you very very much, I tried it out and it works fine.
Just a little thing :
To get cars reordered correctly I did:
public function setCars($cars)
{
$cars = array_values($cars);
parent::setCars(serialize($cars));
}
and in the bind method the same thing:
public function bind(array $taintedValues = null, array $taintedFiles = null)
if(isset($taintedValues["cars"]))
{
$taintedValues["cars"] = array_values($taintedValues["cars"]);
}
return parent::bind($taintedValues,$taintedFiles);
}
mc_maho
in
March 23rd, 2009 at 8:34
-
Hi Jordi, Carlos
Jordi, in your sample why you do the save manually ? why you didn’t let the saveEmbeddedForms method do it’s job?
Christian
in
April 26th, 2009 at 12:42
-
Hi Carlos,
excellent article, just what I was looking for! Thanks a lot for your effort, really well done.
I have one question: is there a particular reason why you store the cars as a string (serialised object)? Couldn´t this be done with DB as usual? I´m curious about the answer… I bet you have a good reason for it.
Thanks again,
Christian
Rafael Hernandez
in
May 1st, 2009 at 15:57
-
Can yo help me I have error unexpected T_STRING in –C:\wamp\www\ubicacion\lib\widget\sfWidgetFormSchemaFormatterOptions.php on line 14
Jordi can you send me you source code of your app in a zip file
I use symfony 1.2
Carlos Barros
in
June 2nd, 2009 at 8:52
-
Hi Christian, sorry for the long time to reply, was in vacation
Well, I had to good reason for that, maybe just because I thought it would be easier to do. It really can be done using DB objects, actually you can see the comments from Jordi Hernández, he did it using DB objects.
Rafael, can u paste your code here in the comments??
Carlos
Christian
in
June 2nd, 2009 at 9:15
-
Thanks for your reply, in the meanwhile I implemented the DB version (thanks Jordi!!!)… and it works.
There is one big problem though: I’d like to mix the adding of users and cars on one single page without page loads. The problem: cars and users start to mix and it is much more difficult to keep the correct cars with their users.
Do you think it is possible with your approach to accomplish this?
Carlos Barros
in
June 3rd, 2009 at 9:21
-
Hi Christian.
Well, I think it’s possible to mix both together. Probably not very simple but not impossible.
Carlos
Christian
in
June 3rd, 2009 at 9:32
-
Thanks Carlos. I fight for quite some time with this mix of multiple users with multiple cars (n x m relationship). The main problem I encountered:
Instead using embedforeach I need to write my own foreach with a simple embed to pass the initialised car objects and not all the time the same empty car object as for the embedforeach. This caused quite some mess, because now I need to manually build the array of embedded forms.
Would be great if you could make a follow-up tutorial on this one.
xiao
in
July 10th, 2009 at 17:46
-
testing
hendra1
in
July 30th, 2009 at 5:33
-
there is a typo in :
lib/model/User.class.php:
there is no User.class.php
but CpUser.class.php
hendra1
in
July 30th, 2009 at 7:21
-
by the way how to re display the data of cars after i save the form,
i try to reterivebypk object CpUserPeer but the result of CpCars is empty
is there something wrong?
Carlos Barros
in
July 30th, 2009 at 10:58
-
Hi hendra1.. About the typo, I just verified here and i have lib/model/User.class.php. cp_cars is the table name.
As far as retrieving cars after a save, you just need to call the method getCars of the User (or CpUser in your case) object. If you are using my method, it will return you an array of [make,model], or if you are using Jordi method, it will return you an array of Cars object.
Carlos
Matthew
in
July 31st, 2009 at 14:30
-
Hi Carlos and Jordi and thanks for this tutorial
I’m having difficulty with the add_car part of the form. The HTML returned by executeAddCar() action and the render(null) ?> call in the template is being escaped so that actual HTML tags appear in the browser and not the form.
I’m using Symfony version 1.2.5
Please help:)
Many thanks, Matthew
Carlos Barros
in
August 3rd, 2009 at 15:13
-
Hi Matthew…
Im not yet using Symfony 1.2 (I stopped for some months developing for symfony) so Im not sure about this.. But, did u check your escaping strategy??
Carlos
Matthew
in
August 3rd, 2009 at 20:23
-
Thanks Carlos
Yes that was it, somehow my escaping_strategy was set to true in settings.yml…
Erin
in
December 3rd, 2009 at 2:16
-
I’ve written up a tutorial on how to implement a dynamic expanding form using symfony’s embedded forms. If you’re interested check out http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ and let me know what you think. Any feedback would be most welcome.
dead fish » Blog Archive » Doctrine Horror in January 15th, 2010 at 10:47-
[...] want to add FooBarBazForm via sfForm::embedFormForEach a couple of times (similar to the use case described here), you suddenly have the problem that your embedded form for the appended new FooBarBaz object [...]
Carl
in
January 21st, 2010 at 5:18
-
When I originally commented I clicked the "Notify me when new comments are added" checkbox and now each time a comment is added I get four emails with the same comment. Is there any way you can remove me from that service?Thanks
iki488
in
January 25th, 2010 at 15:53
-
I Carlos, thank you for this post, it is one of the first time I see an article using form serialization in symfony. Actually, this topic is what I’m searching for. I tried to adapt your idea to my project, but it gives me errors that I couldn’t manage to solve…
Would be really cool if you could help me with this :
For my project, I embed only one subform, but it is actually a filter form from my user module generated by the admin generator in my backend. What I’m trying to do is to embed this form in the form of another module, called "saved_filter". The purpose of this module is to store the criteria entered in the subform, to be able to reuse them anytime the user will want. So a "saved_filter" form contains a name (inputtext), a filter_object field (textarea, will contain the serialization of the subform) and the subform itself (to remind, it is a filter form object from another module "user" generated by the admin generator in my backend). So I need to be able to make CRUD operation on my "saved_filter" object plus be able to "execute" it, meaning to get the resulting list of users corresponding to the criteria of the subform.
I’m stuck for days on the implementation of this…
If you can help me, please, I would really really appreciate
Carlos Barros
in
January 26th, 2010 at 8:58
-
Hi iki488,
What errors are you getting? It’s a bit hard to understand it without actually seeing some sample and/or code of what you are trying to do.
Regards
Carlos Barros
iki488
in
January 26th, 2010 at 15:30
-
I carlos,
I know it is much to ask, but is it possible tu speak directly via email or gtalk or something?
About the errors, I’m getting an error about nested function limit, basically, it call and recall always the same sequence of function :s
Fatal error: Maximum function nesting level of ‘100′ reached, aborting!
sfFormObject->save( ) ../actions.class.php:192
19 0.1949 15614060 sfFormObject->doSave( ) ../sfFormObject.class.php:130
20 0.1949 15614124 sfFormObject->updateObject( ) ../sfFormObject.class.php:159
21 0.1953 15614476 sfFormDoctrine->doUpdateObject( ) ../sfFormObject.class.php:183
22 0.1953 15614544 Doctrine_Record->fromArray( ) ../sfFormDoctrine.class.php:148
23 0.1959 15616056 Doctrine_Record->set( ) ../Record.php:1967
24 0.1959 15616900 PluginEnaSavedFilter->setFilterObject( ) ../Record.php:1432
25 0.1960 15617268 BaseEnaSavedFilter->setFilterObject( ) ../PluginEnaSavedFilter.class.php:18
26 0.1960 15617648 sfDoctrineRecord->__call( ) ../sfDoctrineRecord.class.php:0
27 0.1961 15619152 call_user_func_array ( ) ../sfDoctrineRecord.class.php:212
28 0.1961 15619428 Doctrine_Record->set( ) ../sfDoctrineRecord.class.php:0
29 0.1961 15619960 PluginEnaSavedFilter->setFilterObject( ) ../Record.php:1432
30 0.1962 15620328 BaseEnaSavedFilter->setFilterObject( ) ../PluginEnaSavedFilter.class.php:18
31 0.1962 15620708 sfDoctrineRecord->__call( ) ../sfDoctrineRecord.class.php:0
32 0.1962 15622212 call_user_func_array ( ) ../sfDoctrineRecord.class.php:212
33 0.1962 15622488 Doctrine_Record->set( )
And it recall that again and again…
Carlos Barros
in
January 28th, 2010 at 10:45
-
Hey..
Hmm, looks like you have a loop here.. Maybe you have a form that embeds another form that in turn links somehow with the parent, so when you
save the first, it calls save in the embedded form that in turns will call save in the parent and start the endless loop. Probably something like this is happening.
Carlos