Introduction
Having multiple select boxes and populate them upon previous selection is quite familiar and useful (country/cities example)
In this article I will explain a way (not the best nor the unique) to have it working with symfony2 and a little but inestimable help of jQuery. We will be facing some concepts like DataTransformers, FormTypes, Depedency Injection and some more, so be prepared.
We’ll go through a simple example of creating/editing providers. When adding new providers, location must be set. Location is composed of a community, province, city and street. Province and community are select boxes, for sake of simplicity I haven’t used a select box for cities. Province is related to community, that means, once we select a community, related provinces will be loaded from database and into province select box.
Community/province case is similar to well-known country/cities case. This is a real example about administrative division in Spain.
You can find the source code for this example in my github account. An on-line example to play with is available here.

community province model
Create the entities
The first step is to create the needed entities into Entiy folder.
class Community
{
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string $name
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @ORM\OneToMany(targetEntity="Community", mappedBy="community")
*/
private $provinces;
/**
* @ORM\OneToMany(targetEntity="Location", mappedBy="province")
*/
private $locations;
class Province
{
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string $name
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="Community", inversedBy="provinces")
* @ORM\JoinColumn(name="community_id", referencedColumnName="id")
*/
private $community;
/**
* @ORM\OneToMany(targetEntity="Location", mappedBy="province")
*/
private $locations;
As you can see there is a new entity named Location, which will be handy at the time of building the forms and separate things. This approach has a drawback increasing the number of relations and breaking some canonical forms in database design. Hopefully someone get this right.
The new model, adding provider entity, will look like this

model complete
class Location
{
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Community", inversedBy="locations")
* @ORM\JoinColumn(name="community_id", referencedColumnName="id")
*/
private $community;
/**
* @ORM\ManyToOne(targetEntity="Province", inversedBy="locations")
* @ORM\JoinColumn(name="province_id", referencedColumnName="id")
*/
private $province;
/**
* @var string $city
*
* @ORM\Column(name="city", type="string", length=255)
*/
private $city;
/**
* @var string $street
*
* @ORM\Column(name="street", type="string", length=255)
*/
private $street;
class Provider
{
/**
* @var integer
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank(groups="Provider")
*/
protected $name;
/**
* @var string
*
* @ORM\Column(type="string")
* @Assert\NotBlank(groups="Provider")
*/
protected $phone;
/**
* @ORM\OneToOne(targetEntity="Location");
* @ORM\JoinColumn(name="location_id", referencedColumnName="id")
*/
protected $location;
For the creation of getters and setters we can use the console command.
app/console doctrine:generate:entities
Loading with fixtures
I’ve created some fixtures in yaml with communities, provinces,locations and providers to start working.This is out of the scope of this article so take a look at the code if you are interested.
You can try to load the fixtures once database and schema is created with the console command.
app/console doctrine:load:fixtures
Building forms
To allow create and edit providers we build the following form
class ProviderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('name');
$builder->add('phone');
$builder->add('location',new LocationType());
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\StoreBundle\Entity\Provider'
);
}
public function getName()
{
return 'provider';
}
}
ProviderType has a LocationType embedded, this cool feature of symfony2 saves us a lot of time and simplifies things. LocationType looks like this:
class LocationType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('community','entity',array(
'class' => 'Acme\StoreBundle\Entity\Community',
'property' => 'name'));
$builder->add('province','null_entity',array(
'class' => 'Acme\StoreBundle\Entity\Province',
'property' => 'name'));
$builder->add('city');
$builder->add('street');
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\StoreBundle\Entity\Location'
);
}
public function getName()
{
return 'location';
}
}
Note how community and province are build. The first one is an entity type which retrieves the data from the database and shows up as a select box. Second one is created as null_entity, a custom type that simply does shows and empty select box and register some data transformers to convert the data coming from data submitted in the request string (id of the entity) into it’s php object class, in this case Province class.
Creating and Registering the null_entity type.
The need of this type is that by default EntityType populates the select box with all the data in the database (Province table in this case). Imagine that province table is holding 8000 record (think about countries around the world) . This is completely unnecessary as province select box will be populated using jQuery with the needed data once a community is selected. To override this behaviour I have created this null_entity which does not load any data by default, but register the DataTransformers.
I have followed Symfony2 source code to place this file, so it allows the implementation of other ORM or ODM. It is interesting to have this FormType along with its DataTransformer in their own bundle as this is likely to be reusable.
class NullEntityType extends AbstractType
{
public function __construct(RegistryInterface $registry)
{
$this->registry = $registry;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder->prependClientTransformer(new TextToIdTransformer(
$this->registry->getEntityManager($options['em']),
$options['class'],
$options['property']
));
}
public function getDefaultOptions(array $options)
{
$defaultOptions = array(
'em' => null,
'class' => null,
'property' => null,
'hidden' => false,
);
$options = array_replace($defaultOptions, $options);
return $options;
}
public function getParent(array $options)
{
if ($options['hidden']) {
return 'hidden';
}
return 'choice';
}
public function getName()
{
return 'null_entity';
}
}
The most interesting thing in this code is the registering of the custom DataTransformer ‘TextToIdTransformer’ which I’ll explain in the next section, but first we need to register this type into de Dependency Injection Container, for better and reusable code .
Create the needed service into Resources/config/services.xml file
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="form.type.null_entity">
<tag name="form.type" alias="null_entity"/>
<argument type="service" id="doctrine"/>
</service>
</services>
</container
We create a new service called form.type.null_entity, tagged as ‘form.type‘ to be recognised within the build form process, and inject doctrine service needed inside the class NullEntityType by the DataTransformer.
Create the following class into DependencyInjection folder to load the created service
class AcmeStoreExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
}
public function getAlias()
{
return 'acme_store';
}
}
This will load the service specified into the xml file.
Creating your own DataTransformer
A DataTransformer is at the time of this writing still undocumented (Symfony 2 API), basically what it does is to transform a value from its original representation to a transformed representation.
What it is for? It’s use, mainly is to serve as a channel of communication between how data must be shown in a browser and how data is stored/managed it the data class. To serve as an example, let’s think about dates, dates are shown in a client browser as select boxes or as simple as textbox, let’s get this last representation for our example. Once the form is submitted input data must be processed and stored as a DateTime PHP Object, so some transformation is needed back and forth.

DateTimeToLocalizedStringTransformer Data Flow Diagram
Why don’t use EntityType?
In the previous section we created an registered a new FormType called NullEntityType. As explained before EntityType which register EntityToIdTransformer as default DataTransformer retrieves data from the dataClass passed by parameter. This behaviour does not fit our needs as selectbox will be populated using jQuery every time the specified event triggers, that’s it when community select box changes.
Even more EntityToIdTransformer expects an EntityChoiceList as argument to get data from. So finally I ended up creating my own DataTransformer and naming it TextToIdTransformer which transform the Id returned by select box to its convenient object to be persisted.
class TextToIdTransformer implements DataTransformerInterface
{
protected $em;
protected $class;
protected $propertyPath;
public function __construct(EntityManager $em, $class, $property = null)
{
$this->em = $em;
$this->class = $class;
// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
}
}
public function transform($entity)
{
if (null === $entity || '' === $entity) {
return 'null';
}
if (!is_object($entity)) {
throw new UnexpectedTypeException($entity, 'object');
}
if ($this->propertyPath) {
// If the property option was given, use it
$value = $this->propertyPath->getValue($entity);
} else {
// Otherwise expect a __toString() method in the entity
$value = (string)$entity;
}
return $value;
}
public function reverseTransform($key)
{
if ('' === $key || null === $key) {
return null;
}
if (!is_string($key))
{
return null;
}
if (!is_numeric($key))
{
throw new UnexpectedTypeException($key, 'numeric');
}
$entity = $this->em->getRepository($this->class)->findOneById($key);
if ($entity === null) {
throw new TransformationFailedException(sprintf('The entity with key "%s" could not be found', $key));
}
return $entity;
}
}
Where jQuery gets into action
A bit of client code is needed to raise an event that fetches convenient data from database
<script type="text/javascript">
$(document).ready(function(){
$("#provider_location_community").change( function() {
$("#loader").show();
$.ajax({
type: "GET",
data: "data=" + $(this).val(),
url:"{{ path('_provinceByCommunity') }}",
success: function(msg){
if (msg != ''){
$("#provider_location_province").html(msg).show();
$('#provider_location_province option[value=' +
{{ provider.location.province.id is defined ? provider.location.province.id : '' }}
+']').attr("selected","selected");
$("#loader").hide();
}
else
{
$("#provider_location_province").html('<em>No item result</em>');
$("#loader").hide();
}
}
});
});
$("#provider_location_community").trigger('change');
});
</script>
Every time a new community is selected a new request to its convenient controller is done, in this case the controller build a HTML string and send it back as a response to be shown in the select box.
/**
* @Route("/provinceByCommunity", name="_provinceByCommunity")
*/
public function listProvinceByCommunityId()
{
$this->em = $this->get('doctrine')->getEntityManager();
$this->repository = $this->em->getRepository('AcmeStoreBundle:Province');
$communityId = $this->get('request')->query->get('data');
$provinces = $this->repository->findByCommunity($communityId);
if (empty($provinces)) {
return new Response('<option>No provinces found for that community</option>');
}
$html = '';
foreach($provinces as $province)
{
$html = $html . sprintf("<option value=\"%d\">%s</option>",$province->getId(), $province->getName());
}
return new Response($html);
}
Final Notes
We have reached the end of a long and quite complicated journey. As stated in the beginning of the post this is just one solution to this problem, and of course there are many of them, for sure much better and convenient, to find them and show them us is your task.
Feedback is appreciated.
Hope it helps.
Links