How to make proper form validation with Symfony

Form validation enables a better user experience while protecting your applications. Our latest hands-on guide shows you how.

Collecting user data is a hot topic, with the GDPR adding weight to registration forms. As a Developer, creating forms in your web applications is almost inevitable. When you do it, you should help users to fill the form correctly without leading them to despair. But you also should make sure that data is filled in a correct format to be processed without damaging your applications.

Your web application must be able to check whether or not data entered in forms is correct. If it is, data can be submitted and stored in a database. Otherwise, the user should get an error message explaining what went wrong. In the following lines, we’ll show you how to get there.

Ok, ready? Let’s start!

In this tutorial, we used Symfony 3.4.8, and two third-party bundles, EWZRecaptchaBundle and blackknight467’s StarRatingBundle.

The first thing you need to do is set a Simfony3 environment from the procedures in the online documentation, and install the two bundles used:

composer require excelwebzone/recaptcha-bundle composer require blackknight467/star-rating-bundle

The usage and setup procedures for both bundles are described here EWZRecaptchaBundle and here StarRatingBundle.

What we want to achieve, is to simulate a generic product review creation mechanism, by receiving the data sent by the reviewer, validate the data, store it on the database, and display the stored reviews. For this purpose, we will need two endpoints, one for the review creation, and another one for listing them.

So, basically, this is the skeleton of our controller, which we have called ReviewController:

namespace AppBundle\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; class ReviewController extends Controller { /** * @Route("/", name="review") */ public function reviewAction(Request $request) { } /** * @Route("/list", name="list") */ public function listAction(Request $request) { } }

Now, we need a data model class, which will receive the data and help on the validation of the form.

Our ReviewModel will have constraints for the validation, and some example properties: name, email, satisfaction, quality, review, and recaptcha.

This recaptcha property will use the validator introduced by the EZWRecaptchaBundle.

And our form will look like this:

namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\OptionsResolver\OptionsResolver; use AppBundle\Model\ReviewModel; use blackknight467\StarRatingBundle\Form\RatingType; use EWZ\Bundle\RecaptchaBundle\Form\Type\EWZRecaptchaType; class ReviewType extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name', TextType::class, ['required' => true]) ->add('email', EmailType::class, ['required' => false]) ->add('satisfaction', RatingType::class, ['required' => true]) ->add('quality', RatingType::class, ['required' => true]) ->add('review', TextareaType::class, ['required' => true]) ->add('recaptcha', EWZRecaptchaType::class, ['required' => true]) ->add('submit', SubmitType::class) ; } /** * {@inheritdoc} */ public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => ReviewModel::class, 'csrf_protection' => false, 'allow_extra_fields' => true, )); } /** * {@inheritdoc} */ public function getBlockPrefix() { return ''; } }

Next, we need some logic in our reviewAction, which will create the form using our ReviewType class, and populate the ReviewModel.

/** * @Route("/", name="review") */ public function reviewAction(Request $request) { $form = $this->createForm(ReviewType::class); $postedData = null; if($request->getMethod() == "POST") { $postedData = new ReviewModel(); $form = $this->createForm(ReviewType::class, $postedData); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // Store the review in the database $this->reviewService->createReview($postedData); // Set success message $this->addFlash('success', 'Your review has been delivered successfuly.'); // Clear the form, since the data was submitted correctly $form = $this->createForm(ReviewType::class); }else{ $postedData = null; // Set error message $this->addFlash('error', 'There are errors with your submission. Please solve the errors, and try again.'); } } return $this->render('review/index.html.twig', [ 'form' => $form->createView() ]); }

If the data in the form is valid, we will call a service (ReviewService) to store the posted information. This service was created to handle the creation and listing of our reviews and relates to the Review entity and the ReviewRepository.

Also, the controller will use addFlash() to define the success and error message, which will be used in our twig:

{% for label, messages in app.flashes(['error', 'success']) %} {% for message in messages %} <div class="confirmation-container"> {% if label == "success" %} <span class="confirmation"> <span class="fa fa-check-circle-o"></span> {% else %} <span class="error_confirmation"> <span class="fa fa-times-circle"></span> {% endif %} <span>{{ message }}</span> </span> </div> {% endfor %} {% endfor %}

And finally, the controller passes the form into the twig, which is used for the creation of the form itself, display error messages related to data validation, and keep the form populated with the data if any validation problem is found.

We could generate the whole form automatically, but in this example, we placed each form element, label and error placeholder by hand, in specific positions.

{{ form_start(form) }} <div class="form_field"> {{ form_label(, 'Name', {'label_attr': {'class': 'label'} }) }} {{ form_errors( }} {{ form_widget(, {'attr': {'maxlength': 150}}) }} </div> <div class="form_field"> {{ form_label(, 'Email address', {'label_attr': {'class': 'label'} }) }} {{ form_errors( }} {{ form_widget(, {'attr': {'placeholder': '(optional)', 'maxlength': 254}}) }} </div> <div class="form_field"> {{ form_label(form.satisfaction, 'Satisfaction', {'label_attr': {'class': 'label'} }) }} {{ form_errors(form.satisfaction) }} {{ form_widget(form.satisfaction) }} </div> <div class="form_field"> {{ form_label(form.quality, 'Product quality', {'label_attr': {'class': 'label'} }) }} {{ form_errors(form.quality) }} {{ form_widget(form.quality) }} </div> <div class="form_field"> {{ form_label(, 'Your review', {'label_attr': {'class': 'label'} }) }} {{ form_errors( }} {{ form_widget(, {'attr': {'maxlength': 1000}}) }} </div> <div class="form_field_recaptcha"> {{ form_errors(form.recaptcha) }} {{ form_widget(form.recaptcha) }} </div> <div class="form_field_submit"> {{ form_widget(form.submit) }} </div> {{ form_end(form) }}

In this project, we already have a CSS formatted layout, and the rendered form will look like this:

If there are any data validation problems, we will get the following:

And in the event of success, we will get the following:

So, we are sure that our form is working correctly, and the validation is being done, including the recaptcha verification.

And now let’s take care of the review listing part. For that we will use the following action:

/** * @Route("/list", name="list") */ public function listAction(Request $request) { $reviews = $this->reviewService->listReviews(); return $this->render('review/list.html.twig', [ 'reviews' => $reviews ]); }

It will basically just get the list of reviews existing in the database, and pass them into the twig, which will populate a table with that information, as follows:

See? I told you it was simple :)

And that’s all. For further reference, make sure to check the full project in GitHub.

Written by Ricardo Correia | Developer at Cleverti

Originally published at