Hlavní navigace

Úvod do frameworku Symfony: validácia dát

Ján Bodnár

Vo piatom pokračovaní série článkov o Symfony sa budeme venovať validácii dát. V minulom článku sme použili na validáciu dát externú knižnicu Rakit Validation. Symfony má vlastný komponent pre validáciu: symfony/validator.

Doba čtení: 12 minut

Sdílet

Symfony PropertyAccess komponent

Prv než prejdeme k validácii, predstavíme si Symfony PropertyAccess. Ide o komponent, ktorý unifikuje prístup k atribútom objektov a polí. PHP umožňuje prístup k atribútom objektov aj pomocou operátorov, ktoré sa používajú pri poliach.

K atribútom objektov a polí pristupujeme pomocou property paths. Napríklad [index] je ekvivalentom $data['index'] a prop.sub je ekvivalentom $data->getProp()->getSub().

$ composer req symfony/property-access

Pomocou composera si nainštalujeme balík symfony/property-access.

<?php

// array_access.php

require('vendor/autoload.php');

use Symfony\Component\PropertyAccess\PropertyAccess;

$accessor = PropertyAccess::createPropertyAccessor();

$bag = [
    'new' => [
        'items' => ['coins' => 6, 'pens' => 3, 'keys' => 2],
        'colours' => ['red', 'green', 'yellow']
    ],
    'borrowed' => [
        'items' => ['books' => 23, 'computers' => 2]
    ],
];

echo $accessor->getValue($bag, '[new][items][pens]') . "\n";
echo $accessor->getValue($bag, '[new][colours][0]') . "\n";
echo $accessor->getValue($bag, '[borrowed][items][books]') . "\n";

V tomto príklade máme viacnásobne vnorené polia. Pomocou PropertyAccess komponentu pristupujeme k atribútom týchto vnorených polí. V prípate polí majú property paths hranaté zátvorky [].

<?php

// obj_access.php

require('vendor/autoload.php');

use Symfony\Component\PropertyAccess\PropertyAccess;

class User
{
    public $name = '';
    public $occupation = '';
}

$user1 = new User();
$user1->name = 'John Doe';
$user1->occupation = 'gardener';

$user2 = new User();
$user2->name = 'Peter Novak';
$user2->occupation = 'programmer';

$users = ['user1' => $user1, 'user2' => $user2];

$accessor = PropertyAccess::createPropertyAccessor();

echo $accessor->getValue($users, '[user1].name') . "\n";
echo $accessor->getValue($users, '[user2].occupation') . "\n";

V druhom príklade máme pole objektov. K atribútom objektov pristupujeme pomocou operátora bodky.

Symfony Validator komponent

Symfony Validator komponent je pokročilý nástroj na validáciu dát. Bol inšpirovaný Java Bean Validation špecifikáciou.

Vstupným bodom do procesu validácie je Validator\Validation. Pomocou neho získame objekt validátoru, ktorý vykonáva validáciu pomocou metódy validate().

$validator = Validation::createValidatorBuilder()->getValidator();
$violations = $validator->validate($name, $constraint);

Dáta sa validujú pomocou Validator\Constraints pravidiel. Štandardne sa Validator\Constraints dáva v Symfony alias Assert:

use Symfony\Component\Validator\Constraints as Assert;

Pravidlá sa kategorizujú do skupín. Poznáme základné, reťazcové, dátumové, alebo finančné pravidlá. Môžeme si vytvoriť aj vlastné pravidlá. Pravidlá aplikujeme jednotlivo alebo častejšie v kolekciách Constraints\Collection.

Pravidlá sa môžu špecifikovať a) PHP kódom, b) pomocou anotácií, alebo c) v externých YAML alebo XML súboroch.

Ak validačný proces neprejde, docháza k tzv. porušeniam pravidiel (violations). Porušenie pravidla reprezentuje ConstraintViolation, ktorý obsahuje chybovú hlášku. Tú dostaneme pomocou metódy getMessage().

Metóda validate() vracia ConstraintViolationList. Ak je tento zoznam prázdny, validácia dát bola úspešná. Počet porušení môžeme zistiť pomocou funkcie count().

Príprava

Symfony Validator komponent si budeme demonštrovať na CLI príkladoch. Potrebujeme si nainštalovať viacero balíčkov a pripraviť autoloading.

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "require": {
        "symfony/validator": "^4.2",
        "symfony/var-dumper": "^4.2",
        "symfony/property-access": "^4.2",
        "doctrine/annotations": "^1.6",
        "doctrine/cache": "^1.8",
        "symfony/config": "^4.2",
        "symfony/translation": "^4.2"
    }
}

Takto vyzerá composer.json súbor.

Validácia PHP kódom

V nasledujúcich príkladoch budeme validovať dáta PHP kódom.

Validácia jednej premennej

V prvom príklade budeme validovať jednoduchú premennú.

<?php
// single_val.php

require('vendor/autoload.php');

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

$name = '';
$constraint = new Assert\NotBlank;

$validator = Validation::createValidatorBuilder()->getValidator();
$violations = $validator->validate($name, $constraint);

// dump($violations);

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    echo $violations->get(0)->getMessage();
}

V príklade máme premennú $name na ktorú aplikujeme pravidlo Assert\NotBlank.

$validator = Validation::createValidatorBuilder()->getValidator();

Pomocou ValidatorBuilder objektu vytvoríme validátor.

$violations = $validator->validate($name, $constraint);

Premennú validujeme pomocou metódy validate(), ktorej zadáme názov premennej a pravidlo. Metóda vráti zoznam porušení pravidiel.

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    echo $violations->get(0)->getMessage();
}

Pomocou count() metódy zistíme počet porušení. Eventuálnu chybovú hlášku vypíšeme pomocou getMessage().

$ php single_val.php
This value should not be blank.

Toto je výpis nášho programu.

Validácia pomocou viacerých pravidiel

Ak máme viaceré validačné pravidlá, použijeme Assert\Collection.

<?php
// multiple_val.php

require('vendor/autoload.php');

use Symfony\Component\Validator\Validation;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Constraints as Assert;

$name = 'p';
$email = 'peter@gmailcom';

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

$constraints = new Assert\Collection([
    'name' => [new Assert\Length(['min' => 2]), new Assert\Regex('/^[a-zA-Z1-9]+$/')],
    'email' => [new Assert\Email, new Assert\notBlank],
]);

$validator = Validation::createValidatorBuilder()->getValidator();
$violations = $validator->validate($vals, $constraints);

$accessor = PropertyAccess::createPropertyAccessor();

if (count($violations) > 0) {

    $messages = [];

    foreach ($violations as $violation) {

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

    dump($messages);
} else {

    echo 'validation passed';
}

V príklade máme dve premenné a viacero validačných pravidiel.

$constraints = new Assert\Collection([
    'name' => [new Assert\Length(['min' => 2]), new Assert\Regex('/^[a-zA-Z1-9]+$/')],
    'email' => [new Assert\Email, new Assert\notBlank],
]);

Užívateľské meno sa musí skladať z alfanumerických znakov a mať aspoň dva znaky. Email nesmie byť prázdny a musí zodpovedať požiadavkám na emailovú adresu.

$accessor = PropertyAccess::createPropertyAccessor();

V tomto príklade tiež použijeme PropertyAccess komponent na extrakciu názvov premenných a chybových hlášok do poľa. Použijeme ho z dôvodu, aby nám v názvoch premenných nezostali hranaté zátvorky (napr. [name]).

foreach ($violations as $violation) {

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

dump($messages);

Z ConstraintViolationList  získame premenné a zodpovedajúce chybové hlášky. Zapíšeme ich do poľa $messages a vypíšeme na konzolu pomocou dump() metódy. Metódu máme zo symfony/var-dumper. Metóda nám poskytuje farebne vyladený a prívetivejší informačný výstup. Je to lepšia alternatíva k zabudovanej var_dump() metóde.

$ php multiple_val.php
array:2 [
    "name" => "This value is too short. It should have 2 characters or more."
    "email" => "This value is not a valid email address."
]

Tentoraz máme dve chyby.

Vytvorenie vlastného pravidla

Pre vlastné pravidlo potrebujeme vytvoriť pravidlo a k nemu prislúchajúci validátor. V nasledujúcom príklade si vytvoríme pravidlo, ktoré bude prijímať len alfanumerické znaky.

<?php

// src/Validator/Constraints/AlphaNumeric.php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

class AlphaNumeric extends Constraint
{
    public $message = 'The string "{{ string }}" contains an illegal character: it can only contain letters or numbers.';
}

Máme AlphaNumeric pravidlo. Trieda obsahuje chybovú hlášku.

<?php

// src/Validator/Constraints/AlphaNumericValidator.php

namespace App\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

class AlphaNumericValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof AlphaNumeric) {
            throw new UnexpectedTypeException($constraint, AlphaNumeric::class);
        }

        // custom constraints should ignore null and empty values to allow
        // other constraints (NotBlank, NotNull, etc.) take care of that
        if (null === $value || '' === $value) {
            return;
        }

        if (!is_string($value)) {
            // throw this exception if your validator cannot handle the passed type so
            // that it can be marked as invalid
            throw new UnexpectedValueException($value, 'string');
        }

        if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
            $this->context->buildViolation($constraint->message)
                ->setParameter('{{ string }}', $value)
                ->addViolation();
        }
    }
}

Validačný proces sa vykoná v metóde validate() triedy AlphaNumericValidator.

if (!preg_match('/^[a-zA-Z0-9]+$/', $value, $matches)) {
    $this->context->buildViolation($constraint->message)
        ->setParameter('{{ string }}', $value)
        ->addViolation();
}

V tejto podmienke máme regulárny výraz, ktorý očakáva buď písmená alebo znaky. Ak hodnota nesplní regulárny výraz, vytvorí sa ConstraintViolation.

<?php

// custom_constraint.php

require('vendor/autoload.php');

use Symfony\Component\Validator\Validation;
use App\Validator\Constraints as MyAssert;
use App\Validator\Constraints\AlphaNumeric;

$name = 'Peter^';
$constraint = new MyAssert\AlphaNumeric;

$validator = Validation::createValidatorBuilder()->getValidator();
$violations = $validator->validate($name, $constraint);

// dump($violations);

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    echo $violations->get(0)->getMessage();
}

Príklad ukazuje použitie nášho nového pravidla.

$ php custom_constraint.php
The string "Peter^" contains an illegal character: it can only contain letters or numbers.

Po spustení programu dostaneme takúto chybovú hlášku.

Preklad chybových hlášok

Pre preklad chybových hlášok do iných jazykov použijeme Translator, ktorý máme v balíčku symfony/translation.

Preklady sa nachádzajú v súboroch, ktoré môžu mať rôzny typ. Odporúčaný je XLIFF súbor. Jedná sa o špecializovaný XML formát. Súbory sa umiestňujú do preddefinovaných adresárov. Adresár translations, ktorý použijeme v našom príklade, má z nich najvyššiu prioritu.

<?xml version="1.0"?>
<!-- translations/validators.en.xlf -->
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="en" datatype="plaintext"
            original="file.ext">
        <body>
          <trans-unit id="name.not_blank">
                <source>name.not_blank</source>
                <target>User name should not be blank</target>
            </trans-unit>
        </body>
    </file>
</xliff>

Tento súbor je pre angličtinu. K reťazcom sa dostaneme pomocou identifikátorov. V našom prípade máme name.not_blank.

<?xml version="1.0"?>
<!-- translations/validators.sk.xlf -->
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" target-language="sk" datatype="plaintext"
            original="file.ext">
        <body>
          <trans-unit id="name.not_blank">
                <source>name.not_blank</source>
                <target>Užívateľské meno nesmie byť prázdne</target>
            </trans-unit>
        </body>
    </file>
</xliff>

Tento súbor je pre slovenčinu.

<?php
// translate_val.php

require('vendor/autoload.php');

use Symfony\Component\Validator\Validation;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Translation\Loader\XliffFileLoader;

$locale = 'sk';

$translator = new Translator($locale);

$translator->addLoader('xlf', new XliffFileLoader());
$translator->addResource('xlf', 'translations/validators.en.xlf', 'en', 'validators');
$translator->addResource('xlf', 'translations/validators.sk.xlf', 'sk', 'validators');

$validator = Validation::createValidatorBuilder()
    ->setTranslator($translator)
    ->setTranslationDomain('validators')
    ->getValidator();

$name = '';
$constraint = new Assert\NotBlank(['message' => 'name.not_blank']);

$violations = $validator->validate($name, $constraint);

// dump($violations);

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    echo $violations->get(0)->getMessage();
}

V príklade použijeme preklady pri validácii jednej premennej.

$locale = 'sk';

Zvolíme slovenskú lokalizáciu.

$translator = new Translator($locale);

$translator->addLoader('xlf', new XliffFileLoader());
$translator->addResource('xlf', 'translations/validators.en.xlf', 'en', 'validators');
$translator->addResource('xlf', 'translations/validators.sk.xlf', 'sk', 'validators');

Vytvoríme Translator a priradíme súbory prekladov. Preklady rozdeľujeme do skupín, ktorým sa hovorí doména (domain). V našom prípade máme doménu validators.

$validator = Validation::createValidatorBuilder()
    ->setTranslator($translator)
    ->setTranslationDomain('validators')
    ->getValidator();

Pri vytváraní validátoru mu priradíme translator a doménu.

$constraint = new Assert\NotBlank(['message' => 'name.not_blank']);

Nakoniec validačnému pravidlu priradíme v atribúte message prislúchajúci identifikátor prekladu.

$ php translate_val.php
Užívateľské meno nesmie byť prázdne

Po spustení programu dostaneme takýto výstup.

Validácia pomocou XML súboru

V nasledujúcom príklade si zapíšeme validačné pravidlá do externého XML súboru.

<?php

// src/Entity/User.php

namespace App\Entity;

use Symfony\Component\Validator\Constraints;

class User
{
    private $first_name;
    private $last_name;
    private $email;

    public function __construct(string $first_name, string $last_name,
            string $email) {

        $this->first_name = $first_name;
        $this->last_name = $last_name;
        $this->email = $email;
    }

    public function __toString()
    {
        return "$this->first_name $this->last_name $this->email";
    }
}

Máme objekt User, ktorý má tri atribúty. Tieto atribúty budú mať validačné pravidlá umiestnené v XML súbore.

<?xml version="1.0" encoding="UTF-8" ?>
<!-- config/validation.xml -->
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
        https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">

    <class name="App\Entity\User">
        <property name="first_name">
            <constraint name="NotBlank"/>
        </property>
        <property name="last_name">
            <constraint name="NotBlank"/>
        </property>
        <property name="email">
            <constraint name="Email"/>
        </property>
    </class>
</constraint-mapping>

V tomto XML súbore si zadefinujeme validačné pravidlá. Atribúty sa definujú pomocou property tagov a pre pravidlá slúžia constraint tagy.

<property name="first_name">
    <constraint name="NotBlank"/>
</property>

Pre atribút first_name aplikujeme pravidlo NotBlank, teda meno užívateľa nesmie byť prázdne.

<?php
// xml_validation.php

require('vendor/autoload.php');

use App\Entity\User;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validation;

$user = new User('', 'Novak', 'pnovak@examplecom');

$validator = Validation::createValidatorBuilder()
        ->addXmlMapping('config/validation.xml')->getValidator();
$violations = $validator->validate($user);

$messages = [];

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    foreach ($violations as $violation) {

        $messages[$violation->getPropertyPath()] = $violation->getMessage();
    }

    dump($messages);
}

V príklade validujeme objekt User pomocou XML súboru. Pri vytváraní objektu validátora použijeme metódu addXmlMapping(), ktorou určíme názov validačného XML súboru.

$ php xml_validation.php
array:2 [
    "first_name" => "This value should not be blank."
    "email" => "This value is not a valid email address."
]

Po spustení programu dostaneme takýto výpis.

Validácia pomocou anotácií

Pravdepodobne najčastejší spôsob validácie v súčasnosti v Symfony aplikáciách je pomocou anotácií.

<?php

// src/Entity/User.php

namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class User
{
    /**
     * @Assert\NotBlank
     */
    public $first_name;

    /**
     * @Assert\NotBlank
     */
    public $last_name;

    /**
     * @Assert\Email
     */
    public $email;

    public function __construct(string $first_name, string $last_name,
            string $email) {

        $this->first_name = $first_name;
        $this->last_name = $last_name;
        $this->email = $email;
    }

    public function __toString()
    {
        return "$this->first_name $this->last_name $this->email";
    }
}

Validačné pravidlá sa nachádzajú v anotáciách, ktoré sú umiestnené v PHP komentároch.

/**
 * @Assert\NotBlank
 */
public $first_name;

Na atribút $first_name aplikujeme pravidlo @Assert\NotBlank. Ako sme si už uviedli vyššie, v Symfony je zaužívaný postup používať alias Assert.

<?php

// annot_val.php

require('vendor/autoload.php');

use App\Entity\User;

use Symfony\Component\Validator\Validation;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Symfony\Component\Validator\Constraints as Assert;

$user = new User('', 'Novak', 'pnovak@examplecom');

$loader = require __DIR__ . '/vendor/autoload.php';
AnnotationRegistry::registerLoader(array($loader, 'loadClass'));

$validator = Validation::createValidatorBuilder()
        ->enableAnnotationMapping()->getValidator();
$violations = $validator->validate($user);

$messages = [];

if (0 === count($violations)) {

    echo 'validation passed';
} else {

    foreach ($violations as $violation) {
        $messages[$violation->getPropertyPath()] = $violation->getMessage();
    }
}
dump($messages);

Tento príklad validuje objekt User pomocou anotácií.

$loader = require __DIR__ . '/vendor/autoload.php';
AnnotationRegistry::registerLoader(array($loader, 'loadClass'));

Toto je režijný kód pre spojazdnenie autoloadingu pre Doctrine anotácie. Musíme ho použiť, lebo pracujeme s terminálovou aplikáciou. V Symfony webových aplikáciách je takýto režijný kód už pre nás automaticky vygenerovaný.

$validator = Validation::createValidatorBuilder()
        ->enableAnnotationMapping()->getValidator();
$violations = $validator->validate($user);

Pri generovaní validátora nastavíme validáciu pomocou Doctrine anotácií metódou enableAnnotationMapping().

$ php annot_val.php
array:2 [
    "first_name" => "This value should not be blank."
    "email" => "This value is not a valid email address."
]

Pri spustení programu dostaneme dve chybové hlášky.

V tento časti seriálu sme sa podrobnejšie zaoberali validáciou dát pomocou Symfony Validator komponenta. V ďalšej časti seriálu si spojíme validáciu s formulármi a flash správami.