Symfony 4 Workshop

Validation

Theory

Some words about validation.

One important piece that is left is to validate the data that comes from the user.

We would like to validate things like:

  • The name of the user must have at least 2 characters.
  • The email of the user is a valid email string.
  • We don't sell more tickets than what we have available.

For that, we can use the Validator component. We already installed it when we were building our forms.

Let's add some validation constraints in the Enquiry Entity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\Table(name="enquiry")
 */
class Enquiry
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     * @Assert\Length(min=2)
     */
    private $fullName;

    /**
     * @ORM\Column(type="string")
     * @Assert\Email()
     */
    private $email;

    /**
     * @ORM\Column(type="text")
     * @Assert\Length(max=100)
     */
    private $question;

    //...

That is it, we are now enforcing these restrictions.

Exercise

Can you add a validation constraint in Sale so fullName has at least 2 characters?

Reveal the solution

We can create our own constraints, even when they are complicated. Let's see an example:

We would like to apply a validation constraint in our form so it is not valid if the number of free seats is less than the amount that the user wants to book.

To build a validator, we need to write two objects: a Constraint and a Validator. This is similar to the pattern we saw previously with Events and EventListeners.

The Constraint will hold the data.

The Validator will contain the actual logic of the validation.

Let's create a new directory src/Validator/Constraint.

In there, we will write a new file src/Validator/Constraint/HasAvailableSeats.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\Validator\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class HasAvailableSeats extends Constraint
{
    public $message = 'This show does not have {{ number }} available seats.
    There are only {{ available }} seats available.';

    protected $movie;
    
    public function __construct($options)
    {
        $this->movie = $options['movie'];
    }
    
    public function getMovie()
    {
        return $this->movie;
    }

    public function validatedBy()
    {
        return get_class($this).'Validator';
    }
}

And the validator in src/Validator/Constraint/HasAvailableSeatsValidator.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

namespace App\Validator\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class HasAvailableSeatsValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        $available = $constraint->getMovie()->getAvailableSeats();
        if ($value > $available) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ number }}', $value)
                ->setParameter('{{ available }}', $available)
                ->addViolation();
        }
    }
}

Can you guess how this works? Validator will call a method in the Entity Movie to get the number of available seats, and if there are not enough seats, it will add a violation using our constraint message.

The logic to get the number of available seats is better placed in the entity Movie. Whenever there is logic that is related to the entities, and not to external services, a good place to put it is in the Entity:

1
2
3
4
5
6
7
8
9
10
11
class Movie
{
    const ROOM_ROWS = 5;
    const SEATS_PER_ROW = 5;

    //...

    public function getAvailableSeats()
    {
        return Movie::ROOM_ROWS * Movie::SEATS_PER_ROW - count($this->tickets);
    }

Now, how to use it from our Form? Let's update SaleType:

(The only changes are in lines 10, 30 and 41)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
namespace App\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\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use App\Validator\Constraint\HasAvailableSeats;
use App\Entity\Sale;

class SaleType extends AbstractType
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('fullName', TextType::class)
            ->add('numTickets', ChoiceType::class, [
                'label' => 'Number of tickets',
                'mapped' => false,
                'choices' => [
                    1 => 1,
                    2 => 2,
                    3 => 3,
                    4 => 4,
                    5 => 5,
                ],
                'constraints' => new HasAvailableSeats(['movie' => $options['movie']]),
            ])
            ->add('save', SubmitType::class, ['label' => 'Book tickets'])
            ->getForm();
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => Sale::class,
            'movie' => null,
        ));
    }
}

Line 41 allows us now to pass movie as an option when we build the form in our Controller:

1
2
3

        $form = $this->createForm(SaleType::class, $sale, ['movie' => $movie]);
        

Try it booking more than 25 tickets in several sales of the same movie, does it work?

This was just an example to show that we can integrate whatever rule we think about in our validations.