Introduction
This tip presents the steps you have to take to create a functional form using the Symfony Form Component and the Symfony framework without the use of Doctrine ORM.
Background
For an easy understanding of this tip, 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.
Using the Code
To use the code from this tip, you have to copy and paste the content into the indicated spots from the Symfony framework structure (model, form, controller, view).
In this code, I use the Address model that you can grab from the attached documentation (src/Demo/TestBundle/Model/Address.php file).
Implementation
Step 0
Suppose we have to create an add/edit address form and integrate it into the Symfony Framework version 2.5 already installed somewhere on your web server and properly configured with the following structure:
Before any action, we should define our routes:
- This code comes from the src/Demo/TestBundle/Resources/routing.yml file:
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)
- This file should be saved under the src/Demo/TestBundle/Form folder, in the AddressForm.php file
- Here I listed only the
buildForm()
method of the AddressForm
form generator class (the rest of the class you can find in the archive) - I split the code into separate section to explain what it does
<?php
public function buildForm(FormBuilderInterface $builder, array $options){
$regions = Address::getAllRegions();
ksort($regions);
$localities = array();
if(!empty($this->_data['region_id'])){
$localities = Address::getLocalitiesByRegion($this->_data['region_id']);
}
?>
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).
<?php
$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
),
'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.
<?php
$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',
'oninvalid'=> 'setCustomValidity("")',
'onfocus' => 'setCustomValidity("")'
),
'required' => true,
'trim' => true,
'data' => !empty($this->_data['address'])?$this->_data['address']:'',
'constraints' => array(
new NotBlank(array('message'=>'Please fill the address!')),
new Length(
array(
'min' =>10,
'minMessage'=>'
Please fill minimum %s characters!|Please fill minimum 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.
<?php
$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.
<?php
$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));
}
?>
Step 2
Create the view where we are going to display the form (address.html.twig
).
- This file should be saved under the src/Demo/TestBundle/Resources/views/Address folder
- 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 specifying 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 %}
{# domain for errors/validation rules is validators.bg.xlf #}
{{ 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)
- This file should be saved under the src/Demo/TestBundle/Controller folder
- Here I listed the address form processing method; the rest of the code is in the AddressController.php controller file
<!--?php
public function addAddressFormAction() {
$form = $this--->createForm(new AddressForm());
if($this->get('request')->isMethod('POST')){
$form->handleRequest($this->get('request'));
if($form->isValid()){
echo 'VALID';
}else{
$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]:'';
}
}
}
}
return $this->render('DemoTestBundle:Address:address.html.twig',
array('addressForm'=>$form->createView()));
}
?>
Step 4
Your application should look like this:
or like this, when there are validation errors:
Points of Interest
Client-side validation of Symfony forms: