diff --git a/book/controller.rst b/book/controller.rst index d5869219282..06422cf8351 100644 --- a/book/controller.rst +++ b/book/controller.rst @@ -325,7 +325,7 @@ working with forms, for example:: { $form = $this->createForm(...); - $form->bind($request); + $form->handleRequest($request); // ... } @@ -663,7 +663,8 @@ For example, imagine you're processing a form submit:: { $form = $this->createForm(...); - $form->bind($this->getRequest()); + $form->handleRequest($this->getRequest()); + if ($form->isValid()) { // do some sort of processing diff --git a/book/forms.rst b/book/forms.rst index 272eae5e86e..f9c7e69e8fc 100644 --- a/book/forms.rst +++ b/book/forms.rst @@ -145,35 +145,27 @@ helper functions: .. code-block:: html+jinja {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} -
- {{ form_widget(form) }} - - -
+ {{ form(form) }} .. code-block:: html+php -
enctype($form) ?> > - widget($form) ?> - - -
+ form($form) ?> .. image:: /images/book/form-simple.png :align: center .. note:: - This example assumes that you've created a route called ``task_new`` - that points to the ``AcmeTaskBundle:Default:new`` controller that - was created earlier. + This example assumes that you submit the form in a "POST" request and to + the same URL that it was displayed in. You will learn later how to + change the request method and the target URL of the form. -That's it! By printing ``form_widget(form)``, each field in the form is -rendered, along with a label and error message (if there is one). As easy -as this is, it's not very flexible (yet). Usually, you'll want to render each -form field individually so you can control how the form looks. You'll learn how -to do that in the ":ref:`form-rendering-template`" section. +That's it! By printing ``form(form)``, each field in the form is rendered, along +with a label and error message (if there is one). As easy as this is, it's not +very flexible (yet). Usually, you'll want to render each form field individually +so you can control how the form looks. You'll learn how to do that in the +":ref:`form-rendering-template`" section. Before moving on, notice how the rendered ``task`` input field has the value of the ``task`` property from the ``$task`` object (i.e. "Write a blog post"). @@ -201,7 +193,7 @@ Handling Form Submissions The second job of a form is to translate user-submitted data back to the properties of an object. To make this happen, the submitted data from the -user must be bound to the form. Add the following functionality to your +user must be written into the form. Add the following functionality to your controller:: // ... @@ -217,53 +209,54 @@ controller:: ->add('dueDate', 'date') ->getForm(); - if ($request->isMethod('POST')) { - $form->bind($request); + $form->handleRequest($request); - if ($form->isValid()) { - // perform some action, such as saving the task to the database + if ($form->isValid()) { + // perform some action, such as saving the task to the database - return $this->redirect($this->generateUrl('task_success')); - } + return $this->redirect($this->generateUrl('task_success')); } // ... } -.. versionadded:: 2.1 - The ``bind`` method was made more flexible in Symfony 2.1. It now accepts - the raw client data (same as before) or a Symfony Request object. This - is preferred over the deprecated ``bindRequest`` method. - -Now, when submitting the form, the controller binds the submitted data to the -form, which translates that data back to the ``task`` and ``dueDate`` properties -of the ``$task`` object. This all happens via the ``bind()`` method. - -.. note:: - - As soon as ``bind()`` is called, the submitted data is transferred - to the underlying object immediately. This happens regardless of whether - or not the underlying data is actually valid. +.. versionadded:: 2.3 + The :method:`Symfony\Component\Form\FormInterface::handleRequest` method was + added in Symfony 2.3. Before you had to do some manual work to achieve the + same result. This controller follows a common pattern for handling forms, and has three possible paths: -#. When initially loading the page in a browser, the request method is ``GET`` - and the form is simply created and rendered; +#. When initially loading the page in a browser, the form is simply created and + rendered. :method:`Symfony\Component\Form\FormInterface::handleRequest` + recognizes that the form was not submitted and does nothing. + :method:`Symfony\Component\Form\FormInterface::isValid` returns ``false`` + if the form was not submitted. -#. When the user submits the form (i.e. the method is ``POST``) with invalid - data (validation is covered in the next section), the form is bound and - then rendered, this time displaying all validation errors; +#. When the user submits the form, :method:`Symfony\Component\Form\FormInterface::handleRequest` + recognizes this and immediately writes the submitted data back into the + ``task`` and ``dueDate`` properties of the ``$task`` object. Then this object + is validated. If it is invalid (validation is covered in the next section), + :method:`Symfony\Component\Form\FormInterface::isValid` returns ``false`` + again, so the form is rendered together with all validation errors; -#. When the user submits the form with valid data, the form is bound and - you have the opportunity to perform some actions using the ``$task`` - object (e.g. persisting it to the database) before redirecting the user - to some other page (e.g. a "thank you" or "success" page). + .. note:: -.. note:: + You can use the method :method:`Symfony\Component\Form\FormInterface::isBound` + to check whether a form was submitted, regardless of whether or not the + submitted data is actually valid. + +#. When the user submits the form with valid data, the submitted data is again + written into the form, but this time :method:`Symfony\Component\Form\FormInterface::isValid` + returns ``true``. Now you have the opportunity to perform some actions using + the ``$task`` object (e.g. persisting it to the database) before redirecting + the user to some other page (e.g. a "thank you" or "success" page). - Redirecting a user after a successful form submission prevents the user - from being able to hit "refresh" and re-post the data. + .. note:: + + Redirecting a user after a successful form submission prevents the user + from being able to hit "refresh" and re-post the data. .. index:: single: Forms; Validation @@ -429,7 +422,7 @@ to an array callback, or a ``Closure``:: } This will call the static method ``determineValidationGroups()`` on the -``Client`` class after the form is bound, but before validation is executed. +``Client`` class after the form is submitted, but before validation is executed. The Form object is passed as an argument to that method (see next example). You can also define whole logic inline by using a Closure:: @@ -605,35 +598,30 @@ of code. Of course, you'll usually need much more flexibility when rendering: .. code-block:: html+jinja {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} -
+ {{ form_start(form) }} {{ form_errors(form) }} {{ form_row(form.task) }} {{ form_row(form.dueDate) }} - {{ form_rest(form) }} - -
+ {{ form_end(form) }} .. code-block:: html+php -
enctype($form) ?>> + start($form) ?> errors($form) ?> row($form['task']) ?> row($form['dueDate']) ?> - rest($form) ?> - -
+ end($form) ?> Take a look at each part: -* ``form_enctype(form)`` - If at least one field is a file upload field, this - renders the obligatory ``enctype="multipart/form-data"``; +* ``form_start(form)`` - Renders the start tag of the form. * ``form_errors(form)`` - Renders any errors global to the whole form (field-specific errors are displayed next to each field); @@ -642,10 +630,8 @@ Take a look at each part: form widget for the given field (e.g. ``dueDate``) inside, by default, a ``div`` element; -* ``form_rest(form)`` - Renders any fields that have not yet been rendered. - It's usually a good idea to place a call to this helper at the bottom of - each form (in case you forgot to output a field or don't want to bother - manually rendering hidden fields). This helper is also useful for taking +* ``form_end()`` - Renders the end tag of the form and any fields that have not + yet been rendered. This is useful for rendering hidden fields and taking advantage of the automatic :ref:`CSRF Protection`. The majority of the work is done by the ``form_row`` helper, which renders @@ -740,7 +726,7 @@ field: .. code-block:: html+jinja - {{ form_widget(form.task, { 'attr': {'class': 'task_field'} }) }} + {{ form_widget(form.task, {'attr': {'class': 'task_field'}}) }} .. code-block:: html+php @@ -783,6 +769,75 @@ available in the :doc:`reference manual`. Read this to know everything about the helpers available and the options that can be used with each. +.. index:: + single: Forms; Changing the action and method + +.. _book-forms-changing-action-and-method: + +Changing the Action and Method of a Form +---------------------------------------- + +So far, we have used the ``form_start()`` helper to render the form's start tag +and assumed that each form is submitted to the same URL in a POST request. +Sometimes you want to change these parameters. You can do so in a few different +ways. If you build your form in the controller, you can use ``setAction()`` and +``setMethod()``:: + + $form = $this->createFormBuilder($task) + ->setAction($this->generateUrl('target_route')) + ->setMethod('GET') + ->add('task', 'text') + ->add('dueDate', 'date') + ->getForm(); + +.. note:: + + This example assumes that you've created a route called ``target_route`` + that points to the controller that processes the form. + +In :ref:`book-form-creating-form-classes` you will learn how to outsource the +form building code into separate classes. When using such a form class in the +controller, you can pass the action and method as form options:: + + $form = $this->createForm(new TaskType(), $task, array( + 'action' => $this->generateUrl('target_route'), + 'method' => 'GET', + )); + +At last, you can override the action and method in the template by passing them +to the ``form()`` or the ``form_start()`` helper: + +.. configuration-block:: + + .. code-block:: html+jinja + + {# src/Acme/TaskBundle/Resources/views/Default/new.html.twig #} + {{ form(form, {'action': path('target_route'), 'method': 'GET'}) }} + + {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} + + .. code-block:: html+php + + + form($form, array( + 'action' => $view['router']->generate('target_route'), + 'method' => 'GET', + )) ?> + + start($form, array( + 'action' => $view['router']->generate('target_route'), + 'method' => 'GET', + )) ?> + +.. note:: + + If the form's method is not GET or POST, but PUT, PATCH or DELETE, Symfony2 + will insert a hidden field with the name "_method" that stores this method. + The form will be submitted in a normal POST request, but Symfony2's router + is capable of detecting the "_method" parameter and will interpret the + request as PUT, PATCH or DELETE request. Read the cookbook chapter + ":doc:`/cookbook/routing/method_parameters`" for more information. + .. index:: single: Forms; Creating form classes @@ -912,7 +967,7 @@ you can fetch it from the form:: For more information, see the :doc:`Doctrine ORM chapter`. -The key thing to understand is that when the form is bound, the submitted +The key thing to understand is that when the form is submitted, the submitted data is transferred to the underlying object immediately. If you want to persist that data, you simply need to persist the object itself (which already contains the submitted data). @@ -1143,7 +1198,7 @@ renders the form: {% form_theme form 'AcmeTaskBundle:Form:fields.html.twig' 'AcmeTaskBundle:Form:fields2.html.twig' %} -
+ {{ form(form) }} .. code-block:: html+php @@ -1152,7 +1207,7 @@ renders the form: setTheme($form, array('AcmeTaskBundle:Form', 'AcmeTaskBundle:Form')) ?> - + form($form) ?> The ``form_theme`` tag (in Twig) "imports" the fragments defined in the given template and uses them when rendering the form. In other words, when the @@ -1231,7 +1286,7 @@ are 4 possible *parts* of a form that can be rendered: .. note:: - There are actually 3 other *parts* - ``rows``, ``rest``, and ``enctype`` - + There are actually 2 other *parts* - ``rows`` and ``rest`` - but you should rarely if ever need to worry about overriding them. By knowing the field type (e.g. ``textarea``) and which part you want to @@ -1486,12 +1541,12 @@ an array of the submitted data. This is actually really easy:: ->add('message', 'textarea') ->getForm(); - if ($request->isMethod('POST')) { - $form->bind($request); + $form->handleRequest($request); - // data is an array with "name", "email", and "message" keys - $data = $form->getData(); - } + if ($form->isBound()) { + // data is an array with "name", "email", and "message" keys + $data = $form->getData(); + } // ... render the form } @@ -1526,15 +1581,15 @@ Adding Validation The only missing piece is validation. Usually, when you call ``$form->isValid()``, the object is validated by reading the constraints that you applied to that -class. If your form is binding to an object (i.e. you're using the ``data_class`` +class. If your form is mapped to an object (i.e. you're using the ``data_class`` option or passing an object to your form), this is almost always the approach you want to use. See :doc:`/book/validation` for more details. .. _form-option-constraints: -But if you're not binding to an object and are instead retrieving a simple -array of your submitted data, how can you add constraints to the data of your -form? +But if the form is not mapped to an object and you instead want to retrieve a +simple array of your submitted data, how can you add constraints to the data of +your form? The answer is to setup the constraints yourself, and attach them to the individual fields. The overall approach is covered a bit more in the :ref:`validation chapter`, diff --git a/book/validation.rst b/book/validation.rst index 42e1ce8ff0b..717a3daa969 100644 --- a/book/validation.rst +++ b/book/validation.rst @@ -220,14 +220,12 @@ workflow looks like the following from inside a controller:: $author = new Author(); $form = $this->createForm(new AuthorType(), $author); - if ($request->isMethod('POST')) { - $form->bind($request); + $form->handleRequest($request); - if ($form->isValid()) { - // the validation passed, do something with the $author object + if ($form->isValid()) { + // the validation passed, do something with the $author object - return $this->redirect($this->generateUrl(...)); - } + return $this->redirect($this->generateUrl(...)); } return $this->render('BlogBundle:Author:form.html.twig', array( diff --git a/cookbook/doctrine/file_uploads.rst b/cookbook/doctrine/file_uploads.rst index 34778c3c0aa..ff1d89b58be 100644 --- a/cookbook/doctrine/file_uploads.rst +++ b/cookbook/doctrine/file_uploads.rst @@ -227,56 +227,28 @@ The following controller shows you how to handle the entire process:: /** * @Template() */ - public function uploadAction() + public function uploadAction(Request $request) { $document = new Document(); $form = $this->createFormBuilder($document) ->add('name') ->add('file') - ->getForm() - ; + ->getForm(); - if ($this->getRequest()->isMethod('POST')) { - $form->bind($this->getRequest()); - if ($form->isValid()) { - $em = $this->getDoctrine()->getManager(); + $form->handleRequest($request); - $em->persist($document); - $em->flush(); + if ($form->isValid()) { + $em = $this->getDoctrine()->getManager(); - return $this->redirect($this->generateUrl(...)); - } + $em->persist($document); + $em->flush(); + + return $this->redirect($this->generateUrl(...)); } return array('form' => $form->createView()); } -.. note:: - - When writing the template, don't forget to set the ``enctype`` attribute: - - .. configuration-block:: - - .. code-block:: html+jinja - -

Upload File

- - - {{ form_widget(form) }} - - - - - .. code-block:: html+php - -

Upload File

- -
enctype($form) ?>> - widget($form) ?> - - -
- The previous controller will automatically persist the ``Document`` entity with the submitted name, but it will do nothing about the file and the ``path`` property will be blank. diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst index 9e735ee877a..82f4ac216bb 100644 --- a/cookbook/doctrine/registration_form.rst +++ b/cookbook/doctrine/registration_form.rst @@ -232,10 +232,10 @@ controller for displaying the registration form:: { public function registerAction() { - $form = $this->createForm( - new RegistrationType(), - new Registration() - ); + $registration = new Registration(); + $form = $this->createForm(new RegistrationType(), $registration, array( + 'action' => $this->generateUrl('create'), + )); return $this->render( 'AcmeAccountBundle:Account:register.html.twig', @@ -249,22 +249,18 @@ and its template: .. code-block:: html+jinja {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #} -
- {{ form_widget(form) }} - - -
+ {{ form(form) }} Finally, create the controller which handles the form submission. This performs the validation and saves the data into the database:: - public function createAction() + public function createAction(Request $request) { $em = $this->getDoctrine()->getEntityManager(); $form = $this->createForm(new RegistrationType(), new Registration()); - $form->bind($this->getRequest()); + $form->handleRequest($request); if ($form->isValid()) { $registration = $form->getData(); diff --git a/cookbook/form/form_collections.rst b/cookbook/form/form_collections.rst index be64bb969b7..e84136970ec 100755 --- a/cookbook/form/form_collections.rst +++ b/cookbook/form/form_collections.rst @@ -177,12 +177,10 @@ In your controller, you'll now initialize a new instance of ``TaskType``:: $form = $this->createForm(new TaskType(), $task); - // process the form on POST - if ($request->isMethod('POST')) { - $form->bind($request); - if ($form->isValid()) { - // ... maybe do some form processing, like saving the Task and Tag objects - } + $form->handleRequest($request); + + if ($form->isValid()) { + // ... maybe do some form processing, like saving the Task and Tag objects } return $this->render('AcmeTaskBundle:Task:new.html.twig', array( @@ -205,7 +203,7 @@ zero tags when first created). {# ... #} -
+ {{ form_start(form) }} {# render the task's only field: description #} {{ form_row(form.description) }} @@ -216,10 +214,9 @@ zero tags when first created).
  • {{ form_row(tag.name) }}
  • {% endfor %} + {{ form_end(form) }} - {{ form_rest(form) }} - {# ... #} -
    + {# ... #} .. code-block:: html+php @@ -227,16 +224,17 @@ zero tags when first created). -
    + start($form) ?> + + row($form['description']) ?> +

    Tags

    • row($tag['name']) ?>
    - - rest($form) ?> -
    + end($form) ?> @@ -642,40 +640,38 @@ the relationship between the removed ``Tag`` and ``Task`` object. $editForm = $this->createForm(new TaskType(), $task); - if ($request->isMethod('POST')) { - $editForm->bind($this->getRequest()); + $editForm->handleRequest($request); - if ($editForm->isValid()) { + if ($editForm->isValid()) { - // filter $originalTags to contain tags no longer present - foreach ($task->getTags() as $tag) { - foreach ($originalTags as $key => $toDel) { - if ($toDel->getId() === $tag->getId()) { - unset($originalTags[$key]); - } + // filter $originalTags to contain tags no longer present + foreach ($task->getTags() as $tag) { + foreach ($originalTags as $key => $toDel) { + if ($toDel->getId() === $tag->getId()) { + unset($originalTags[$key]); } } + } - // remove the relationship between the tag and the Task - foreach ($originalTags as $tag) { - // remove the Task from the Tag - $tag->getTasks()->removeElement($task); + // remove the relationship between the tag and the Task + foreach ($originalTags as $tag) { + // remove the Task from the Tag + $tag->getTasks()->removeElement($task); - // if it were a ManyToOne relationship, remove the relationship like this - // $tag->setTask(null); + // if it were a ManyToOne relationship, remove the relationship like this + // $tag->setTask(null); - $em->persist($tag); + $em->persist($tag); - // if you wanted to delete the Tag entirely, you can also do that - // $em->remove($tag); - } + // if you wanted to delete the Tag entirely, you can also do that + // $em->remove($tag); + } - $em->persist($task); - $em->flush(); + $em->persist($task); + $em->flush(); - // redirect back to some edit page - return $this->redirect($this->generateUrl('task_edit', array('id' => $id))); - } + // redirect back to some edit page + return $this->redirect($this->generateUrl('task_edit', array('id' => $id))); } // render some form template diff --git a/cookbook/routing/method_parameters.rst b/cookbook/routing/method_parameters.rst index f0360e7e87f..ecf152ecc47 100644 --- a/cookbook/routing/method_parameters.rst +++ b/cookbook/routing/method_parameters.rst @@ -86,28 +86,7 @@ Unfortunately, life isn't quite this simple, since most browsers do not support sending PUT and DELETE requests. Fortunately Symfony2 provides you with a simple way of working around this limitation. By including a ``_method`` parameter in the query string or parameters of an HTTP request, Symfony2 will -use this as the method when matching routes. This can be done easily in forms -with a hidden field. Suppose you have a form for editing a blog post: - -.. code-block:: html+jinja - -
    - - {{ form_widget(form) }} - -
    - -The submitted request will now match the ``blog_update`` route and the ``updateAction`` -will be used to process the form. - -Likewise the delete form could be changed to look like this: - -.. code-block:: html+jinja - -
    - - {{ form_widget(delete_form) }} - -
    - -It will then match the ``blog_delete`` route. +use this as the method when matching routes. Forms automatically include a +hidden field for this parameter if their submission method is not GET or POST. +See :ref:`the related chapter in the forms documentation` +for more information. diff --git a/reference/forms/twig_reference.rst b/reference/forms/twig_reference.rst index 7022dbf19be..6ad0aa55347 100644 --- a/reference/forms/twig_reference.rst +++ b/reference/forms/twig_reference.rst @@ -24,6 +24,75 @@ rendering forms. There are several different functions available, and each is responsible for rendering a different part of a form (e.g. labels, errors, widgets, etc). +.. _reference-forms-twig-form: + +form(view, variables) +--------------------- + +Renders the HTML of a complete form. + +.. code-block:: jinja + + {# render the form and change the submission method #} + {{ form(form, {'method': 'GET'}) }} + +You will mostly use this helper for prototyping and if you use custom form +themes. If you need more flexibility in rendering the form, you should use +the other helpers to render individual parts of the form instead: + +.. code-block:: jinja + + {{ form_start(form) }} + {{ form_errors(form) }} + + {{ form_row(form.name) }} + {{ form_row(form.dueDate) }} + + + {{ form_end(form) }} + +See ":ref:`twig-reference-form-variables`" to learn more about the ``variables`` +argument. + +.. _reference-forms-twig-start: + +form_start(view, variables) +--------------------------- + +Renders the start tag of a form. This helper takes care of printing the +configured method and target action of the form. It will also include the +correct ``enctype`` property if the form contains upload fields. + +.. code-block:: jinja + + {# render the start tag and change the submission method #} + {{ form_start(form, {'method': 'GET'}) }} + +See ":ref:`twig-reference-form-variables`" to learn more about the ``variables`` +argument. + +.. _reference-forms-twig-end: + +form_end(view, variables) +------------------------- + +Renders the end tag of a form. + +.. code-block:: jinja + + {{ form_end(form) }} + +This helper also outputs ``form_rest()`` unless you set ``render_rest`` to +false: + +.. code-block:: jinja + + {# don't render unrendered fields #} + {{ form_end(form, {'render_rest': false}) }} + +See ":ref:`twig-reference-form-variables`" to learn more about the ``variables`` +argument. + .. _reference-forms-twig-label: form_label(view, label, variables) @@ -119,6 +188,11 @@ obvious (since it'll render the field for you). form_enctype(view) ------------------ +.. note:: + + This helper was deprecated in Symfony 2.3 and will be removed in Symfony 3.0. + You should use ``form_start()`` instead. + If the form contains at least one file upload field, this will render the required ``enctype="multipart/form-data"`` form attribute. It's always a good idea to include this in your form tag: diff --git a/reference/forms/types/collection.rst b/reference/forms/types/collection.rst index ce4e438001f..b394e8bbb5a 100644 --- a/reference/forms/types/collection.rst +++ b/reference/forms/types/collection.rst @@ -151,7 +151,7 @@ you need is the JavaScript: .. code-block:: html+jinja -
    + {{ form_start(form) }} {# ... #} {# store the prototype on the data-prototype attribute #} @@ -167,7 +167,7 @@ you need is the JavaScript: Add another email {# ... #} -
    + {{ form_end(form) }}