With Symfony2, many things are managed through dependency injection. Except for forms. Oh, wait, forms can be services too of course! Remember? Any class instance can be a service... Now, as in many other cases, Symfony2's FrameworkBundle
adds some magic to the Form component by creating services that link several parts of the Form component together. For example, depending on the value of the framework.csrf_protection
parameter in config.yml
file a hidden field "_token" will be added to all forms, site-wide. To create this kind of very general "form extension" in your own project, you need to do just a few things (I am going to use the example of a "Captcha" form extension, but I don't give a full implementation of this idea).
First create a file /src/Acme/DemoBundle/Form/Extension/FormTypeCaptchaExtension.php
containing the FormTypeCaptchaExtension
:
namespace Acme\DemoBundle\Form\Extension;
use Acme\DemoBundle\Form\EventListener\EnsureCaptchaFieldListener;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormBuilder;
class FormTypeCaptchaExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilder $builder, array $options)
{
if (!$options['captcha_enabled']) {
return;
}
// you may add fields or event listeners to the form here
}
public function getExtendedType()
{
return 'form'; // we extend the general "form" type, not some specific form
}
public function getDefaultOptions(array $options)
{
return array(
'captcha_enabled' => false, // we don't want all forms to have a captcha field
'captcha_field_name' => '_captcha'
);
}
}
Then, we should make the new form type extension known to the service container by adding a few lines to /src/Acme/DemoBundle/Resources/config/services.xml
:
<?xml version="1.0" ?>
<container xmlns="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="acme.form.extension.captcha" class="Acme\DemoBundle\Form\Extension\FormTypeCaptchaExtension">
<tag name="form.type_extension" alias="form" />
</service>
</services>
</container>
Though the extension doesn't do anything yet, we already have the option "captcha_enabled" available in all forms of the application.
In many cases we will need to add event listeners to the form, because we want to hook into certain events, like "pre bind" or "post data". You will find the available event types in the FormEvents class.
As you can see in the CSRF form extension an event listener is used to embed a special form for the CSRF token in an existing form. This can be done by first creating an event listener, which handles this special field (in our case, it ensures the existence of the CSRF form):
class FormTypeCaptchaExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilder $builder, array $options)
{
if (!$options['captcha_enabled']) {
return;
}
$listener = new EnsureCaptchaFieldListener(
$builder->getFormFactory(),
$options['captcha_field_name']
);
$builder
->setAttribute('captcha_field_name', $options['captcha_field_name'])
->addEventListener(FormEvents::PRE_SET_DATA, array($listener, 'ensureCaptchaField'), -10)
->addEventListener(FormEvents::PRE_BIND, array($listener, 'ensureCaptchaField'), -10)
;
}
}
As you can see, we listen to pre_set_data
and pre_bind
events, and we make sure we are called late in the process (by adding a low priority of -10). Now we should materialize the EnsureCaptchaFieldListener
, for example in /src/Acme/DemoBundle/Form/EventListener/EnsureCaptchaFieldListener.php
namespace Acme\DemoBundle\Form\EventListener;
use Symfony\Component\Form\Event\DataEvent;
use Symfony\Component\Form\FormFactoryInterface;
class EnsureCaptchaFieldListener
{
private $factory;
private $name;
public function __construct(FormFactoryInterface $factory, $name)
{
$this->factory = $factory;
$this->name = $name;
}
public function ensureCaptchaField(DataEvent $event)
{
$form = $event->getForm();
$form->add($this->factory->createNamed('captcha', $this->name, null, array()));
}
}
We inject the form factory into the listener, so it can create the captcha form and embed it into the form.
One important thing to take notice of is the first argument of the createNamed
method. This argument is in fact an alias for a service we shall soon create. The service will be a FormType
, and it will contain the field(s) responsible for adding some kind of captcha functionality to a form.
Let's first create a CaptchaType
form in for example /src/Acme/DemoBundle/Form/CaptchaType.php
:
<?php
namespace Acme\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class CaptchaType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('verify_captcha', 'text', array('label' => 'Copy the characters from the image'));
}
public function getName()
{
return 'captcha';
}
}
As I said before, this CaptchaType
will be a service, so let's add to the list of service definitions:
<service id="form.type.captcha" class="Acme\DemoBundle\Form\CaptchaType">
<tag name="form.type" alias="captcha" />
</service>
For now, you don't have any real benefits from making the CaptchaType
a service, but, you will, once you need to inject dependencies, for instance a captcha field may well depend on some data stored in the session...
Thank Matthias, was looking for something really quick on this topic and your blog post was the most efficient explanation online, even if it's from 2011.
Many thanks for taking the time to share it.
I see that all types that extend AbstractType have 'form' as their parent. Using 'form' as an alias therefore works when you ant to add functionality to all these types. However, the button type overrides the getParent method to return nothing. That is why form extension with the 'form' alias do not work for buttons. Is there a reason for that?
Hi Matthias !
Very interesting post ! I have tried to follow your example to use it in a similar situation. But I encountered a problem. When adding a new field with the listener to a simple form, an exception is thrown: "You cannot add children to a simple form. Maybe you should set the option "compound" to true?" So for example, I have tried to add the option compound to a text field or a checkbox field to make the exception disappear. Then there is no exception but when rendering the form in the template with:
{{ form_widget(form) }}
{{ form_rest(form) }}
I can see fields added with the listener, but not the original widget for the text field or the checkbox. I think I misunderstood the way option 'compound' works. Do you have any clue why this is occuring ? Thanks in advance for your reply.
In the extension wrap the form->add with:
if ($form->isRoot() && $form->getConfig()->getOption('compound')) { … }