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.
Thank you for this post! We still use this DI. In our cases each form is defined as service and the EventSubscriber will be injected. We use POST_SUBMIT and the PRE_SET_DATA Event. But how can we use a data transformer into the dispatcher? I need to pre set data on a field which use a ModelTransformer. It seems it's only possible with the standard form hooks.
For Example
In my UserFormType:
$customerConfiguration = [
'label' => 'customer_field'
];
$builder->add(
$builder
->create('customer', 'text', $customerConfiguration)
->addModelTransformer($customerTransformer)
)
$builder->addEventSubscriber($this->userFormSubscriber);
In my UserFormSubscriber onPreSetData:
$customerField = $form->get('customer');
if ($user instanceof FOO) {
$customerField->setData('12345');
}
But no data is set to customer :(
Very useful and detailed tips. Thanks !
Ok. But only the root form "is valid". Does I miss something ?
isValid()
loops over all child forms, so if$form->isValid()
returns true, all subforms are valid also (it would not be so useful otherwise ;)).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 alwaysfalse
. Only the root form know that.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 returnfalse
. It will returntrue
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.
Thanks for shining some light on this issue, Christophe.
Are you sure that
$children->isValid()
will always returnfalse
? 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.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/...
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.
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).
Hi Bernhard,
Thanks for your suggestion. I did not know of the existence of
POST_SUBMIT
. Very nice!Hi Bernhard,
I know this post is rather old but could you please explain your suggestion a little more or link to some information about this. If I register a POST_SUBMIT EventSubscriber on a child form, it gets triggered before the parent form gets validated.
So what is the cleanest way to execute some action on a child form after the parent has been validated. Thanks!
POST_SUBMIT was called POST_BIND up until 2.3 :)
Haha, right. I understand why this would make things much simpler. Dispatching a form event from within a controller is not an ideal solution ;)