diff --git a/book/security.rst b/book/security.rst index 46f6579378d..c42a7f49934 100644 --- a/book/security.rst +++ b/book/security.rst @@ -438,7 +438,7 @@ Next, create the controller that will display the login form:: use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\SecurityContext; + use Symfony\Component\Security\Core\SecurityContextInterface; class SecurityController extends Controller { @@ -447,20 +447,20 @@ Next, create the controller that will display the login form:: $session = $request->getSession(); // get the login error if there is one - if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { + if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) { $error = $request->attributes->get( - SecurityContext::AUTHENTICATION_ERROR + SecurityContextInterface::AUTHENTICATION_ERROR ); } else { - $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); - $session->remove(SecurityContext::AUTHENTICATION_ERROR); + $error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR); + $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR); } return $this->render( 'AcmeSecurityBundle:Security:login.html.twig', array( // last username entered by the user - 'last_username' => $session->get(SecurityContext::LAST_USERNAME), + 'last_username' => $session->get(SecurityContextInterface::LAST_USERNAME), 'error' => $error, ) ); diff --git a/components/dependency_injection/factories.rst b/components/dependency_injection/factories.rst index f156d7a8fab..96df6429244 100644 --- a/components/dependency_injection/factories.rst +++ b/components/dependency_injection/factories.rst @@ -79,8 +79,8 @@ class: When you specify the class to use for the factory (via ``factory_class``) the method will be called statically. If the factory itself should be instantiated -and the resulting object's method called (as in this example), configure the -factory itself as a service: +and the resulting object's method called, configure the factory itself as a service. +In this case, the method (e.g. get) should be changed to be non-static: .. configuration-block:: diff --git a/components/form/form_events.rst b/components/form/form_events.rst new file mode 100644 index 00000000000..1b81360f529 --- /dev/null +++ b/components/form/form_events.rst @@ -0,0 +1,398 @@ +.. index:: + single: Forms; Form Events + +Form Events +=========== + +The Form component provides a structured process to let you customize your +forms, by making use of the :doc:`EventDispatcher ` +component. Using form events, you may modify information or fields at +different steps of the workflow: from the population of the form to the +submission of the data from the request. + +Registering an event listener is very easy using the Form component. + +For example, if you wish to register a function to the +``FormEvents::PRE_SUBMIT`` event, the following code lets you add a field, +depending on the request' values:: + + // ... + + use Symfony\Component\Form\FormEvent; + use Symfony\Component\Form\FormEvents; + + $listener = function (FormEvent $event) { + // ... + }; + + $form = $formFactory->createBuilder() + // add form fields + ->addEventListener(FormEvents::PRE_SUBMIT, $listener); + + // ... + +The Form Workflow +----------------- + +The Form Submission Workflow +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: /images/components/form/general_flow.png + :align: center + +1) Pre-populating the Form (``FormEvents::PRE_SET_DATA`` and ``FormEvents::POST_SET_DATA``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: /images/components/form/set_data_flow.png + :align: center + +Two events are dispatched during pre-population of a form, when +:method:`Form::setData() ` +is called: ``FormEvents::PRE_SET_DATA`` and ``FormEvents::POST_SET_DATA``. + +A) The ``FormEvents::PRE_SET_DATA`` Event +......................................... + +The ``FormEvents::PRE_SET_DATA`` event is dispatched at the beginning of the +``Form::setData()`` method. It can be used to: + +* Modify the data given during pre-population; +* Modify a form depending on the pre-populated data (adding or removing fields dynamically). + +:ref:`Form Events Information Table` + ++-----------------+-----------+ +| **Data type** | **Value** | ++-----------------+-----------+ +| Model data | ``null`` | ++-----------------+-----------+ +| Normalized data | ``null`` | ++-----------------+-----------+ +| View data | ``null`` | ++-----------------+-----------+ + +.. caution:: + + During ``FormEvents::PRE_SET_DATA``, + :method:`Form::setData() ` + is locked and will throw an exception if used. If you wish to modify + data, you should use + :method:`FormEvent::setData() ` + instead. + +.. sidebar:: ``FormEvents::PRE_SET_DATA`` in the Form component + + The ``collection`` form type relies on the + :class:`Symfony\\Component\\Form\\Extension\\Core\\EventListener\\ResizeFormListener` + subscriber, listening to the ``FormEvents::PRE_SET_DATA`` event in order + to reorder the form's fields depending on the data from the pre-populated + object, by removing and adding all form rows. + +B) The ``FormEvents::POST_SET_DATA`` Event +.......................................... + +The ``FormEvents::POST_SET_DATA`` event is dispatched at the end of the +:method:`Form::setData() ` +method. This event is mostly here for reading data after having pre-populated +the form. + +:ref:`Form Events Information Table` + ++-----------------+------------------------------------------------------+ +| **Data type** | **Value** | ++-----------------+------------------------------------------------------+ +| Model data | Model data injected into ``setData()`` | ++-----------------+------------------------------------------------------+ +| Normalized data | Model data transformed using a model transformer | ++-----------------+------------------------------------------------------+ +| View data | Normalized data transformed using a view transformer | ++-----------------+------------------------------------------------------+ + +.. sidebar:: ``FormEvents::POST_SET_DATA`` in the Form component + + The :class:`Symfony\\Component\\Form\\Extension\\DataCollector\\EventListener\\DataCollectorListener` + class is subscribed to listen to the ``FormEvents::POST_SET_DATA`` event + in order to collect information about the forms from the denormalized + model and view data. + +2) Submitting a Form (``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT`` and ``FormEvents::POST_SUBMIT``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: /images/components/form/submission_flow.png + :align: center + +Three events are dispatched when +:method:`Form::handleRequest() ` +or :method:`Form::submit() ` are +called: ``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT``, +``FormEvents::POST_SUBMIT``. + +A) The ``FormEvents::PRE_SUBMIT`` Event +....................................... + +The ``FormEvents::PRE_SUBMIT`` event is dispatched at the beginning of the +:method:`Form::submit() ` method. + +It can be used to: + +* Change data from the request, before submitting the data to the form. +* Add or remove form fields, before submitting the data to the form. + +:ref:`Form Events Information Table` + ++-----------------+------------------------------------------+ +| **Data type** | **Value** | ++-----------------+------------------------------------------+ +| Model data | Same as in ``FormEvents::POST_SET_DATA`` | ++-----------------+------------------------------------------+ +| Normalized data | Same as in ``FormEvents::POST_SET_DATA`` | ++-----------------+------------------------------------------+ +| View data | Same as in ``FormEvents::POST_SET_DATA`` | ++-----------------+------------------------------------------+ + +.. sidebar:: ``FormEvents::PRE_SUBMIT`` in the Form component + + The :class:`Symfony\\Component\\Form\\Extension\\Core\\EventListener\\TrimListener` + subscriber subscribes to the ``FormEvents::PRE_SUBMIT`` event in order to + trim the request's data (for string values). + The :class:`Symfony\\Component\\Form\\Extension\\Csrf\\EventListener\\CsrfValidationListener` + subscriber subscribes to the ``FormEvents::PRE_SUBMIT`` event in order to + validate the CSRF token. + +B) The ``FormEvents::SUBMIT`` Event +................................... + +The ``FormEvents::SUBMIT`` event is dispatched just before the +:method:`Form::submit() ` method +transforms back the normalized data to the model and view data. + +It can be used to change data from the normalized representation of the data. + +:ref:`Form Events Information Table` + ++-----------------+-------------------------------------------------------------------------------------+ +| **Data type** | **Value** | ++-----------------+-------------------------------------------------------------------------------------+ +| Model data | Same as in ``FormEvents::POST_SET_DATA`` | ++-----------------+-------------------------------------------------------------------------------------+ +| Normalized data | Data from the request reverse-transformed from the request using a view transformer | ++-----------------+-------------------------------------------------------------------------------------+ +| View data | Same as in ``FormEvents::POST_SET_DATA`` | ++-----------------+-------------------------------------------------------------------------------------+ + +.. caution:: + + At this point, you cannot add or remove fields to the form. + +.. sidebar:: ``FormEvents::SUBMIT`` in the Form component + + The :class:`Symfony\\Component\\Form\\Extension\\Core\\EventListener\\ResizeFormListener` + subscribes to the ``FormEvents::SUBMIT`` event in order to remove the + fields that need to be removed whenever manipulating a collection of forms + for which ``allow_delete`` has been enabled. + +C) The ``FormEvents::POST_SUBMIT`` Event +........................................ + +The ``FormEvents::POST_SUBMIT`` event is dispatched after the +:method:`Form::submit() ` once the +model and view data have been denormalized. + +It can be used to fetch data after denormalization. + +:ref:`Form Events Information Table` + ++-----------------+---------------------------------------------------------------+ +| **Data type** | **Value** | ++-----------------+---------------------------------------------------------------+ +| Model data | Normalized data reverse-transformed using a model transformer | ++-----------------+---------------------------------------------------------------+ +| Normalized data | Same as in ``FormEvents::POST_SUBMIT`` | ++-----------------+---------------------------------------------------------------+ +| View data | Normalized data transformed using a view transformer | ++-----------------+---------------------------------------------------------------+ + +.. caution:: + + At this point, you cannot add or remove fields to the form. + +.. sidebar:: ``FormEvents::POST_SUBMIT`` in the Form component + + The :class:`Symfony\\Component\\Form\\Extension\\DataCollector\\EventListener\\DataCollectorListener` + subscribes to the ``FormEvents::POST_SUBMIT`` event in order to collect + information about the forms. + The :class:`Symfony\\Component\\Form\\Extension\\Validator\\EventListener\\ValidationListener` + subscribes to the ``FormEvents::POST_SUBMIT`` event in order to + automatically validate the denormalized object, and update the normalized + as well as the view's representations. + +Registering Event Listeners or Event Subscribers +------------------------------------------------ + +In order to be able to use Form events, you need to create an event listener +or an event subscriber, and register it to an event. + +The name of each of the "form" events is defined as a constant on the +:class:`Symfony\\Component\\Form\\FormEvents` class. +Additionally, each event callback (listener or subscriber method) is passed a +single argument, which is an instance of +:class:`Symfony\\Component\\Form\\FormEvent`. The event object contains a +reference to the current state of the form, and the current data being +processed. + +.. _component-form-event-table: + ++--------------------+-------------------------------+------------------+ +| **Name** | ``FormEvents`` **Constant** | **Event's data** | ++--------------------+-------------------------------+------------------+ +| form.pre_set_data | ``FormEvents::PRE_SET_DATA`` | Model data | ++--------------------+-------------------------------+------------------+ +| form.post_set_data | ``FormEvents::POST_SET_DATA`` | Model data | ++--------------------+-------------------------------+------------------+ +| form.pre_bind | ``FormEvents::PRE_SUBMIT`` | Request data | ++--------------------+-------------------------------+------------------+ +| form.bind | ``FormEvents::SUBMIT`` | Normalized data | ++--------------------+-------------------------------+------------------+ +| form.post_bind | ``FormEvents::POST_SUBMIT`` | View data | ++--------------------+-------------------------------+------------------+ + +.. versionadded:: 2.3 + + Before Symfony 2.3, ``FormEvents::PRE_SUBMIT``, ``FormEvents::SUBMIT`` + and ``FormEvents::POST_SUBMIT`` were called ``FormEvents::PRE_BIND``, + ``FormEvents::BIND`` and ``FormEvents::POST_BIND``. + +.. caution:: + + The ``FormEvents::PRE_BIND``, ``FormEvents::BIND`` and + ``FormEvents::POST_BIND`` constants will be removed in version 3.0 of + Symfony. + The event names still keep their original values, so make sure you use the + ``FormEvents`` constants in your code for forward compatibility. + +Event Listeners +~~~~~~~~~~~~~~~ + +An event listener may be any type of valid callable. + +Creating and binding an event listener to the form is very easy:: + + // ... + + use Symfony\Component\Form\FormEvent; + use Symfony\Component\Form\FormEvents; + + $form = $formFactory->createBuilder() + ->add('username', 'text') + ->add('show_email', 'checkbox') + ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $user = $event->getData(); + $form = $event->getForm(); + + if (!$user) { + return; + } + + // Check whether the user has chosen to display his email or not. + // If the data was submitted previously, the additional value that is + // included in the request variables needs to be removed. + if (true === $user['show_email']) { + $form->add('email', 'email'); + } else { + unset($user['email']); + $event->setData($user); + } + }) + ->getForm(); + + // ... + +When you have created a form type class, you can use one of its methods as a +callback for better readability:: + + // ... + + class SubscriptionType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->add('username', 'text'); + $builder->add('show_email', 'checkbox'); + $builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData')); + } + + public function onPreSetData(FormEvent $event) + { + // ... + } + } + +Event Subscribers +~~~~~~~~~~~~~~~~~ + +Event subscribers have different uses: + +* Improving readability; +* Listening to multiple events; +* Regrouping multiple listeners inside a single class. + +.. code-block:: php + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\Form\FormEvent; + use Symfony\Component\Form\FormEvents; + + class AddEmailFieldListener implements EventSubscriberInterface + { + public function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => 'onPreSetData', + FormEvents::PRE_SUBMIT => 'onPreSubmit', + ); + } + + public function onPreSetData(FormEvent $event) + { + $user = $event->getData(); + $form = $event->getForm(); + + // Check whether the user from the initial data has chosen to + // display his email or not. + if (true === $user->isShowEmail()) { + $form->add('email', 'email'); + } + } + + public function onPreSubmit(FormEvent $event) + { + $user = $event->getData(); + $form = $event->getForm(); + + if (!$user) { + return; + } + + // Check whether the user has chosen to display his email or not. + // If the data was submitted previously, the additional value that + // is included in the request variables needs to be removed. + if (true === $user['show_email']) { + $form->add('email', 'email'); + } else { + unset($user['email']); + $event->setData($user); + } + } + } + +To register the event subscriber, use the addEventSubscriber() method:: + + // ... + + $form = $formFactory->createBuilder() + ->add('username', 'text') + ->add('show_email', 'checkbox') + ->addEventSubscriber(new AddEmailFieldListener()) + ->getForm(); + + // ... diff --git a/components/form/index.rst b/components/form/index.rst index 5ea5add00f6..a2de93c6245 100644 --- a/components/form/index.rst +++ b/components/form/index.rst @@ -6,3 +6,4 @@ introduction type_guesser + form_events diff --git a/components/map.rst.inc b/components/map.rst.inc index 9eb981425e3..49c1ee10249 100644 --- a/components/map.rst.inc +++ b/components/map.rst.inc @@ -79,6 +79,7 @@ * :doc:`/components/form/index` * :doc:`/components/form/introduction` + * :doc:`/components/form/form_events` * :doc:`/components/form/type_guesser` * :doc:`/components/http_foundation/index` diff --git a/cookbook/form/dynamic_form_modification.rst b/cookbook/form/dynamic_form_modification.rst index c100fb52210..97a33d49118 100644 --- a/cookbook/form/dynamic_form_modification.rst +++ b/cookbook/form/dynamic_form_modification.rst @@ -24,6 +24,10 @@ Example: on a registration form, you have a "country" field and a "state" field which should populate dynamically based on the value in the "country" field. +If you wish to learn more about the basics behind form events, you can +take a look at the :doc:`Form Events ` +documentation. + .. _cookbook-form-events-underlying-data: Customizing your Form based on the underlying Data @@ -129,25 +133,6 @@ the event listener might look like the following:: }); } -.. note:: - You can of course use any callback type instead of a closure, e.g. a method - call on the ``ProductType`` object itself for better readability:: - - // ... - class ProductType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - // ... - $builder->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData')); - } - - public function onPreSetData(FormEvent $event) - { - // ... - } - } - .. note:: The ``FormEvents::PRE_SET_DATA`` line actually resolves to the string diff --git a/images/components/form/general_flow.png b/images/components/form/general_flow.png new file mode 100644 index 00000000000..31650e52af6 Binary files /dev/null and b/images/components/form/general_flow.png differ diff --git a/images/components/form/set_data_flow.png b/images/components/form/set_data_flow.png new file mode 100644 index 00000000000..3cd4b1e2f7b Binary files /dev/null and b/images/components/form/set_data_flow.png differ diff --git a/images/components/form/submission_flow.png b/images/components/form/submission_flow.png new file mode 100644 index 00000000000..a3c6e9cfb90 Binary files /dev/null and b/images/components/form/submission_flow.png differ