PHP & Symfony About PHP and Symfony2 development

Symfony2: Defining and dispatching custom form events

Posted on by Matthias Noback

The Symfony Form Component has an architecture that keeps inspiring me. It supports horizontal and vertical inheritance and it has a solid event system on a very fine-grained level. In this post I would like to explain how you can expand the event system with your own events. As an example., I will create a new event type used for indicating that the bound form is valid.

Using event listeners and event subscribers with forms

As you can read in a Symfony Cookbook article you can already hook into special form events, defined in Symfony\Component\Form\FormEvents:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;

class CustomFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
            // respond to the event, modify data, or form elements

            $data = $event->getData();
            $form = $event->getForm();
        });
    }
}

When the set of events you want to listen to does not have to be determined dynamically, you could also use an event subscriber instead of an event listener:

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CustomSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'preSetData'
        );
    }

    public function preSetData(FormEvent $event)
    {
        $data = $event->getData();
        $form = $event->getForm();

        // ...
    }
}

Registering an event subscriber goes like this:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventSubscriber(new CustomSubscriber());
}

Every form element has its own dispatcher

The nice thing is: event listeners and event subscribers work on the level of a form element, which could be the main form object, or any specific form field in the tree of form elements. This allows you to be very specific in your actions inside the listener itself.

Each form element has its own event dispatcher, due to this line in ResolvedFormType::createBuilder():

$builder = new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options);

This builder will be passed around as the argument for the buildForm() methods. And every call to addEventSubscriber() or addEventListener() will result in an extra event subscriber or listener on this little event dispatcher object.

After a form has been fully resolved, you can still retrieve its event dispatcher:

$eventDispatcher = $form->getConfig()->getEventDispatcher();

Firing custom events on form elements

With the event dispatcher at our service, we can now fire any event we like on form elements. As I mentioned, I want to be able to notify event subscribers of the fact that the bound form has been validated. This means I need an event class and an event name to dispatch. The event class could be:

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Form\FormInterface;

class ValidFormEvent extends Event
{
    private $form;

    public function __construct(FormInterface $form)
    {
        $this->form = $form;
    }

    public function getForm()
    {
        return $this->form;
    }
}

I choose the event name valid_form and then inside a controller we can do:

if ($form->isValid()) {
    $eventDispatcher = $form->getConfig()->getEventDispatcher();
    $eventDispatcher->dispatch('valid_form', new ValidFormEvent($form));
}

This will have no effect whatsoever, until we register a listener in a form type:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder->addEventListener('valid_form', function(FormEvent $event) {
        // ...
    }
}

Propagating custom events

As I already mentioned, each form element has its own event dispatcher. This means that the dispatch() call issued in the code above will not reach any child form elements and therefore may still have no effect, only for the root form element.

There is a simple trick for this - use an event subscriber which merely propagates the custom form event:

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormInterface;

class PropagateValidFormEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            'form_valid' => 'onFormValid',
        );
    }

    public function onFormValid(FormValidEvent $event)
    {
        $form = $event->getForm();

        foreach ($form as $child) {
            /** @var $child FormInterface */

            $childEventDispatcher = $child->getConfig()->getEventDispatcher();
            $childEventDispatcher->dispatch('form_valid', new FormValidEvent($child));
        }
    }
}

We simple recreate the event for child elements and dispatch it using the event dispatcher of the child element.

Now we only have to make sure that every form element has this event subscriber by default, since we don't want to (and can't) call

$builder->addEventSubscriber(new PropagateValidFormEventSubscriber());

in all buildForm() methods.

Creating a form type extension for registering event subscribers

Luckily, the Form Component has support for form type extensions, which means that we can modify a form type's behavior at all different levels of abstraction (this is the "horizontal inheritance" part), without requiring other form types to have a different parent form type. A form type extension looks very much like a normal form type, except that its buildForm() method will be called for all form elements whose type is (either directly or in the hierarchy of parent types) the type returned by getExtendedType(). This allows us to add the omnipresent PropagateValidFormEventSubscriber to the event dispatcher of each and every form.

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;

class ValidFormEventTypeExtension extends AbstractTypeExtension
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->addEventSubscriber(new PropagateValidFormEventSubscriber());
    }

    public function getExtendedType()
    {
        return 'form';
    }
}

To make the type extension work, register this class as a service with a tag form.type_extension and an alias attribute with the value "form".

From now on, you can happily dispatch the form_valid event on the root form element's event dispatcher, and it will be automatically propagated to all child form elements.

Categories: PHP Symfony2

Tags: events forms

Comments: Comments