There is also an interesting Cookbook article about the same subject (not written by me).

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="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="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...

PHP Symfony2 dependency injection events forms

I have removed Disqus from this website, so for now, you can't comment on articles. In the near future you will be able to send comments by email. For now, if you want to say something, you can always send an email to me personally: info@matthiasnoback.nl.

Comments
Vincent

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.

leevigraham

In the extension wrap the form->add with:

if ($form->isRoot() && $form->getConfig()->getOption('compound')) { … }

disqus_QIG5PTrga5

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?

ludofleury

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.