The Form Builder

By the end of this lesson, you’ll have a basic comments system attached to your blog. Along the way, we’ll be:

  • Using the comment entity to quickly building a form
  • Handling Submission
  • Moving the form in to a dedicated file

Follow along using the code available from this GIT repository.
Start - setting_up_the_admin_builder
End - the_form_builder

Creating a Form

In an earlier lesson, when we created the comment entity we also what will become the backend for our form. The Symfony Form Builder takes a data object and uses it as a template for data definitions.

All that is needed for us to do is decide how the form is presented on the frontend. Taking user input is a common task and the Symfony controller has helper methods to aid in the task so, aside from the object definition, there is very little that actually needs to be done.

  1. <?php
  2. // # src/AppBundle/Controller/BlogController.php
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6.  
  7. // Include functionality for form submission
  8. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  9. // Include the Data Entity
  10. use AppBundle\Entity\Comment;
  11.  
  12. class BlogController extends Controller
  13. {
  14.   /**
  15.    * @Route("/blog/{id}", name="blog_singular")
  16.    */
  17.   public function singularAction($id)
  18.   {
  19.     $post = $this->getDoctrine()
  20.     ->getRepository('AppBundle:Post')
  21.     ->find($id);
  22.  
  23.     // Prepare a data object for use with the form
  24.     $comment = new Comment();
  25.     $form = $this->createFormBuilder($comment)
  26.     ->add('email')
  27.     ->add('body')
  28.     // Submit isn't part of data object so we add it
  29.     ->add('save', SubmitType::class, array('label' => 'Submit comment'))
  30.     ->getForm();
  31.  
  32.     return $this->render('blog/singular.html.twig', array(
  33.       'post' => $post,
  34.       // Pass the view object to the template
  35.       'comment_form' => $form->createView(),
  36.     ));
  37.   }
  38. }

Take note that a data object is setup prior to form creation and used as a parameter in the form builder factory. From there, we are just defining what form fields we want to use and passing the view object to the template.

The Symfony form builder will try to auto guess what field type to use based on the object definition. In the case of the submit button, we need to define the class ourselves because it isn't part of the data object.

You don't have to use the type guessing abilities of Symfony if you specify the second parameter yourself

Rendering the form

Twig has helpers to control rendering of a complete form in one line. You can also exhert control of rendering down to labels, input fields and the way feedback is provided.

For the sake of this tutorial, we'll use the single line command. To get a better understanding of the options available, take a look at the official documentation.

  1. {{ form(comment_form) }}

Form Submission

So far, request parameters have been handled automatically by the Route annotation handler and there has been no need to use the Symfony Request object. With forms, because we want direct a greater degree of access to the request, we need to make sure it is include as a method parameter.

We need to add code to the controller after the getForm method (from form builder) has been called. Logically, it makes sense to perform the check straight after as it could mean avoiding any extra processing if the code path forks or redirects away.

  1. <?php
  2. // # src/AppBundle/Controller/BlogController.php
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  7. use AppBundle\Entity\Comment;
  8.  
  9. use Symfony\Component\HttpFoundation\Request;
  10.  
  11. class BlogController extends Controller
  12. {
  13.   /* Take note of the extra parameter passed to the method */
  14.   public function singularAction($id, Request $request)
  15.   {
  16.     /* Form definition */
  17.     $post = $this->getDoctrine()
  18.     ->getRepository('AppBundle:Post')
  19.     ->find($id);
  20.  
  21.     // Prepare a data object for use with the form
  22.     $comment = new Comment();
  23.     $form = $this->createFormBuilder($comment)
  24.       ->add('email')
  25.       ->add('body')
  26.       // Submit isn't part of data object so we add it
  27.       ->add('save', SubmitType::class, array('label' => 'Submit comment'))
  28.       ->getForm();
  29.  
  30.     // Get Symfony to parse the form
  31.     $form->handleRequest($request);
  32.     if ($form->isSubmitted() && $form->isValid())
  33.     {
  34.  
  35.       /* Set some comment properties */
  36.       $comment->setPost($post);
  37.       $comment->setCreatedAt(new \DateTime());
  38.  
  39.       // Save the comment
  40.       $em = $this->getDoctrine()->getManager();
  41.       $em->persist($comment);
  42.       $em->flush();
  43.     }
  44.  
  45.     return $this->render('blog/singular.html.twig', array(
  46.       'post' => $post,
  47.       'comment_form' => $form->createView(),
  48.     ));
  49.  
  50.   }
  51. }

The only code which should be new to you in the above controller is that which handles the form request

  1. $form->handleRequest($request);
  2. if ($form->isSubmitted() && $form->isValid())

Feedback

With our form code complete and structured this way, fields will only present themselves if the form is not yet valid. We've not taken in to consideration feedback on a successful submission.

We don't want to send the feedback to the template because, a return message might not exist and cause an error forcing us to create placeholder variables. There is an efficient way of solving this problem.

The Symfony session component has a a key/value store called the FlashBag. The FlashBag is storage container that maintains variable state only until it has been used. E.G. If setting a variable on the first request and reading on the third; it will also be present on the second request but be unset by the fourth.

To access the FlashBag, you can get it from either the service container or Request object (if one is available, which if using forms it should be). I will list both methods below, which you choose is more a matter of preference.

  1. # Using the container
  2. $flashBag = $this->get("session")->getFlashBag();
  3. # Using the request object
  4. $flashBag = $request->getSession()->getFlashBag();

If you wanted to support different notification levels (success, failure, warning, etc.) how you structure your data within the flashbag is choice because it's just a PHP data object.

For the sake of this tutorial will set them to a simple alerts key, the significance of which should make itself apparent when configuring the template in a moment.

  1. $flashBag->add("alerts", array(
  2.   "type" => "success",
  3.   "message" => "Thanks for submitting some feedback!"));

Because we only need to use the flashbag for a single operation and nothing else, it is possible to optimise the code saving a bit of memory. This is done by chaining the methods together without any variable.

  1. $request->getSession()->getFlashBag()
  2. ->add("alerts", array(
  3.   "type" => "success",
  4.   "message" => "Thanks for submitting some feedback!"));

See the complete code for our Blog post with comment submission so far below:

  1. <?php
  2. // # src/AppBundle/Controller/BlogController.php
  3.  
  4. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
  6. use Symfony\Component\Form\Extension\Core\Type\SubmitType;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use AppBundle\Entity\Comment;
  9.  
  10.  
  11. class BlogController extends Controller
  12. {
  13.   public function singularAction($id, Request $request)
  14.   {
  15.     /* Form definition */
  16.     $post = $this->getDoctrine()
  17.     ->getRepository('AppBundle:Post')
  18.     ->find($id);
  19.  
  20.     // Prepare a data object for use with the form
  21.     $comment = new Comment();
  22.     $form = $this->createFormBuilder($comment)
  23.       ->add('email')
  24.       ->add('body')
  25.       // Submit isn't part of data object so we add it
  26.       ->add('save', SubmitType::class, array('label' => 'Submit comment'))
  27.       ->getForm();
  28.  
  29.     // Get Symfony to parse the form
  30.     $form->handleRequest($request);
  31.     if ($form->isSubmitted() && $form->isValid())
  32.     {
  33.  
  34.       /* Set some comment properties */
  35.       $comment->setPost($post);
  36.       $comment->setCreatedAt(new \DateTime());
  37.  
  38.       // Save the comment
  39.       $em = $this->getDoctrine()->getManager();
  40.       $em->persist($comment);
  41.       $em->flush();
  42.  
  43.       /* Adding the notification */
  44.       $request->getSession()->getFlashBag()
  45.       ->add("alerts", array(
  46.         "type" => "success",
  47.         "message" => "Thanks for submitting some feedback!"));
  48.     }
  49.  
  50.     return $this->render('blog/singular.html.twig', array(
  51.       'post' => $post,
  52.       'comment_form' => $form->createView(),
  53.     ));
  54.  
  55.   }
  56. }

Displaying the notifications within your templates requires very little code but, if you intend to define a specific area within your page and want to check for the existence of messages before rendering (not just looping through available messages) you should be aware of something.

Once you use a value from the flashbag, it is gone! If you need to check if a key exists, you use the peek method, this is a simple check for whether the value exists without actually using (and emptying) it.

Within your template (we'll simply place it in the base template for the moment), add the following code where you want the notifications to show

  1. {# app/Resources/views/base.html.twig #}
  2.  
  3. {# Check if there are any alerts in the flashbag #}
  4. {% if app.session.flashBag.peek("alerts") is not empty %}
  5.   {# Start looping through the notifications #}
  6.   {% for msg in app.session.flashBag.get("alerts") %}
  7.     <div class="alert alert-{{ msg.type }}">
  8.       {{ msg.message }}
  9.     </div>
  10.   {% endfor %}
  11. {% endif %}

Exercises

Communication, our greatest tool

Whether you want to connect, have a problem I can help with or simply want to drop a line, I welcome your words!