Hlavní navigace

PHP Jet - Formuláře

22. 1. 2023 14:24 Mirek Marek

Úvod

Vítám vás u dalšího článku :-) V dnešním příspěvku si ukážeme jak je v PHP Jet řešena práce s formuláři.

Když tvoříme aplikace jako jsou různé administrace, informační systémy, ale i REST (či jiné) API (ano, Jet Form se používá i v REST API – ukážeme si) tak formuláře jsou vlastně středobodem aplikace.

Formuláře v PHP Jet jsou věc, která prošla velkou evolucí. Zkusil jsem několik cest jak problematiku řešit, až jsem dospěl k současnému řešení, se kterým jsem opravdu spokojený a které by mohlo posloužiti vám.

Problematiku si rozdělíme na následující okruhy:

  • Obecná definice formulářů
  • Zobrazování formulářů
  • Zachytávání a validace dat
  • Formuláře definované třídou a „naklikané“ v Jet Studiu
  • Použití formulářů v REST API
  • Napojení formulářů na překladač a lokalizovatelnost
  • Na konci článku si vytvoříme z nuly jednoduchý kontaktní formulář (video)
  • A na úplný závěr pár stručných informací co je kolem PHP Jet obecně nového

Definice formulářů

Účelem subsystému formulářů v PHP Jet je formulář zpracovávat ve smyslu zachycení a validace dat, ale rovněž formuláře i zobrazovat. Ale to jsou dvě rozdílné (i když propojené) věci. Ovšem vždy je nutné formulář nějak definovat a na základě této definice následně probíhá jak zachytávání a validace dat a práce s chybami, tak zobrazení formuláře.

Definici formuláře představuje instance třídy Form a definice jednotlivých formulářových polí jsou pak instance tříd Form_Field_*, které reprezentují jednotlivé typy formulářových polí a vstupních dat.

Definice zahrnuje vše co je nutné o formuláři a jeho polích vědět. To znamená vše od názvů polí, výchozí hodnoty, popisky a nápovědu, přes definice chybových hlášek pro různé chybové stavy až po vlastní validátory a zachytávače hodnot. 

Zde je příklad definice formuláře s jedním polem. Pole je povinné, má definované vlastní doplňkový validátor, chybové hlášení a definice je připravená dále pracovat se zachycenou validní hodnotou:

use Jet\Form;
use Jet\Form_Field_Input;

$name_field = new Form_Field_Input(
    name: 'name',
    label: 'Vaše jméno:'
);

$name_field->setIsRequired( true );

$name_field->setErrorMessages([
    Form_Field_Input::ERROR_CODE_EMPTY => 'Prosím zadejte vaše jméno',
    'too_short' => 'Minimální délka jména jsou 3 znaky. Zadané jméno má %curr_len%'
]);

$name_field->setDefaultValue( 'Jára Cimrman' );

$name_field->setValidator(
function( Form_Field_Input $field ) : bool  {
    if(strlen($field->getValue())<3) {
        $field->setError('too_short', [
            'curr_len' => strlen($field->getValue())
        ]);
        return false;
    }

    return true;
});

$name_field->setFieldValueCatcher( function( string $value ) : void
{
    //TODO: Něco udělat se zachycenou a validní hodnotou
    echo $value;
});

$form = new Form( name: 'example_form', fields: [
    $name_field
] );

A pozor! Toto není jediný způsob jak formulář definovat a v praxi je to také ten méně častý. Jak si ukážeme hlavně ve videu, definice formuláře může být vázána na třídu a může být „naklikána“ v Jet Studiu. Ale to zatím nechme být. Pro objasnění principu je nutné začít touto ruční definicí, která je v uvedeném příklad. Ostatně i tento způsob je možné používat a někdy je to i vhodnější cesta.

Zobrazení formulářů

Když už máme formulář definovaný, pak je dobré jej také zobrazit. To se dělá velice jednoduše takto:

<?=$form->start()?>
    <?=$form->field('name')?>
<?=$form->end()?>

Je důležité zdůraznit, že podoba formuláře (v ukázkové aplikaci využívající Bootstrap) není vůbec pevně dána a do PHP Jet není jakkoliv zabudována. Jet nabízí pouze abstraktní model rendererů který je pomyslným mostem mezi vaším view, které takto jednoduše zobrazí formulář a obecnými view, které definici formuláře a rendereru formuláře převedou vše již na konkrétní HTML.

Skutečná view, která formulářová pole převedou na jejich konkrétní vizuální podobu najdete v rámci ukázkové aplikace v těchto adresářích:

  • ~/application/bases/admin/views/form
  • ~/application/bases/web/views/form
  • ~/_tools/studio/application/views/form
  • ~/_installer/views/form

Právě tam najdete view skripty využívající Bootstrap. A ano, ve všech těchto adresářích jsou skripty prakticky totožné. Teoreticky by mohly být na jednom místě, ale po pečlivém zvážení jsme to raději udělal takto, aby bylo možné ihned začít vše upravovat a zároveň aby se nic nerozbilo. 

Protože například pro administraci budete chtít zachovat Bootstrap (příklad – nemusíte, udělejte to jak chcete), ale pro web, či e-shop si uděláte něco vlastního. A co takové Jet Studio? To určitě potřebuje, aby mu na formuláře nikdo nesahal. A to samé instalátor. Tedy to že najdete na několika místech totéž není chyba, ale záměr a má to svou logiku.

Ostatně račte si to upravit jak libo. To kde má systém formulářů hledat své view je věc, kterou je tak jako tak nutné nastavit. A provádí se to například v inicializátorech bází:

namespace JetApplication;

//...
use Jet\SysConf_Jet_Form;
//...

class Application_Web
{
    //...

    public static function init( MVC_Router $router ): void
    {
        //...
        SysConf_Jet_Form::setDefaultViewsDir( $router->getBase()->getViewsPath() . 'form/' );
        //...
    }
    //...
}

A teď zpět k zobrazení. Pochopitelně by nestačilo pouze vypisovat něco již pevně daného, bez možnosti flexibility. Formulářové prvky je nutné dále modifikovat a donastavovat. A to také systém zobrazení formulářů umí. 

Zobrazení prvků je možné všemožně ovlivňovat, přidávat CSS třídy a styly, JavaScript, volitelné atributy HTML tagů a tak dále. Je to velice obsáhlé téma a je nutné „mrknout“ do dokumentace a pohrát si s ukázkovou aplikací. Ale opět stačí pochopit princip a pak už by to mělo být intuitivní. Jeden příklad si ukažme. 

Co takhle řádek s políčkem jméno udělat poloprůhledný? Není problém:

<?php
$form->field('name')->row()->addCustomCssStyle('opacity: 0.5');
?>
<?=$form->start()?>
    <?=$form->field('name')?>
<?=$form->end()?>

Zachytávání a validace dat

Na validaci a zachycení jednotlivého pole jsme již narazili hned v úvodním příklad. Ukažme si teď ještě názorně zachycení a validaci celého formuláře.

Zachycení samotného formuláře se provádí takto:

if(
    $form->catchInput() && //Formulář byl odeslán a je zachycen
    $form->getIsValid() //Formulář byl zvalidován a všechna pole jsou validní
) {

    /*
     * Vrátí všechny hodnoty formuláře v podobě asocciovaného pole
     *
     * V praxi velmi málo používané, ale možné
     */
    var_dump( $form->getValues() );


    /*
     * Získání zachycené hodnoty konkrétního pole
     */
    $form->field('name')->getValue();


    /*
     * Zavolá všechny definované zachytávače hodnot všech polí a postará se tak o předání hodnot na určená místa
     *
     * V praxi používané nejčastěji
     */
    $form->catchFieldValues();

}

Jak uvádím v příkladu, tak v praxi se nejčastěji používá systém zachytávačů hodnot, protože nejčastější je asociace definice formuláře na třídu. To je také nejpohodlnější způsob jak formuláře používat a prakticky si jej ukážeme ve videu.

Při tomto způsobu použití (když je formulář namapován na třídu – ukážeme si v zápětí) stačí při zachycení postupovat takto:

if( $form->catch() ) {
    /*
     * Formulář je odeslán, zachycen, zvalidován.
     * Jeho validní hodnoty jsou již předány na místo určení.
     */
}

Formuláře definované třídou a „naklikané“ v Jet Studiu

Jak jsme již zmínil, tak nejčastější způsob použití formuláře je jeho definice nějakou třídou a napojení na tuto třídu.

V praxi to znamená, že definované vlastnosti třídy tvoří zároveň formulářová pole a tato pole po validaci předají své hodnoty těmto vlastnostem v rámci instance této třídy.

Právě takto je tvořena většina ukázkové aplikace. Právě takto funguje i napojení formuláře na entity v rámci ORM (například ukázkové mikro CMS a administrace), ale i konfigurační entity (ty formuláře, co vyplňujete při instalaci) a tak dále. 

Jde o to, že právě díky tomuto konceptu je možné odstranit velké množství otravné rutinní práce a zároveň zachovat jednoduchost.

Definici formuláře a napojení na formulář může představovat jakákoliv třída. Stačí pouze, když implementuje potřebné rozhraní, použije určený trait a ty vlastnosti tříd předurčené k namapování na formulář mají potřebné definice. A pochopitelně nesmí chybět samotné vytvoření definice konkrétního formuláře, respektive formulářů – složitější entity mohou mít více různých formulářů. Zde je malý příklad, ale lépe si vše ukážeme ve videu, které opravdu vřele doporučuji. 

Teď zatím textový příklad:

namespace JetApplicationModule\ContactForm;

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

class ContactForm implements Form_Definition_Interface
{
    use Form_Definition_Trait;

    #[Form_Definition(
        type: Form_Field::TYPE_INPUT,
        label: 'Vaše jméno:',
        is_required: true,
        error_messages: [
            Form_Field::ERROR_CODE_EMPTY => 'Prosím zadejte Vaše jméno'
        ]
    )]
    protected string $from_name = '';

    //....

    protected ?Form $form = null;

    public function getForm() : Form
    {
        if( !$this->form ) {
            $this->form = $this->createForm( form_name: 'contact_form' );
        }

        return $this->form;
    }

    public function catchForm() : bool
    {
        return $this->getForm()->catch();
    }
}

A ano, takové definice můžete naklikad v Jet Studiu v nástroji k tomu určeném. Opět opakuji – viz video. 

Ale připomínám, že můžete, ale nemusíte. Klidně můžete definici vytvořit ručně, nebo si ji „předklikat“ a ručně upravovat. Jet Studio je pouze pomocný nástroj a ne mandatorní věc – to platí obecně.

Více viz video níže ;-)

Použití formulářů v REST API

Formuláře nemusí být nutné vizuální. Mohou sloužit pouze k zachycení a validaci vstupních dat, která mohou pocházet z libovolného zdroje – ne nutně z POST či GET. 

Stejně tak je možné pracovat s validačními chybami formulářů. Ty lze například jednoduše převést na JSON a například poslat v odpovědi na chybný REST požadavek.

Koncepce má to jednu zásadní výhodu – formulář často stačí definovat jednou. Například entita Article má jednu společnou definici formuláře pro přidání a editaci článku. A tyto formuláře slouží jak v administraci, tak v ukázkovém REST API a mohou sloužit na dalším místě. Napadá mě další využití například v nějakém nástroji pro hromadný import dat. Fantazii se meze nekladou.

Ale teď si pojďme ukázat konkrétně právě REST API.

Koukněme se na modul Content.Articles.REST, což je jedene z ukázkových modulů REST API serveru.

Konkrétně tedy nahlédněme na třídu JetApplicationModule\Con­tent\Articles\REST a její metodu add_Action sloužící pro přidávání článku (pomocí REST API):

public function add_Action(): void
{
    $article = new Content_Article();

    $form = $article->getEditForm();

    $data = $this->getRequestData();

    $form->catchInput( $data, true );

    if( $form->validate() ) {
        $form->catchFieldValues();

        $article->save();

        Logger::success(
            event: 'article_created',
            event_message: 'Article created',
            context_object_id: $article->getId(),
            context_object_name: $article->getTitle(),
            context_object_data: $article
        );

        $this->responseData( $article );
    } else {
        $this->responseValidationError( $form->getValidationErrors() );
    }

}

Vytvoření této operace v rámci REST API je vlastně absolutně triviální. Má to jedno jediné specifikum. Data nejsou načtena přímo z POST, ale s JSON v rámci těla REST HTTP požadavku a je vynucené zachycení formuláře. To dělá tento kousek kódu:

$data = $this->getRequestData();

$form->catchInput( $data, true );

A pokud je něco špatně, tak se REST klient dozví co přesně, protože jsou mu předány chybové hlášky z vazbou na vstupní pole:

$this->responseValidationError( $form->getValidationErrors() );

To je vše – nic složitějšího v tom není.

Napojení formulářů na překladač a lokalizovatelnost

Protože je PHP Jet jet stavěn primárně pro prostředí EU, tak pochopitelně neschází zabudovaná podpora lokalizovatelnosti a překladů přímo do formulářů (ale nikdo vám neříká, že to musíte používat ;-) ).

O překladech se dozvíte více v dokumentaci a také to uvidíte „v akci“ ve videu na konci článku – teda vlastně hned teď záhy ;-)

Ukázka – kontaktní formulář

Účelem článku nebylo obsáhnout vše co Jet Form umí, ale nastínit filozofii a naznačit schopnosti celé té věci. Ovšem jsem toho názoru, že praxe je lepší než teorie a tak si to ukažme rovnou prakticky na jednoduchém příkladu:

(Mimochodem: Už dorazil fantomový napaječ pro mikrofon – zvuk je již o hodně lepší. Ještě jednou díky za rady!)

Co nového v PHP Jet

A již tradičně krátká zpráva o tom, co se od posledního článku událo s PHP Jet:

  • Doplnil jsem podporu zabudovaného web serveru PHP.  Jet je tedy možné rychle vyzkoušet bez instalace na plnohodnotný web server.
  • Na GitHub jsme zpnul diskuze. Po zvážení všech možnosti se mi to jeví jako nejlepší cesta jak zřídit fórum.
  • Připravil jsem stránku pro rychlé seznámení s PHP Jet určenou pro nově příchozí uživatele.
  • Pár drobných oprav a vylepšení.
  • Začala se již chystat anglická mutace webu a dokumentace. Můj domov ČR bude vždy o něco napřed, ale na web přichází poměrně dost lidí z celého světa a PHP Jet by do budoucna neměl být pouze lokální žáležitostí.
  • Začala práce na podpoře PostgreSQL, Oracle a MS SQL.
  • EasyDeployer již má podporu SFTP/SCP a dotal pár vylepšení (např. první vývojářský účet je možné vytvořit již během instalace).
  • PHP Jet i Easy Deployer jsou od teď i na sourceforge.net

A to je pro dnešek vše. Velice děkuji za Váš čas. Pokud chcete, vše si vyzkoušejte, ptejte se a tak dále.

Brzy se ozvu s dalšími články, které již teď chystám.

Sdílet

  • 23. 1. 2023 1:16

    BoneFlute

    1/ Chápu to dobře, že validace formuláře je řešena pouze na serverové straně, a negeneruje se tedy zároveň validace u klienta?

    2/ Dělal jste nějaké porovnání s Nette a Symfony formuláři?

    3/ To použití formuláře na API mi přijde vtipné. Inspirativní nápad.

  • 24. 1. 2023 8:12

    Mirek Marek

    Díky za dotazy

    1) V dnešní době už umí validovat formulářová pole přímo prohlížeč. Tedy není nutné se s tím trápit a doplňovat JS. Ovšem absolutně nic nebrání frontend udělat na jakékoliv technologii a doplnit tam cokoliv. View není integrální součástí Jet, ale je to v aplikačním prostoru s tím, že v základu je tam Bootstrap, ale vývojář si snadno může udělat cokoliv.
    V rámci Jet je de facto model, který přenáší informace z view aplikace do view prvků samotných. Jak budou prvky vypadat, to už je čistě jen na vás. To Jet jakkoliv neurčuje.

    2) Ano,. porovnávám neustále. Pokud bude zájem, udělám opět srovnávací článek.
    Každopádně toto je výsledek dvanácti let praktického používání. A používání na projektech, které jsou hlavně o formulářích.
    Především porovnávám pracovní efektivitu. A proto jsem vytvořil Jet. Jsem schopen v něm pracovat daleko efektivněji a dělat lepší aplikace (za méně času) než v čemkoliv jiném.
    Dokonce jsem se opakovaně setkal s tím, že má konkurence kroutila hlavou (pomyslně i doslova) a ptala se jak je možné určitou aplikaci a projekt vyvinout v takové kvalitě, rozsahu a tak rychle ... A já tu odpověď na otázku "jak" teď postupně uvolňuji do světa.
    To nejsou subjektivní věci, ale měřitelné - reálná praxe.
    V Jet platí, že méně je více: méně času, měně starostí, méně zdrojáků, méně složitostí = lepší projekty za lepší čas.
    Články budu vydávat cca každé dva týdny a dostanu se určitě k dalším a dalším porovnáním.

    3) Děkuji za pochvalu ;-)
    Ono je to ve své podstatě logické ... Entitu zakládáte tak jako tak. Jen jednou z adminstrace, jednou z REST API, jednou z importu.

    Proto formuláře nejsou zaměřené na vizuální prezentaci formulářů. To je sice také věc důležitá a v Jet propracovaná, ale jiná.
    Jet Form je o práci se vstupy. A proč ty vstupy omezovat jen na to co pošle uživatel z formu, když vstup je stále vstup a je irelevantní, kde je zdroj? Validační pravidla pro entitu jsou vždy totožná. Tedy není žádný důvod mít víc validací - to je vlastně neefektivní to tak dělat.

  • 25. 1. 2023 16:42

    BoneFlute

    1/ O to nejde.
    Nette formuláře fungují tak, že si vytvořím validační pravidla, a ty se mi pak použijí jak na serveru, tak na klientu. Ve vašem případě, pokud jsem to pochopil správně, musím tu validaci pro klienta psát ručně. To je mínus.

    2/ Určitě mě to zajímá. I kdyby se nakonec ukázalo, že máte mezery ve znalostech konkurence, tak ale vynikne vaše vize. Což bude užitečné.

    3/ Pouvažoval bych o přejmenování. Formuláře jsou vizuálu formuáře.

  • 10. 2. 2023 11:05

    Mirek Marek

    1)
    To jak se budou formuláře validovat na straně klienta (v prohlížeči) není věc PHP, ale JavaScriptu. Teda potažmo prohlížeče samotného, ale pokud to nestačí, tak JS
    Jet je PHP framework, který umožňuje použít jakoukoliv technologii pro frontend, ale drží se svého "kopita" - jak se říká.
    Jet nemá ambice plést se nějak zásadně do frontentdu. To je jiné hřiště. A je tam spousta schopných hráčů.
    Udělat view pro formuláře s nějakou JS validací je z mého pohledu naprosto triviální věc (triviální z pohledu doplnění do Jet aplikace), kterou si ale každý může udělat dle svých potřeb a preferencí na libovolné FE technologii.
    Jak jsme říkal: Jet je PHP, svou práci dělá na serveru a tam je primárně jeho hřiště.

    Jasně, ukázková aplikace má i frontend - jinak by neměla smysl, ale to není a nemá být hlavní pole působnosti Jetu. Ostatně specialisté na frontend udělají svou práci lépe než já.

    Ovšem velice Vám děkuji za námět. Za zamyšlenou to určitě stojí a implementačně to není nic složitého a nemělo by to nabourávat kompatibilitu (změní se view v aplikačním prostoru, ne API knihovny).

    Tedy: Díky, píšu si do kolonky "pořádně si to rozmyslet".

    2)
    Nebojte, konkurenci znám relativně dobře a neustále porovnávám a zkoumám. To je nekonečný proces.
    A proto můžu směle tvrdit a dokázat, že Jet je lépe navržen a prověřen praxí.

    3)
    Formuláře jak prvek frontendu, tak se musí zpracovávat na serveru.
    To je z mého pohledu backend vývojáře extrémně důležité a proto je na to kladen důraz.
    Tam se determinuje efektivita aplikace (jak vývoje, tak běhu), ale i bezpečnost (řešená na vstupu).
    Od toho to bylo primárně navrženo - pro tento účel a proto se to jmenuje formuláře.
    To že to lze bez problému použít i jinak je pouze důkazem správnosti SW architektury a celého návrhu a dodržení smyslu OOP. (Teda než někdo v budoucnu najde ještě lepší architekturu ....)

    Ale Vy máte naprostou pravdu v tom, že název nemusí být výstižný.
    Ano, za úvahu by to stálo - zcela určitě.

    Ovšem i kdybych si to rozmyslel a našel nějaký nový supervýstižný název, tak to nepřejmenuji. Bylo by velice nešťastné framework neustále měnit a to pouze ze subjektivních důvodů. To je přesně to co nechci a jeden z důvodů proč Jet vznikl.
    V žádném případě se nebude překotně měnit. To je neslučitelné s použitím ve firemním prostředí. Ostatně o tom jsem psal na samém začátku.

    Jet již má nějaké uživatel. Jasně, z pohledu uveřejnění a uvedení na světlo světa je to ještě novorozenec (s vyzrálou duší), ale již je několik lidí, kteří jej používají, nehledě na to, že já sám na tom mám postaveno několik projektů a chystanou e-commerce platformy. Teď už žádná zásadní změna nepřipadá v úvahu.

    Ale kvituji Váš názor. Opravdu na tom něco je a téma k zamyšlení je to bezesporu.