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