Validating your data fixtures with the Alice Symfony bundle

Published on 2022-10-15 • Modified on 2022-10-15

This post shows how to validate data fixtures with the Alice Symfony bundle before inserting them into the database. It's essential, as you could have invalid data otherwise in the development or test environments. Let's go! 😎

Prerequisite

I will assume you have at least a basic knowledge of Symfony and that you know how to load some fixtures with the hautelook/alice-bundle.

Configuration

  • PHP 8.1
  • Symfony 5.4.14
  • hautelook/alice-bundle 2.11

The hautelook/alice-bundle composer reference, in fact, points out to https://github.com/theofidry/AliceBundle, click here to understand why.

Introduction

This is something that may not be obvious. I have used the Alice bundle for a long time, but it is something I wasn't aware of. I realized that when someone asked a question on the #alice-fixtures channel on the Symfony dev slack. I verified, and yes, he was right; by default, fixtures are not validated. 😮

Goal

The goal is to create a simple processor that checks that each fixture item is valid before inserting it into the database. We will, of course, use the Symfony validator.

The problem

Let's check the problem. Here are the fixtures corresponding to this blog post:

  article_224 (extends article):
    previous_article: '@article_216'
    #next_article: '@article_november'
    name: 'Validating your data fixtures with the Alice Symfony bundle'
    date_published: <date_create('2022-10-15')>
    date_modified: <date_create('2022-10-15')>
    keyword: 'symfony,fixtures,alice,validation,processor,data'
    time_required: 3
    stackoverflow_url: https://stackoverflow.com/q/28641360/633864
    icon: fad fa-box-check

This fixture item extends the article template:

App\Entity\Article:
  article (template):
    #type: !php/const App\DBAL\Types\ArticleType::TYPE_BLOG_POST
    type: !php/const App\Enum\ArticleType::BLOG_POST
    active: true
    author: <{pseudo}>
    publisher: '@organization_strangebuzz'
    in_language: <{locales_list}>

Now, let's add a constraint to the Author property of the Article entity; it's not free text anymore, but it must be an allowed value. We can use the Symfony Choice constraint for that:

    private const ALLOWED_AUTHORS = ['COil', 'Soyuka', 'Dunglas'];

    /**
     * The author of this content or rating. Please note that author is special in that HTML 5 provides a special mechanism for indicating authorship via the rel tag. That is equivalent to this and may be used interchangeably.
     */
    #[ORM\Column(type: 'text', nullable: false)]
    #[Assert\NotBlank]
    #[Assert\Choice(choices: self::ALLOWED_AUTHORS, message: '{{ value }} is an invalid value for the author! Allowed values are: {{ choices }}')]
    protected ?string $author = null;

In the fixture, let's put another value than the allowed ones:

  article_224 (extends article):
    author: foobar

If we reload the fixtures, as expected, there is no error. In the database, we have foobar in the author field. 😕

The solution ✨

As the bundle is extensible, we can add a processor to do some stuff before and after inserting each fixtures item in the database. Here it is:

<?php

declare(strict_types=1);

namespace App\DataFixtures\Processor;

use App\Entity\Article;
use Fidry\AliceDataFixtures\ProcessorInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class ArticleProcessor implements ProcessorInterface
{
    public function __construct(
        private readonly ValidatorInterface $validator
    ) {
    }

    public function preProcess(string $id, object $object): void
    {
        if (!$object instanceof Article) {
            return;
        }

        /** @var ConstraintViolationList $violations */
        $violations = $this->validator->validate($object);
        if ($violations->count() > 0) {
            $message = sprintf("Error when validating fixture %s (Article), violation(s) detected:\n%s", $id, $violations);
            throw new \DomainException($message);
        }
    }

    public function postProcess(string $id, object $object): void
    {
    }
}

Thanks to Symfony, this service is auto-configured. If not, you must assign the fidry_alice_data_fixtures.processor tag to it.

Information for Service "App\DataFixtures\Processor\ArticleProcessor"
=====================================================================

---------------- ---------------------------------------------
Option           Value
---------------- ---------------------------------------------
Service ID       App\DataFixtures\Processor\ArticleProcessor
Class            App\DataFixtures\Processor\ArticleProcessor
Tags             fidry_alice_data_fixtures.processor
Public           no
Synthetic        no
Lazy             no
Shared           yes
Abstract         no
Autowired        yes
Autoconfigured   yes
---------------- ---------------------------------------------

Some explanations. We modify the preProcess() function as we want to validate the data before inserting it into the database. The first argument string $id, is the identifier of the fixture; for this blog post, it is article_224. The second argument is the entity being processed. In this example, I trigger the validation only if it's an Article entity, but we could do the same for all our entities. In this case, remove the instanceof check. Now, let's try to reload the fixtures:

php bin/console doctrine:cache:clear-metadata
php bin/console doctrine:database:create --if-not-exists
php bin/console doctrine:schema:drop --force
php bin/console doctrine:schema:create
php bin/console doctrine:schema:validate
php bin/console hautelook:fixtures:load --no-interaction

In ArticleProcessor.php line 29:

Error when validating fixture article_224 (Article), violation(s) detected:
Object(App\Entity\Article).author:
"foobar" is an invalid value for the author! Allowed values are: "COil", "Soyuka", "Dunglas" (code 8e179f1b-97aa-4560-a02f-2a8b42e49df7)

hautelook:fixtures:load [-b|--bundle [BUNDLE]] [--no-bundles] [-m|--manager MANAGER] [--append] [--shard SHARD] [--purge-with-truncate]
make: *** [load-fixtures] Error 1

Victory, we finally have an error, and the process is stopped (nothing is loaded). We have a nice debug message with the fixture identifier that raises the error and the violations list to fix.

Conclusion

We saw how to validate Alice fixtures with a processor. It was fast to add, but we are sure we won't insert incorrect values in the database. This has a slight performance cost as the validator is called for each fixture, but this is insignificant regarding additional checks being made.

That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊

  Read the doc  More on Stackoverflow

They gave feedback and helped me to fix errors and typos in this article, many thanks to Laurent . 👍


Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )

Thank you for reading! And see you soon on Strangebuzz! 😉

COil