Symfony2: Defining and Dispatching Custom Form Events

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.

Posted in Events, Forms, Symfony2 | 14 Comments

14 Responses to Symfony2: Defining and Dispatching Custom Form Events

  • # May 6, 2013 at 09:25

    I did not check, but I think if you register a form type extension on “form”, the extension will be call on ALL form type as every type is a form type.

    So for a form with 4 inputs, the event will be fire 4 (inputs) + 1 (main form) times.

    You should check if there is not parent before executing some action. see: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php#L96

    Greg

    Reply

    • # May 7, 2013 at 21:29

      Hi Greg,
      That’s right, and it is how I intended it to be: I needed the event to “reach” all subforms/fields of the main form.
      When some code only has to be executed for the main form, you should indeed check for the none-existing parent.

      Matthias Noback

      Reply

      • # May 29, 2013 at 12:47

        You can simplify your solution by making the subscriber listen to POST_SUBMIT with a very low priority. Once it is called, check whether $event->getForm()->isValid() and fire your custom event in that case.

        Since POST_SUBMIT is triggered on every form, there is no need to recursively trigger the event.

        However, you should take the performance implications of events into account. In most cases, a single event listener that listens to POST_SUBMIT and does the isValid() check itself is a better solution (since faster) than two listeners, one for POST_SUBMIT and another for the custom event (given that the listeners are present on every instance in the form tree).

        Bernhard

        Reply

        • # May 29, 2013 at 13:17

          Hi Bernhard,
          Thanks for your suggestion. I did not know of the existence of POST_SUBMIT. Very nice!

          Matthias Noback

          Reply

          • # May 29, 2013 at 13:28

            POST_SUBMIT was called POST_BIND up until 2.3 :)

            Bernhard

          • # May 29, 2013 at 13:39

            Haha, right. I understand why this would make things much simpler. Dispatching a form event from within a controller is not an ideal solution ;)

            Matthias Noback

  • # May 7, 2013 at 22:40

    Ok. But only the root form “is valid”. Does I miss something ?

    Greg

    Reply

    • # May 10, 2013 at 15:50

      isValid() loops over all child forms, so if $form->isValid() returns true, all subforms are valid also (it would not be so useful otherwise ;) ).

      Matthias Noback

      Reply

      • # May 10, 2013 at 22:36

        Yes. I agree. But the event Is triggered for each form (and so sub form).
        So IMHO, you should do

        1. check if it is the root form
        2. check if it Is valid
        3. trigger your personnal event.

        More over, call children->isvalid() return always false. Only the root form know that.

        Greg

        Reply

        • # May 12, 2013 at 11:44

          Are you sure that $children->isValid() will always return false? Anyhow, if I only dispatch the custom event in the root form, and only when it is valid, it will not propagate to all other fields, like I wanted to. My assumption here is that when the root form is valid, all subforms are valid too.

          Matthias Noback

          Reply

        • # May 29, 2013 at 16:12

          Any form can be valid. A form is valid if it has no error and all its children are valid.
          children->isvalid() will not always return false. It will return true more often than on the root.

          So you can call isValid on any element of the form tree. However, a children can be valid without the root being valid (if a sibling is invalid). So if you want to do something only when the whole form is valid, you can only do it on the root form.

          Stof

          Reply

          • # May 29, 2013 at 19:07

            Thanks for shining some light on this issue, Christophe.

            Matthias Noback

  • Pingback: Symfony2 components overview: EventDispatcher | ServerGrove

  • # December 9, 2013 at 16:45

    Very useful and detailed tips. Thanks !

    Adrian

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

* *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>