ZetCode

Symfony keep form values

last modified July 5, 2020

Symfony keep form values tutorial shows how to keep form values after form submission after the submission of the form fails. In this tutorial, we do the traditional form submission; we do not use form builders.

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 heavily inspired by the Spring Framework.

Keeping form values

When the form is submitted by the user, it is validated by the application. When the validation fails, the application redirects the user back to the form, displaying the validation errors. It is a good practice to keep the already entered values in the form.

Symfony keep form values example

In the example, we have a simple form with two fields: name and email. After the form is submitted, we check for CSRF protection and validate the input values with Symfony's Validator. We store the entered values into the session to retrieve them back when the submission fails.

Setting up the application

We start with setting up the application with composer.

$ composer create-project symfony\skeleton formkeepvals
$ cd formkeepvals

We create a new Symfony skeleton project and go the the newly created project directory.

$ composer require twig annot validator

We install three basic Symfony packages: twig, annot, and validator. The packages may have aliases. For instance, the symfony/validator has two aliases: validator and validation. Check the Symfony Recipes Server for more details.

$ composer require symfony/security-csrf
$ composer require symfony/monolog-bundle

The security-csrf package is needed against cross-site request forgeries and monolog-bundle for logging.

$ composer require symfony/property-access

We install the PropertyAccess component, which is used for convenient reading and writing of properties/keys of objects and arrays.

$ composer require maker server --dev

We install the maker component and the development server.

$ php bin/console make:controller HomeController

We create a HomeController. The controller sends a form to the client.

src/Controller/HomeController.php
<?php

namespace App\Controller;

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

class HomeController extends AbstractController
{
    /**
     * @Route("/home", name="home")
     */
    public function index()
    {
        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 MessageController

We create a MessageController that responds to the form submission.

src/Controller/MessageController.php
<?php

namespace App\Controller;

use App\Service\ValidationService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class MessageController extends AbstractController
{
    /**
     * @Route("/message", name="message")
     */
    public function index(Request $request, ValidationService $validator)
    {
        $token = $request->get("token");

        $valid = $validator->validateToken($token);

        if (!$valid) {

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

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

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

        $errorMessages = $validator->validateInput($input);

        if (count($errorMessages) > 0)
        {
            $session = $request->getSession();
            $session->set('name', $name);
            $session->set('email', $email);

            foreach ($errorMessages as $key => $val) {
                $this->addFlash($key, $val);
            }

            return $this->redirectToRoute('home');

        } else {

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

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

public function index(Request $request, ValidationService $validator)
{

The validation is delegated to the ValidationService, which is injected into the method.

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

$valid = $validator->validateToken($token);

if (!$valid) {

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

We get the CSRF token and validate it. If the validation fails, we send a response with the error message back to the client.

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

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

$errorMessages = $validator->validateInput($input);

We retrieve the form input values and validate them with the validation service. The validation service returns error messages if it fails.

if (count($errorMessages) > 0)
{
    $session = $request->getSession();
    $session->set('name', $name);
    $session->set('email', $email);
...

If there are some error messages, we add the input values to the session so that we can retrieve them after redirection.

foreach ($errorMessages as $key => $val) {
    $this->addFlash($key, $val);
}

We add the messages to the flash bag; the flash bag is used for storing temporary messages such as our validation messages.

return $this->redirectToRoute('home');

We redirect back to the form with redirectToRoute().

src/Service/ValidationService.php
<?php

namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class ValidationService
{
    private $tokenManager;
    private $validator;
    private $accessor;
    private $logger;

    public function __construct(CsrfTokenManagerInterface $tokenManager,
        ValidatorInterface $validator, PropertyAccessorInterface $accessor,
        LoggerInterface $logger)
    {
        $this->tokenManager = $tokenManager;
        $this->validator = $validator;
        $this->accessor = $accessor;
        $this->logger = $logger;
    }

    public function validateToken($token): bool
    {
        $csrf_token = new CsrfToken('myform', $token);

        $isValid = $this->tokenManager->isTokenValid($csrf_token);

        if (!$isValid) {
            $this->logger->error("CSRF failure");
        }

        return $isValid;
    }

    public function validateInput(array $input): array
    {
        $constraints = new Assert\Collection([
            'name' => [new Assert\Length(['min' => 2]), new Assert\NotBlank],
            'email' => [new Assert\Email, new Assert\NotBlank],
        ]);

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

        if (count($violations) > 0) {

            $this->logger->info("Validation failed");

            $messages = [];

            foreach ($violations as $violation) {

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

            return $messages;
        } else {

            return [];
        }
    }
}

The ValidationService checks the CSRF token and validates the input.

public function __construct(CsrfTokenManagerInterface $tokenManager,
    ValidatorInterface $validator, PropertyAccessorInterface $accessor,
    LoggerInterface $logger)
{
    $this->tokenManager = $tokenManager;
    $this->validator = $validator;
    $this->accessor = $accessor;
    $this->logger = $logger;
}

We inject four objects in the constructor: the token manager, the validator, the property accessor and the logger.

public function validateToken($token): bool
{
    $csrf_token = new CsrfToken('myform', $token);

    $isValid = $this->tokenManager->isTokenValid($csrf_token);

    if (!$isValid) {
        $this->logger->error("CSRF failure");
    }

    return $isValid;
}

This code validates the CSRF token using the token manager.

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

These are constraints for validating the form input.

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

With the validator we validate the form input values.

if (count($violations) > 0) {

    $this->logger->info("Validation failed");

    $messages = [];

    foreach ($violations as $violation) {

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

    return $messages;
} else {

    return [];
}

If there are some violations, we log the failure and build validation error messages. To build the messages, we utilize the Symfony property accessor. If there are no violations, we return an empty array.

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

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

{% block stylesheets %}
<style>
    .topmargin {
        margin-top: 10px;
    }
</style>
{% endblock %}

{% block body %}

<section class="ui container topmargin">

    <form class="ui form" action="message" method="post">

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

        {% for msg in app.flashes('name') %}
        <div class="ui small red message">
            {{ msg }}
        </div>
        {% endfor %}

        <div class="field">
            <label>Name:</label>
            <input type="text" name="name" value="{{app.session.get('name')}}">
        </div>

        {% for msg in app.flashes('email') %}
        <div class="ui small red message">
            {{ msg }}
        </div>
        {% endfor %}

        <div class="field">
            <label>Email</label>
            <input type="text" name="email" , value="{{app.session.get('email')}}">
        </div>

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

    </form>

</section>

{% endblock %}

The home page has a form. 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.

{% for msg in app.flashes('name') %}
<div class="ui small red message">
    {{ msg }}
</div>
{% endfor %}

If there are some error messages in the flash bag, we display them.

<input type="text" name="name" value="{{app.session.get('name')}}">

The input tag retrieves its value from the session, if there is one. This is useful after redirections to the form.

templates/base.html.twig
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        <link href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css"
                 rel="stylesheet">
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}

        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.js"></script>
        {% block javascripts %}{% endblock %}
    </body>
</html>

This is the base Twig template. It contains the Semantic UI CSS framework.

In this tutorial we have validated a simple form in a Symfony application.

List all Symfony tutorials.