Symfony Forms: The Basics

Forms, the Web's Way of Interacting with Data

Symfony's approach to forms is three form in my mind: model, validation and view.

The model abstracts the data structure from the form, it can be a simple class with no persistance, it just needs the appropriate getters and setters.

Validation allows the model to be verified on a stricter level than database constraints. You can use Symfony's built in validation types to rigorously vet your data before further processing. This is on a higher level than just the form, this validation can be used all over the application. However the form component makes good use of it.

The view determines how the form is going to be constructed and rendered. The form chooses which properties of the model it wants to present and what form type is used to display that property. This can be more than just an input type, but an interactive widget to handle the control, such as a date picker.

How To Create a Basic Contact Form

Model

Let's keep this model as simple as can be, no data persistance, no fancy magic under the hood. It's just a simple PHP class with two properties, email and message.

//Acme/DemoBundle/Model/Contact.php namespace Acme\DemoBundle\Model; class Contact{ public $email; public $message; public function setEmail($email) { $this->email = $email; } public function setMessage($message) { $this->message = $message; } public function getEmail() { return $this->email; } public function getMessage() { return $this->message; } }

The Validation

Symfony's validation mechanism has a bunch of types that we can use to handle the requirements we'll need with this example.

//Acme/DemoBundle/Resources/config/validation.yml Acme\DemoBundle\Model\Contact: properties: email: - Email: message: Address {{ value }} is not valid. checkMX: true message: - NotBlank: - Length: min: 3

The View

There are a few ways to handle the view, however I think the cleanest and most reusable approach is to create this as it's own class and make it available to the controllers via the service container.

A builder or type class that will accept a form builder object and based on the builder's logic will construct the appropriate form object that gets passed to the controller.

//Acme/DemoBundle/Builder/ContactForm.php namespace Acme/DemoBundle/Builder; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class ContactForm extends AbstractType{ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('email', 'text') ->add('message', 'textarea') ->add('save', 'submit') ->setMethod('POST'); } }

Now that we have the form builder class set up we need to expose this class as a service and apply the right service tag.

//Acme/DemoBundle/Resources/config/services.yml acme_demo.form.builder.contact: class: Acme\DemoBundle\Builder\ContactForm tags: - name: form.type alias: acme_demo_contact

Now we'll need to create the template to render the form. Symfony makes this really easy, it can actually be done in just a few lines.

//Acme/DemoBundle/Resources/view/Default/contactForm.html.twig {% extends 'AcmeDemoBundle:Layout:full-width.html.twig' %} {% block content %} <div class="container"> {{ form(form_view) }} </div> {% endblock %}

Handling the Form in a Controller

We now have the model, validation and the form builder set up as a service. We can now render, submit and validate the form.

namespace Acme\DemoBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Acme\DemoBundle\Model\Contact; class DefaultController extends Controller { public function contactFormAction(Request $request) { $contact = new Contact(); $form = $this->createForm('acme_demo_contact', $contact); if($request->getMethod() == 'POST') { $form->handleRequest($request); if($form->isValid()) { $this->get('session')->getFlashBag()->add('notice', 'Thanks for contacting Acme Demo'); $this->sendEmail($form->getData()); } else{ $this->get('session')->getFlashBag()->add('error', 'Invalid information sent to contact processor'); } } $view = $form->createView(); return $this->render('AcmeDemoBundle:Default:contactForm.html.twig', array('form_view' => $view)); } public function sendEmail(Contact $contact) { //... } }

The code above retrieves the form object from the service container through the createForm function. It then tests the request to determine if it's a POST request, meaning our form has been submitted. If that's true, it sends the request to the form's handleRequest function. This is where the form processes the request's parameters to bind data to it's model. After that the validation of the model is tested in the form's isValid function, that determines whether we'll send the email or whether we'll just add an error and render the form again.

Finishing Touches

The controller brings all 3 components together. It creates the model, Contact. Validation is invoked through the configuration we had set up in the validation.yml file and the form's isValid method, which delegates to Symfony's validator service. The view is constructed from the form and rendered in the contactForm.html.twig template using the twig method form.

All of this work is greatly alleviated by an architecture of modular components. Our model was very simple but this design is even stronger when coupled to a doctrine entity that can persist. Validation can get much more complicated, it's designed to stack and have multiple validation constraints, including custom ones.