Forms in Symfony 2 without Doctrine

Posted on April 8, 2014, 3:05 pm by about-dev.com


I’ve received recently a task to create a form with the Symfony Form Component. So far so good, what a big deal. A form is a form no matter on what framework you develop it. Well, not quite! 
The fun begins when you realise that you are constraint to display the form using one of the available Symfony Form Component layouts: table or div (available in /vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/Form/*) and you have to use event listeners for the on change events on different input fields, like selects. And the icing on the cake: you should not use Doctrine ORM, but some “homemade“ models that connects with MySQL.

I’ve spend a lot of time searching for solutions and the results I’ve found where for Symfony with Doctrine ORM, that’s why I’ve decided to write this post on how to use Symfony Form Component without Doctrine, how to use Symfony Form Component with MySQL.

For an easy understanding of this article you can read about the structure of the Symfony framework and how it works here and you can grab a copy of the framework from here

Let’s take it by steps:

Step 0: Suppose we have to create an add/edit address form and integrate it into the Symfony Framework version >=2 already installed somewhere on your web server and properly configured with the following structure created into /var/www/site_name, for example:

Before any action we should define our routes:

demo_test_add_address:
          pattern:  /add-address
          defaults: { _controller: DemoTestBundle:Address:addAddressForm }
   requirements:
          _method: GET|POST

demo_test_localities:
          pattern:  /localities/{id}
   defaults: { _controller: DemoTestBundle:Address:getLocalities }
   requirements:
          _method: GET
          id: (\d+)

Step 1: Create the form class generator using the Symfony Form Component (AddressForm.php)

*) here I listed only the buildForm() method of the AddressForm form generator class (you can found the rest of the class in the archive)
*) I split the code into separate section to explain what it does

/*
            - $_data property is used when we edit a form 
            - this property will be filled with information from an address, send from the controller to the form generator class
*/

//the buildForm() method of the AddressForm form generator class
public function buildForm(FormBuilderInterface $builder, array $options){
            $regions = Address::getAllRegions();
            ksort($regions);
     
            //get localities for editing
            $localities = array();
            if(!empty($this->_data['region_id'])){
                $localities = Address::getLocalitiesByRegion($this->_data['region_id']);
            }

Next I set the options for the region and locality form inputs here because they are needed in many places in this method. The locality content comes from an ajax request and I attached a form event listener to it (see below how).

$options =
            array(
                'region'
                    => array(
                            'label'             => 'County*',
                            'choices'           => $regions,
                            'data'              => !empty($this->_data['region_id'])?$this->_data['region_id']:0,
                            'empty_value'       => 'Pick a county',
                            'attr'              => array('style'=>'width:210px'),
                            'trim'              => true,
                            'required'          => false,
                            'constraints'       => array(new NotBlank(array('message'=>'Please pick a county!'))),
                            'invalid_message'   => 'Please pick a county!'
                            ),
                'locality'
                    => array(
                            'label'            => 'Locality*',
                            'choices'           => !empty($localities)?$localities:array(),
                            'data'              => !empty($this->_data['locality_id'])?$this->_data['locality_id']:0,
                            'empty_value'       => 'Pick a locality',
                            'required'          => false,
                            'attr'              => array(
                                                        'style'    =>'width:210px',
                                                        'disabled'  => !empty($this->_data['locality_id'])?false:true
                                                        ),
                            //'label_attr'        => array('style'=>'margin-left:30px;'),
                            'trim'              => true,
                            'constraints'       => array(new NotBlank(array('message'=>'Please pick a locality!'))),
                            'invalid_message'   => 'Please pick a locality!',
                            'auto_initialize'   => false
                            )
                );

   

The form elements are listed in the order they are created, so I begin with the hidden id input and I finish with the zip code input.

$builder->add('id', 'hidden', array('data'=>!empty($this->_data['id'])?$this->_data['id']:0));
 
$builder->add('street_address',
                        'text',
                        array(
                            'label'             => 'Address*',
                            'attr'              => array(
                                                        'style'    => 'width:210px',
                                                        //this should customize HTML5 validation messages BUT is not working
                                                        'oninvalid'=> 'setCustomValidity("")',    
                                                        'onfocus'  => 'setCustomValidity("")'
                                                        ),
                            //default is required ==> force HTML5 validation by browsers
                            'required'          => true,  
                            'trim'              => true,
                            'data'              => !empty($this->_data['address'])?$this->_data['address']:'',
                            //needed to acces form errors in controller, for example (not to display them directly in Twig)
                            //'error_bubbling'    => true,   
                            'constraints'       => array(
                                                        new NotBlank(array('message'=>'Please fill the address!')),
                                                        new Length(
                                                                array(
                                                                    'min'      =>10,
                                                                    /*
                                                                      because in SF 2.2 is a BUG with this validator
                                                                      ==> tries to apply transChoice
                                                                     */
                                                                    'minMessage'=>'Please fill minimun %s characters!|Please fill minimun 10 characters!',
                                                                    'max'       =>200,
                                                                    'maxMessage'=>'Please fill maximum %s characters!|Please fill maximum 200 characters!'
                                                                    )),   
                                                    ),
                            'invalid_message'   => 'Please fill the address!'
                            )
                    );
 
$builder->add('region', 'choice', $options['region']);
$builder->add('locality', 'choice', $options['locality']);

As I mentioned above I attached an event listener to the region form element. When it is changed the locality input is populated with content, based on the selected region id.

$extVaribles = array('factory'  => $builder->getFormFactory(),
                            'options'   => $options,
                            'self'      => $this);
$regionModifier = function (FormInterface $form, $region) use (&$extVaribles){
            $regionData = !empty($extVaribles['options']['region']['data'])?$extVaribles['options']['region']['data']:$region;
            $extVaribles['options']['locality']['choices'] = $extVaribles['self']->getLocalities($regionData);
            $extVaribles['options']['region']['data']      = $regionData;
            unset($extVaribles['options']['locality']['attr']['disabled']);
 
            $form->add('locality', 'choice', $extVaribles['options']['locality']);
};
 
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($regionModifier){
                                                                $region = $event->getData();
                                                                $regionModifier($event->getForm(), $region);
                                                            });
$builder->get('region')->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) use ($regionModifier){
                                                                $region = $event->getForm()->getData();
                                                                $regionModifier($event->getForm()->getParent(), $region);
});

This is the last input element of the address form, the zip code:

$builder->add('postcode', 'text', array('label'     => 'Zip code',
                                                'data'      => !empty($this->_data['zip_code'])?$this->_data['zip_code']:'',
                                                'attr'      => array('style'=>'width:210px !important;'),
                                                'required'  => false,
                                                'trim'      => true));
}    //this acolade is from the start of the buildForm() method

Step 2: Create the view where we are going to display the form (address.html.twig)

*) here I listed the form generation; the rest of the code is in the address.html.twig view file

  {# overwrite the form_rows block and the form_errors block #}
  {% block form_rows %}
   {% spaceless %}
    {% for child in addressForm %}
     {# check for input type: if hidden do not display label #}
     {% if child.vars.name != "hidden" %}
      {#
      label can be translated from form class, by specifing the translation domain
      and then add the text in that file
      #}
      {{ form_label(child) }}
      {#{{ form_label(child, child.vars.label|trans) }}#}   
      {# to translate label from TWIG; NOT translated by default #}
     {% endif %}
     
     {{ form_widget(child) }}
     {% if form_errors(child) %}
      {% block form_errors %}
       {% spaceless %}
        {{ form_errors(child)|striptags }}
       {% endspaceless %}
      {% endblock form_errors %}
     {% endif %}
    {% endfor %}
   {% endspaceless %}
  {% endblock form_rows %}

Step 3: Create the controller that takes care of the form processing (AddressController.php

*) here I listed the address form processing method; the rest of the code is in the AddressController.php controller file

public function addAddressFormAction() {
         $form = $this->createForm(new AddressForm());
   
         if($this->get('request')->isMethod('POST')){
             $form->handleRequest($this->get('request'));
             if($form->isValid()){
              //do smth
                 echo 'VALID';
             }else{
                 //get form errors
                 $formErrors = array();
                 foreach($form->all() as $item){
                     if(is_array($item->getErrors()) && count($item->getErrors()) > 0){
                         $localErrors = explode('ERROR: ', $item->getErrorsAsString());
                         $formErrors[$item->getName()] = !empty($localErrors[1])?$localErrors[1]:'';
                     }
                 }
                  
                 //set errors into a notification handler or send them to the view in order to display them
             }
         }
   
         return $this->render('DemoTestBundle:Address:address.html.twig', array('addressForm'=>$form->createView()));
}

Step 4: Your application should like this:

Download source files: Download source code and installation instructions

Client-side validation of Symfony forms:

  1. APYJsFormValidation bundle: https://github.com/Abhoryo/APYJsFormValidationBundle
    1. Compatibility: Symfony version 2.1+
    2. Prerequisite: 
      1. BazingExposeTranslation bundle ==> we already use JMSTranslation bundle
      2. Doctrine ORM 
         
  2. JsFormValidation bundle: https://github.com/formapro/JsFormValidatorBundle
    1. Compatibility: Symfony version 2.3+
    2. Prerequisite: NONE

Related articles:


Leave a Comment:

User
Email
Website

Blog Search

Popular Blog Categories

Newsletter

Want to be informed about latest posts? Subscribe to our newsletter