Validating array keys type and presence with Symfony

Published on 2021-08-07 • Modified on 2021-08-07

In this snippet, we see how to validate array keys type and presence with the Symfony validator component. This is straightforward to do, thanks to the collection and type constraints. The Symfony documentation (link below) says that we can cast the $violations variable as a string. It works, but PHPStan, as of level two, reports an error. That's why I use here an arrow function to extract and display the errors.


<?php

declare(strict_types=1);

// src/Controller/Snippet/Snippet158Trait.php

namespace App\Controller\Snippet;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validation;

/**
 * I am using a PHP trait to isolate each snippet in a file.
 * This code should be called from a Symfony controller extending AbstractController (as of Symfony 4.2)
 * or Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
 * Services are injected in the main controller constructor.
 */
trait Snippet158Trait
{
    public function snippet158(): void
    {
        $validator = Validation::createValidator();
        $inputs = [
            [ // valid
                'bar' => 'string', // string
                'babar' => true,   // bool
                'john' => 555,     // int
                'fab' => ['4545'], // array
            ],
            [ // NOT valid
                'bar' => false,           // wrong type
                'babar' => 'true',        // wrong type
                'john' => 0.1,            // wrong type
                'fab' => new \stdClass(), // wrong type
                'extra' => null,          // extra field
            ],
        ];

        $constraints = new Assert\Collection([
            'bar' => new Assert\Type('string'),
            'babar' => new Assert\Type('bool'),
            'john' => new Assert\Type('int'),
            'fab' => new Assert\Type('array'),
        ]);

        foreach ($inputs as $input) {
            $violations = $validator->validate($input, $constraints);
            if (!$violations instanceof ConstraintViolationList) {
                throw new \RuntimeException('Invalid violations type.'); // this is also required by PHPStan to avoid a warning
            }
            if ($violations->count() > 0) {
                // echo $violations; // or this if ignoring the PHPStan warning! 😛
                $errors = array_map(static fn (ConstraintViolation $violation) => $violation->getPropertyPath().': '.$violation->getMessage(), iterator_to_array($violations->getIterator(), false));
                echo sprintf("❌ needed keys of the array\n\%s\nare NOT correct:\n%s", var_export($input, true), implode("\n", $errors)).PHP_EOL;
            } else {
                echo sprintf("✅ All needed keys of the array:\n%s\nare present and of the good type.\n", var_export($input, true)).PHP_EOL;
            }
        }

        // That's it! 😁
    }
}

 Run this snippet  ≪ this.showUnitTest ? this.trans.hide_unit_test : this.trans.show_unit_test ≫  More on Stackoverflow   Read the doc  More on the web

<?php

declare(strict_types=1);

namespace App\Tests\Controller\Snippets;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;

/**
 * @covers Snippet158Trait
 */
final class Snippet158Test extends KernelTestCase
{
    /**
     * @return iterable<int, array>
     */
    public function provide(): iterable
    {
        yield [
            '✅ valid case' => [
                'bar' => 'string',
                'babar' => true,
                'john' => 555,
                'fab' => ['4545'],
            ],
            true,
            0,
        ];

        yield [
            '❌ invalid case' => [
                'bar' => false,
                'babar' => 'true',
                'john' => 0.1,
                'fab' => new \stdClass(),
                'extra' => null,
            ],
            false,
            5,
        ];
    }

    /**
     * @covers Snippet158Trait::snippet158
     *
     * @param array<int, mixed> $array
     *
     * @dataProvider provide
     */
    public function testSnippet158(array $array, bool $isValid, int $errorsCount): void
    {
        $validator = Validation::createValidator();
        $constraints = new Assert\Collection([
            'bar' => new Assert\Type('string'),
            'babar' => new Assert\Type('bool'),
            'john' => new Assert\Type('int'),
            'fab' => new Assert\Type('array'),
        ]);

        $violations = $validator->validate($array, $constraints);
        self::assertSame($isValid, $violations->count() === 0);
        self::assertCount($errorsCount, $violations);
    }
}