Přejít k navigační liště

Zdroják » PHP » Symfony po krůčkách – Validator (2)

Symfony po krůčkách – Validator (2)

Články PHP

V tomto díle seriálu o Symfony komponentách se ještě jednou vrátíme ke komponentě Validator. V minulém díle jsme si ukázali validaci skalárních hodnot a polí. Tentokrát se podíváme na validaci objektů a seskupování validačních pravidel do logických skupin.

Nálepky:

Zdrojový kód všech příkladů uvedených v tomto článku najdete v repozitáři na GitHubu.

Validace objektů

Jak jsme si ukázali v minulém díle, při validaci hodnot a polí se všechna validační pravidla přímo zadávají jako druhý parametr metody validate() např. validate(‘’, new NotBlank()). U validace objektů je to trochu jinak. Pro ověření, jestli je objekt validní, slouží validační pravidlo Valid. To ověří, jestli objekt splňuje všechna pravidla. Validace objektu tedy vypadá následovně:

// objekt validation
$validator->validate($object, new Valid());

// second argument can be omitted, Valid Constraint is default value
$validator->validate($object);

Jak ale validátor ví, že je objekt validní? Kde jsou nadefinovaná všechna validační pravidla pro jednotlivé vlastnosti objektu? Pravidla definujeme již při definici třídy a tato pravidla jsou s touto třídou přímo svázaná. Při validaci objektu pak validátor tato pravidla načte a použije pro validaci jeho hodnot. Jak taková definice vypadá?

Validátor v základu dokáže načíst pravidla nadefinovaná ve formátech XML, YAML, nebo pomocí anotací či přímo v PHP. Každá ze zmíněných možností, kromě čistého PHP, ale vyžaduje instalaci dodatečné závislosti, která zajistí řádné načtení a parsování daného formátu.

Abyste mohli využít XML konfiguraci, musíte si přes Composer do vašeho projektu doinstalovat komponentu symfony/config. Pro YAML je vyžadována komponenta symfony/yaml a pro anotace pak 2 komponenty z projektu Doctrine, a to doctrine/annotations a doctrine/cache. Pro jednoduchost si konfiguraci ukážeme ve formátu YAML.

Protože chceme využívat formát YAML, musíme doinstalovat patřičnou závislost.

composer require symfony/yaml

Nyní si vytvořme třídu Person. Pro jednoduchost mají všechny její vlastnosti public viditelnost. Mohou být i protected nebo private, pak bychom si ale pro tyto vlastnosti museli nadefinovat patřičné gettery a settery a nebo alespoň umožnit nastavení vlastností přes konstruktor. Validátor hodnoty vlastností získává přes reflexi, takže gettery a settery pro validaci vlastností nevyžaduje.

// app/entity/Person.php
<?php

namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class Person
{
   public $name;
   public $card;
   public $password;

   public function isPasswordLegal()
   {
       return ($this->name !== $this->password);
   }

   public static function validate(self $object, ExecutionContextInterface $context)
   {
       // somehow you have an array of "fake names"
       $fakeNames = ["FakePetr"];

       // check if the name is actually a fake name
       if (in_array($object->getName(), $fakeNames)) {
           $context->buildViolation('This name sounds totally fake!')
               ->atPath('name')
               ->addViolation();
       }
   }
}

Dále si pro tuto třídu nadefinujme validační pravidla, která umístíme do samostatného souboru validation.yml.

// config/validation.yml
App\Entity\Person:
   properties:
       name:
           - NotBlank: ~
       card:
           - NotBlank: ~
           - CardScheme: {schemes: ["MASTERCARD", "VISA"]}
       password:
           - NotBlank: ~
           - Length: {min: 8}
   getters:
       passwordLegal:
           - 'IsTrue': {message: 'Password cannot match your name'}
   constraints:
       - Callback: {callback: [App\Entity\Person, validate]}

Všimněte si, že všechna validační pravidla pro třídu Person jsou uvedena pod klíčem App\Entity\Person, což je její fully qualified class name (FQCN) – název třídy i s namespacem.

Pravidla jsou dále rozdělena do tří bloků, kde každý je uvozen specifickým klíčem: properties, getters a constraints. V sekci properties definujeme pravidla pro vlastnosti objektu. V sekci getters definujeme pravidla pro metody objektu, které začínají prefixem get, is nebo has. Tento prefix se pak v definici u názvu metody neuvádí. V sekci constraints uvádíme validační pravidla, která je možné aplikovat na celý objekt, např. pravidlo Callback.

Výborně! Třídu a její validační pravidla máme. Pojďme se nyní podívat na validátor v akci. Vytvoříme si následující skript:

// scripts/objectValidation.php
require_once __DIR__ . '/../vendor/autoload.php';

use App\Entity\Person;
use Symfony\Component\Validator\ValidatorBuilder;

// builder
$builder = new ValidatorBuilder();
$builder->addYamlMapping(__DIR__.'/config/validation.yml');

// validator
$validator = $builder->getValidator();

echo (string) $validator->validate(new Person());

V příkladu můžete vidět, že ValidatorBuilder dostane cestu k souboru validation.yml s definicí validačních pravidel. Vytvořený validátor tato pravidla načte a použije při validaci objektu typu Person. Pokud tento skript spustíme, měli bychom na výstupu vidět následující výpis s chybami:

// output
Object(App\Entity\Person).name: This value should not be blank.
Object(App\Entity\Person).card: This value should not be blank.
Object(App\Entity\Person).password: This value should not be blank.
Object(App\Entity\Person).passwordLegal: Password cannot match your name

Fajn, náš validátor funguje. Chybové zprávy ale nejsou ideální. Všimněte si poslední chyby. Ta nás upozorňuje, že se jméno a heslo uživatele nesmí shodovat. Taková chyba je pro uživatele ve chvíli, kdy ještě nevyplnil ani jméno ani heslo, spíše na obtíž. Tuto chybu bychom potřebovali zobrazit až v případě, kdy položky jméno i heslo vyplněné jsou a shodují se. V těchto situacích mohou přijít vhod již zmiňované skupiny validačních pravidel.

Skupiny validačních pravidel

Ve výchozím stavu, kdy při definici validačních pravidel explicitně neuvedeme, do které skupiny je chceme zařadit, spadají všechna nadefinovaná pravidla do skupiny s názvem Default a také do skupiny nesoucí název samotné třídy, pro kterou jsou definovaná. V našem případě by se skupina jmenovala Person.

My si můžeme pravidla přidat do libovolného počtu dalších skupin. Pojďme se podívat, jak na to a k čemu je to vlastně dobré.

// config/validation.yml
App\Entity\Person:
   properties:
       name:
           - NotBlank: ~
       card:
           - NotBlank: ~
           - CardScheme: {schemes: ["MASTERCARD", "VISA"]}
       password:
           - NotBlank: ~
           - Length: {min: 8}
   getters:
       passwordLegal:
           - 'IsTrue': {message: 'Password cannot match your name', groups: [Strict]}
   constraints:
       - Callback: {callback: [App\Entity\Person, validate]}

Co se změnilo? Validační pravidlo pro metodu isPasswordLegal jsme začlenili do skupiny Strict. Pozor, tato pravidla už nyní nepatří do skupiny Default ani Person. Pokud bychom chtěli, aby tam pravidla stále patřila, musíme je explicitně v poli vyjmenovat, např. [Default, Strict].

Podle uvedené konfigurace by se nyní při volání metody validate($person) spustily pouze pravidla pro vlastnosti name, password a pravidlo Callback. Při validaci, kdy ve třetím parametru metody validate() explicitně neuvedeme, kterou skupinu pravidel chceme použít, se totiž spouští pouze pravidla ze skupiny Default.

Pokud bychom nyní chtěli, aby se při validaci, stejně jako v předešlém příkladě, spustila naráz všechna nadefinovaná pravidla, museli bychom metodu zavolat takto:

$validator->validate(new Person(), new Valid(), ["Default", "Strict"]);

Jak ale docílit toho, aby se skupina pravidel Strict spustila až ve chvíli, kdy bez chyby prošla všechna pravidla ze skupiny Default?

Sekvence skupin

Komponenta Validator nám umožňuje pro každou třídu nastavit tzv. sekvenci skupin, která určuje pořadí, v jakém se mají jednotlivé skupiny pravidel při validaci objektu spouštět. Skupiny pravidel ze sekvence se na objekt aplikují jedna po druhé a jen v případě, že všechna validační pravidla z předchozí skupiny prošla bez chyby. Jakmile dojde k chybě, validátor vrátí nalezené chyby a další skupiny pravidel již nespouští.

Požadované chování tedy můžeme zajistit tím, že do našeho souboru validation.yml přidáme následující sekvenci skupin:

// config/validation.yml
App\Entity\Person:
   ...
   group_sequence:
       - Person
       - Strict
   ...

Sekvence skupin se tímto stává výchozí skupinou, to znamená, že pokud spustíme validaci pro skupinu Default, spustí se nadefinovaná sekvence. Podívejte se na ukázku kódu.

// person object
$person = new Person();

// validation of "empty" object
echo (string) $validator->validate($person);

// output
Object(App\Entity\Person).name: This value should not be blank.
Object(App\Entity\Person).card: This value should not be blank.
Object(App\Entity\Person).password: This value should not be blank.

// set some properties of person object
$person->name = Petr;
$person->password = Petr;

// validation of person object
echo (string) $validator->validate($person);

// output
Object(App\Entity\Person).card: This value should not be blank.
Object(App\Entity\Person).passwordLegal: Password cannot match your name.

Možná vám stále vrtá hlavou, jaký je rozdíl mezi skupinami Default a Person. Pokud validujeme čistě objekt Person a nepoužíváme sekvence skupin, tak mezi nimi není rozdíl žádný. Skupinu Default můžeme chápat jako takový alias pro skupinu Person. Podívejme se ale na dvě situace, kdy je rozdíl patrný.

Vezměme si příklad, kdy objekt typu Person má vlastnost address obsahující objekt typu Address. A my jsme na této vlastnosti nadefinovali validační pravidlo Valid, čímž říkáme, že při validaci objektu Person chceme, aby byl validní i objekt Address.

Pokud validaci objektu Person spustíme pro skupinu Default, použijí se všechna výchozí validační pravidla objektu Person a rovněž výchozí validační pravidla objektu Address. Pokud validaci objektu Person spustíme pro skupinu pravidel Person, použijí se výchozí pravidla objektu Person , ale na objektu Address se použijí jen jeho pravidla ze skupiny Person.

Pozor také na to, že skupinu Default není možné použít při definici sekvence skupin. Jak jsme si řekli, sekvence skupin se stává výchozím pravidlem. Pokud bychom v seznamu skupin při definování sekvence uvedli opět skupinu Default, došlo by k zacyklení. V sekvenci tedy vždy používejte skupinu mající název své třídy.

Vlastní validační pravidla

Pokud by vám nestačila výchozí validační pravidla a mechanismy, jenž komponenta Validator v základu nabízí, můžete si vytvořit vlastní validační pravidla a jim odpovídající validátory s vlastní validační logikou. Více se o tom můžete dočíst v dokumentaci.

Zase o krok dál

V dnešním díle jsme si ukázali:

  • jak validovat objekty
  • jak definovat skupiny validačních pravidel
  • jak v danou chvíli aplikovat pouze určitou skupinu pravidel
  • jak spouštět skupiny pravidel v určitém pořadí

Kam jít dál?

Seznam validačních pravidel a jejich možnosti konfigurace najdete na stránkách Symfony v sekci referencí.

Další návody týkající se komponenty Validator pak můžete najít v CookBooku.

Začínáš se Symfony a chceš se rychle posunout dál? Přijď si popovídat na některé z našich setkání v Praze, Brně nebo Ostravě, také se můžeš zúčastnit našeho školení.

Komentáře

Subscribe
Upozornit na
guest
0 Komentářů
Inline Feedbacks
View all comments

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.