ZetCode

Symfony validation

last modified July 5, 2020

Symfony validation tutorial shows how to validate data in a Symfony application. In this tutorial, we use manual validation. We do not use doctrine annotations.

Symfony

Symfony is a set of reusable PHP components and a PHP framework for web projects. Symfony was published as free software in 2005. The original author of Symfony is Fabien Potencier. Symfony was inspired by Ruby on Rails, Djanog, and the Spring Framework.

Symfony validation

Input from users must be validated. Symfony provides a Validator component to perform validation tasks. The component is based on Java's Bean Validation specification.

The Validator is designed to validate objects against constraints. Constraints are assertions that a condition is true. Symfony has many built-in constraints including NotBlank, Email, Length, and Isbn. It is possible to create custom constraints as well.

Symfony validation example

In the example, we have a simple form with two fields: name and email. After the form is submitted, we manually validate the fields with Symfony's Validator. In the example, we use Length, NotBlank, and Email constraints.

Installing packages

$ composer create-project symfony/skeleton myval
$ cd myval

We create a new Symfony project and go to the project directory.

$ composer req maker server --dev

We install symfony/maker-bundle and symfony/web-server-bundle. These are useful for development mode. Note that we are using aliases for the packages.

$ composer req twig annotations 

We install the twig-bundle and the annotations. Annotations are located in the sensio/framework-extra-bundle.

$ composer req validator

The symfony/validator package contains the Symfony validation tools.

$ composer req security-csrf
$ composer req monolog
$ composer req property-access

The symfony/security-csrf package is needed against cross-site request forgeries, symfony/monolog-bundle for logging, and symfony/property-access for manipulating PHP properties.

Building Symfony application

$ php bin/console make:controller HomeController

We create a HomeController.

src/Controller/HomeController.php
<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class HomeController extends AbstractController
{
    /**
     * @Route("/home", name="home")
     */
    public function index(): Response
    {
        return $this->render('home/index.html.twig');
    }
}

This is a simple controller that sends a view containing the web form to the user.

$ php bin/console make:controller FormController

We create a FormController that responds to the form submission.

src/Controller/FormController.php
<?php

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class FormController extends AbstractController
{
    /**
     * @Route("/sendForm", name="form")
     */
    public function index(Request $request, ValidatorInterface $validator,
        LoggerInterface $logger): Response {

        $token = $request->request->get("token");

        if (!$this->isCsrfTokenValid('myform', $token)) {

            $logger->info("CSRF failure");

            return new Response("Operation not allowed", Response::HTTP_OK,
                ['content-type' => 'text/plain']);
        }

        $name = $request->request->get("name");
        $email = $request->request->get("email");

        $input = ['name' => $name, 'email' => $email];

        $constraints = new Assert\Collection([
            'name' => [new Assert\Length(['min' => 2]), new Assert\NotBlank],
            'email' => [new Assert\Email(), new Assert\notBlank],
        ]);

        $violations = $validator->validate($input, $constraints);

        if (count($violations) > 0) {

            $accessor = PropertyAccess::createPropertyAccessor();

            $errorMessages = [];

            foreach ($violations as $violation) {

                $accessor->setValue($errorMessages,
                    $violation->getPropertyPath(),
                    $violation->getMessage());
            }

            return $this->render('form/violations.html.twig',
                ['errorMessages' => $errorMessages]);
        } else {
            return new Response("Validation passed", Response::HTTP_OK,
                ['content-type' => 'text/plain']);
        }
    }
}

In the FormController, we check the CSRF token, validate the form input values, and send a response back to the client.

Note: For simplicity reasons, we have placed the validation code into the controller. In a production application it is better to place such code in a separate service class.

public function index(Request $request, ValidatorInterface $validator, 
        LoggerInterface $logger)
{

We inject the request object, validator object, and logger object.

$token = $request->request->get("token");

if (!$this->isCsrfTokenValid('myform', $token)) {
    
    $logger->info("CSRF failure");

    return new Response("Operation not allowed",  Response::HTTP_OK, 
        ['content-type' => 'text/plain']);
}

We retrieve the token and validate it with isCsrfTokenValid() method.

$name = $request->request->get("name");
$email = $request->request->get("email");

$input = ['name' => $name, 'email' => $email];

We get the request input parameters and place them into an array.

$constraints = new Assert\Collection([
    'name' => [new Assert\Length(['min' => 2]), new Assert\NotBlank],
    'email' => [new Assert\Email(), new Assert\notBlank]
]);

We create a collection of constraints. We can assign multiple constraints for a single value.

$violations = $validator->validate($input, $constraints);

We validate the input data against the constraints with the validate() method. The method returns possible violations. Symfony validator returns a ConstraintViolationList. We use the Symfony accessor to process the list.

if (count($violations) > 0) {

We check if there are some violations.

$accessor = PropertyAccess::createPropertyAccessor();

$errorMessages = [];

foreach ($violations as $violation) {

    $accessor->setValue($errorMessages,
        $violation->getPropertyPath(),
        $violation->getMessage());
}

Symfony PropertyAccess is used to process the violation messages, before sending them to the template. The ConstraintViolationList is transformed into a PHP array, where keys are form fields and values are error messages. The array is later processed in Twig using the for directive.

return $this->render('form/violations.html.twig',
    ['errorMessages' => $errorMessages]);

We render the error messages in a separate page. This is often done using flash messages. Have a look at Symfony keep form values tutorial how to do it with flashes.

} else {
    return new Response("Validation passed", Response::HTTP_OK,
        ['content-type' => 'text/plain']);
}

If everything is OK, we send a plain message Validation passed.

templates/home/index.html.twig
{% extends 'base.html.twig' %}

{% block title %}Home page{% endblock %}

{% block body %}

    <form action="sendForm" method="post">

        <input type="hidden" name="token" value="{{ csrf_token('myform') }}" />
        <div>
            <label>Name:</label>
            <input type="text" name="name">
        </div>

        <div>
            <label>Email</label>
            <input type="email" name="email">
        </div>

        <button type="submit">Send</button>

    </form>

{% endblock %}

The form contains two fields: name and email.

<input type="hidden" name="token" value="{{ csrf_token('myform') }}" />

It also contains a hidden field to guard against cross-site request forgeries.

templates/form/violations.html.twig
{% extends 'base.html.twig' %}

{% block title %}Violations{% endblock %}

{% block body %}
<h2>Validation failed</h2>

<ul>
{% for field, errorMessage in errorMessages %}
    <li>{{ field }}: {{ errorMessage }}</li>
{% endfor %}
</ul>
{% endblock %}

In the violations view, we go through the violations and list them.

{% for field, errorMessage in errorMessages %}
    <li>{{ field }}: {{ errorMessage }}</li>
{% endfor %}

With the for directive, we go through error messages and show them if there are any.

templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

This is the base Twig template.

$ php bin/console server:run

We start the development server. Then locate to the localhost:8000/home url to get the form.

In this tutorial we have validated a simple form in a Symfony application. We have used manual validation.

List all Symfony tutorials.