Pre-fill a form in Symfony
Hi all,
I hope I didn’t miss anything evident since my issue really sounds easy at first sight… What is my problem ? I just want to re-use in the backend objects that already exist to pre-fill some forms.
Let’s illustrate the situation with a concrete exemple before jumping into the code. A shop sells more than 100 natural and organic personal beauty products based on lets say code (butter, face-cream…). Each reference is a complicated and somehow granny-accross-centuries-receipe secret that is made of more than 30 ingredients… plus a pink-tainting product to reach girls. This shop is so successful that the owner has opened a new one just one block away. It sells the exact same range of products, he just replaced the pink-tainting color by a blue-tainting because he now wants to target a male audience. In his backend, he will have to « create » the new shop and then for each one of the 100 product to populate the ingredients list with the 30 identical ingredients, only replacing the pink with the blue tainting-product. How tedious !!! What he’d like to do is create his new shop duplicating the previous products and kind of « crediting » (mix of creating and editing) the object, replacing only one field (the tainting product).
It’s just weird that this basic functionnality took me so many time to build. Did I miss a point in my symfony learning ?? So here is how I solved the problem…
- Create a component to display a productToDuplicate field above the form
- Add a hint of AJAX to autocomplete your search field
- Add a PopulateProductForm action to create a pre-filled form
- Write a PopulateProductFormSuccess to display pre-filled form
- Add a CreatePopulated action to save pre-filled form as a new Object
Create a component to display a productToDuplicate field above the classic shop form
Why creating a component and not just a simple partial that you would call in the generator.yml file of your module ? Well, we need an action to create the search widget and then a template to display it above the form. Seems to match components functions…
Here is the action to write :
//apps/backend/modules/myModule/actions/components.class.php public function executeProductToDuplicateFinder() { $this->productForm = new sfForm(); $this->productForm->setWidgets(array( 'duplicated_product' => new sfWidgetFormJQueryAutocompleter( array( 'url' => '/backend.php/product/findProduct', 'config' => '{ scrollHeight: 250 , autoFill: true , cacheLength: 1 , delay: 50, matchSubset: false}', 'label' => 'Duplicate a product :' ), array( 'size'=> '70' ) ))); }
If you want more information on the sfWidgetFormJQueryAutocompleter, just go and see this previous article about autocomplete widget
Here is the component’s template associated. Notice that you passed the productForm to the template so you can « echo » it with all the methods symfony offers you.
//apps/backend/modules/myModule/template/_productToDuplicateFinder <?php use_helper('Form');?> <!-- Display an autocomplete input text to find a product and duplicate it --> <div class="sf_admin_form"> <!-- helper indicating to call populateProductForm action, dont forget to close it... --> <?php echo $productForm->renderFormTag('populateProductForm')?> <table> <tr> <th> <?php echo $productForm['duplicated_product']->renderLabel();?> </th> <td> <?php echo $productForm['duplicated_product']->renderError();?> <?php echo $productForm['duplicated_product']->render();?> <?php echo submit_image_tag('myFolder/tick.png'); ?> </td> </tr> </table> </form> </div>
Make sure you closed the tag that you implicitely opened with the renderFormTag method…
Add a hint of AJAX to autocomplete your search field
I will go quickly through this part since it is not the heart of the article. In a few words, you have to write an action that will first
- Write a method that will match the objects of ProductTable with the ‘q’ string parameter
- Put all the results you got into a result table (use a foreach loop)
- Return your Json encoded result
return $this->renderText(json_encode($results));
Add a PopulateProductForm action to create a pre-filled form
This is the action we called when clicking on our ‘submit’ button in the product finder. So what do we want to do ? We want to display a product form, I should even say a new product form, but pre-filled with the properties of the selected existing product. That’s easy :
//apps/backend/myModule/actions/actions.class.php public function executePopulateProductForm(sfWebRequest $request) { //throw exception if not POST request $this->forward404Unless($request->isMethod('post'), 'not a "POST" request'); //collect id of duplicated_product $duplicated_product_id = $request->getParameter('duplicated_product'); //Check if duplicated_product ID is valid $this->forward404Unless($duplicated_product = Doctrine::getTable('Product')->find($duplicated_product_id) , sprintf('unknow id for a product (%s)', $duplicated_product_id)); //set copied_product name and shop to null $duplicated_product->setName(null); $duplicated_product->setShopId(null); //create a form based on the copied_product $this->form = new BackendProductForm($duplicated_product); //in the backend the object must be passed to the template $this->product = $this->form->getObject(); }
The only thing that remains is to display this form built on the existing duplicated_product. The problem will be that our form won’t be considered as a new one by symfony (and you can’t set his isNew() property to true since this method is protected…). But for now, let’s display it.
Write a PopulateProductFormSuccess to display pre-filled form
What I did was to go in the cache and then to copy the code from an editSuccess method just adapting it…
//apps/backend/modules/myModule/template/populateProductFormSuccess //apps/backend/modules/myModule/template/populateProductFormSuccess <?php use_helper('I18N', 'Date') ?> <?php include_partial('product/assets') ?> <div id="sf_admin_container"> <h1><?php echo __('New Product', array(), 'messages') ?></h1> <?php include_partial('product/flashes') ?> <div id="sf_admin_header"> <?php include_partial('product/form_header', array('product' => $product, 'form' => $form, 'configuration' => $configuration)) ?> </div> <div id="sf_admin_content"> <?php //include_partial('product/form', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?> <!-- BEGINNING OF THE NEW PART --> <div class="sf_admin_form"> <?php //echo form_tag_for($form, '@form') ?> <!-- Replaced by the hand written form_tag to force a redirect to a create action --> <form action="<?php echo url_for('product/createPopulated') ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>> <?php echo $form->renderHiddenFields() ?> <?php if ($form->hasGlobalErrors()): ?> <?php echo $form->renderGlobalErrors() ?> <?php endif; ?> <?php foreach ($configuration->getFormFields($form, $form->isNew() ? 'new' : 'edit') as $fieldset => $fields): ?> <?php include_partial('product/form_fieldset', array('product' => $product, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?> <?php endforeach; ?> <?php include_partial('product/form_actions', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?> </form> </div> <!-- END OF THE NEW PART --> </div> <div id="sf_admin_footer"> <?php include_partial('product/form_footer', array('product' => $product, 'form' => $form, 'configuration' => $configuration)) ?> </div> </div>
Now we have our pre-filled product with all the ingredients already filled. You just have to change the tainting color from pink to blue and here it is… The last step is to save our new product in the data base.… That’s the aim of the following line of code that I changed from the cache :
<form action="<?php echo url_for('product/createPopulated') ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>
I force a createPopulated action when submitting the form…
Add a CreatePopulated action to save pre-filled form as a new Object
Indeed, when symfony saves an object, it first check whereas the form isNew or not. If isNew is true, a new object will be created in the database. In the edit case, it will just do an update.
So what you have to do is create a new Form in your action, but in the same time use the request object to get the tainted values. If you only do this, you will be very disappointed : symfony will update, not create, an object. A way to avoid this is to set the tainted values ‘id’ to null !! See what it looks like :
//apps/backend/myModule/actions/actions.class.php public function executeCreatePopulated(sfWebRequest $request) { //create a new Form so that isNew() will be true and save will //trigger an insert in the database $this->form = $this->configuration->getForm(); $this->product = $this->form->getObject(); //We have to set the id to null otherwise it will try to update the object, not insert $tainted_values = $request->getParameter($this->form->getName()) ; $tainted_values['id'] = ''; //we modify the $request so that we can call the processForm method without overriding it $request->setParameter($this->form->getName(),$tainted_values); $this->processForm($request, $this->form); $this->setTemplate('edit'); }
And here it is ! Ouf… If anybody has a simpler solution to offer, he will be welcomed. Hope this was useful.
Image may be NSFW.
Clik here to view.
Clik here to view.
