Mapování tříd na formuláře

Pokud jste si již prošli kapitoli o DataModel a konfiguračním systému, tak jistě neuniklo vaší pozornosti, že tyto entity umí přímo generovat formuláře a je to vlastnost v Jet hojně využívaná, která dokáže ušetřit opravdu hodně práce. Mám dobrou zprávu. Toto generování formulářů není záležitostí pouze DataModelu a konfigurace, ale na formulář lze namapovat vlastně libovolnou třídu.

V kapitole o zachytávání, validaci a předání dat byl ukázka hypotetického registračního formuláře a jeho napojení na třídu. S dovětkem, že v praxi se to dá udělat jinak. Tak si teď úplně stejný příklad, ovšem s využitím definic formuláře, ukažme ještě jednou: use Jet\Form;
use 
Jet\Form_Field;
use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Definition_Interface;

class 
MyUser implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Username',
        
is_requiredtrue,
        
error_messages: [
            
Form_Field::ERROR_CODE_EMPTY => 'Please enter username'
        
]
    )]
    protected 
string $username '';
    
    protected ?
Form $reg_form null;
    
    public function 
getUsername(): string
    
{
        return 
$this->username;
    }
    
    public function 
setUsernamestring $username ): void
    
{
        
$this->username $username;
    }
    
    public function 
getRegForm() : Form
    
{
        if(!
$this->reg_form) {
            
$this->reg_form $this->createForm('reg_form');
        }
        
        return 
$this->reg_form;
    }
}

Tím je vyřešeno vše. Definice formuláře, zachytávání hodnot, validace a také předání zachycených a validních hodnot instanci třídy. O dost méně psaní a práce, že? Rozdíl je ještě markantnější, když potřebujeme další a další vlastnosti a k nim formulářová pole. V tomto případě jméno, e-mail, heslo a tak dále. Už stačí pouze přidávat vlastnosti třídy a k nim definice formulářového pole (a samozřejmě gettery a settery, dobré moderní IDE je vygeneruje automaticky a je to zvyk, který se vyplatí).

Náležitosti třídy mapované na formulář

Aby třída mohla být automaticky namapována na formulář (tedy aby mohla formulář definovat a automaticky vytvářet a napoji se na něj), tak musí splňovat dvě jednoduché věci:

Tím třída získá především metodu createForm.

Pokud vytváříte DataModel, nebo definici konfigurace, tak ale máte splněno. Možnost mapovat formuláře je u takových tříd automatická.

Definice formulářového pole - atributy a jejich parametry

Určitě jste nepřehlédli v Jetu běžné použití atributů, konkrétně Form_Definition. Použití atributů zde již vysvětlovat nebudu a pojďme se rovnou kouknout na seznam parametrů, které definice musí (a může) mít.

Parametr Typ Povinný Význam
type string ano Základní parametr určující typ generovaného formulářového pole. Jedná se o řetězec a je vhodné používat konstanty Form_Field::TYPE_*, nebo vaše konstanty pro vaše vlastní typy formulářových polí.
Nepoužívá se zde název třídy daného typu pole, ale identifikátor typu. Pro vytváření instancí příslušných tříd je použita továrna.
is_required bool ne Indikuje zda formulářové pole bude / nebude označeno jako povinné.
label bool ne Popisek formulářového pole.
help_text string ne Text nápovědy formulářového pole.
help_text asociované pole ne Data nápovědy formulářového pole.
error_messages asociované pole podmíněně ano Texty chybových hlášení k chybovým kódům formulářového pole.
Parametr je povinný pokud pole bude taková hlášení potřebovat. Tedy pokud je například pole povinné, tak určitě bude požadováno chybové hlášení pro kód Form_Field::ERROR_CODE_EMPTY.
default_value_getter_name string ne Název metody, která při vytváření formulářového pole vrátí hodnotu, která bude použita jako výchozí pro toto pole.

Běžně se jako výchozí hodnota bere aktuální hodnota vlastnosti objektu ke kterému formulářové pole náleží. A běžně není nutné metodu definovat. Ovšem může se stát, že hodnota vlastnosti není vhodná jako výchozí hodnota formulářového pole. Může se například jednat o pole objektů, nebo jiná komplexní data. V takovém případě je nutné definovat metodu, která formulářovému poli předá použitelná data.
setter_name string ne Pomocí tohoto parametru lze určit metodu třídy, kterou bude volat zachytávač formulářového pole.

Běžné se Jet pokusí zjistit název příslušného setteru sám (stačí dodržet princip jak je název tvořen všude v Jetu a v ukázkové aplikaci) a pokud příslušnou metodu nenajde, tak předává hodnotu přímo do vlastnosti objektu. Ovšem pokud chcete použít specifickou metodu, tak pomocí tohoto parametru je možné určit její název.
creator callable ne Někdy může nastat situace, kdy definice na vytvoření správně nastaveného formulářového pole nestačí a je nutné pole donastavit nějakou komplexnější logikou. V tom případě je možné definovat volání metody, která jako parametr dostane předgenerované pole a jako návratová hodnota je očekávána konečná podoba pole (tedy instance formulářového pole, která bude brána jako definitivní).

Další parametry jsou závislé na konkrétním typu formulářového pole.

Pod-formulář / Pod-formuláře

Systém mapování formulářů disponuje další zajímavou vlastností a to vnořování formulářů do formulářů. Zní to bláznivě? Pojďme si to opět ukázat na něčem z praxe a opět na mém oblíbeném tématu e-shopu.

Vytváříte e-shop a v dnešní době již pravděpodobně nebude stačit jedna jazyková mutace. Potřebujete tedy, aby vše bylo lokalizovatelné. Například popisky zboží. Ukažme si základní definici zboží s lokalizovatelnými daty (pro zjednodušení bez definice DataModel)

Nejprve si uděláme hlavní třídu produktu: namespace JetApplication;

use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Interface;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Field;
use 
Jet\Locale;
use 
JetApplication\Application_Web;

class 
Product implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    protected 
int $id;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Internal code:'
    
)]
    protected 
string $internal_code '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'EAN:'
    
)]
    protected 
string $EAN '';
    
    
/**
     * @var Product_Localized[]
     */
    
#[Form_Definition(is_sub_forms:true)]
    protected array 
$localized = [];
    
    public function 
__construct()
    {
        foreach( 
Application_Web::getBase()->getLocales() as $locale ) {
            
$this->localized[(string)$locale] = new Product_Localized();
            
$this->localized[(string)$locale]->setLocale$locale );
        }
    }
    
    public function 
getInternalCode(): string
    
{
        return 
$this->internal_code;
    }
    
    public function 
setInternalCodestring $internal_code ): void
    
{
        
$this->internal_code $internal_code;
    }
    
    public function 
getEAN(): string
    
{
        return 
$this->EAN;
    }

    public function 
setEANstring $EAN ): void
    
{
        
$this->EAN $EAN;
    }
    
    public function 
getLocalizedLocale $locale ) : Product_Localized
    
{
        return 
$this->localized[(string)$locale];
    }
}

Ta již obsahuje nějaké základní společné údaje a především vlastnost $localized, která je pro nás teď klíčová. Jedná se o pole objektů třídy Product_Localized a vlastnost má atribut #[Form_Definition(is_sub_forms:true)]

Dále si prosím všimněte, že konstruktor vytváří do této vlastnosti instance podle seznamu lokalizací báze.

A teď se koukneme alespoň na torzo třídy Product_Localized, která reprezentuje vše o produktu co bude vázáno na konkrétní lokalizaci: namespace JetApplication;

use 
Jet\Form_Definition;
use 
Jet\Form_Definition_Interface;
use 
Jet\Form_Definition_Trait;
use 
Jet\Form_Field;
use 
Jet\Locale;

class 
Product_Localized implements Form_Definition_Interface {
    use 
Form_Definition_Trait;
    
    protected 
int $product_id;
    
    protected 
Locale $locale;
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_INPUT,
        
label'Name:'
    
)]
    protected 
string $name '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_WYSIWYG,
        
label'Description:'
    
)]
    protected 
string $description '';
    
    #[
Form_Definition(
        
typeForm_Field::TYPE_FLOAT,
        
label'Price:'
    
)]
    protected 
float $price 0.0;
    
    public function 
getLocale(): Locale
    
{
        return 
$this->locale;
    }
    
    public function 
setLocaleLocale $locale ): void
    
{
        
$this->locale $locale;
    }    
}

Zde již na první pohled nic zvláštního není. Poměrně běžná třída napojená na formulář. Ovšem z prvků tohoto formuláře se stanou prvky formuláře pro editaci produktu.

Zkuste si tedy vytvořit objekt produkt a vytvořit formulář (třeba pro přidání produktu): $product = new Product();
$add_form $product->createForm('add_product');
Stále celkem běžný postup (krom toho, že z praktických důvodů je dobré držet instance formulářů jako singeltony v objektech - viz ukázková aplikace).

Formulář je možné běžně zachytávat: if($add_form->catch()) {
    
$product->save();
}

Ovšem ve view můžeme mít toto: <?=$add_form->field('internal_code')?>
<?=$add_form
->field('EAN')?>

<?php foreach( Application_Web::getBase()->getLocales() as $locale ): ?>
    <?=UI::locale($locale)?>
    <?=$add_form->field('/localized/'.$locale.'/name');?>
    <?=$add_form->field('/localized/'.$locale.'/description');?>
    <?=$add_form->field('/localized/'.$locale.'/price');?>
<?php 
endforeach;?>

A teď kontrolní otázka, soudruzi: Co se stane, když oné bázi přidáme další lokalizaci? No nic nezkoroduje, ale přidá se nám další lokalizace i k produktům a automaticky to ovlivní i formulář.

Jedinou podmínkou je říct, že z těch objektů co jsou v poli chceme jejich formulářové prvky začlenit pomocí atributu:
#[Form_Definition(is_sub_forms:true)].

Pokud by se nejednalo o pole objektů, ale o prostou instanci jiného objektu, tak se postupuje úplně stejně, je pouze malý rozdíl v atributu:
#[Form_Definition(is_sub_form:true)]

Předchozí kapitola
Překlad formuláře
Další kapitola
Jet\Form_Definition_Interface