Formuláře a DI Factories – best practice

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Jiří Nápravník
Člen | 710
+
0
-

Jaká je best practice pro tvorbu formulářů? Používám variantu, co doporučuje David , nicméně objevil jsem DI factories a je to ideální pro komponenty. Formuláře jsou taky komponenty, ale dá se to tam nějak rozumně napasovat? Přijde mi blbost dělat si DI továrnu na továrnu a dělat továrnu přímo na formulář, dědí UI/Form taky není ideální…

enumag
Člen | 2118
+
0
-

Filip to už sepsal, jenom to nemohu najít nikde na nette.org. https://github.com/…ponenty.texy

David Matějka
Moderator | 6445
+
0
-

v citelne verzi je to tady

Jiří Nápravník
Člen | 710
+
0
-

Díky za odkaz. Nicméně můžete mi upřesnit, který pohled je „lepší“, sám Filip tam píš, každý se hodí na jiný případ, tak kdybyste mohli nahodit, který je lepší, na který případ. Byl bych vděčný.

Nicméně zkusil jsem druhu možnost a nějak ji nemohu rozchodit, mám to takhle:

class PageEditFormFactory extends Control
{

    private $pageFacade;

    public function __construct(PageFacade $facade)
    {
		parent::__construct();
        $this->pageFacade = $facade;
    }

    /**
     * @return BaseForm - tohle je potomek UI/Form
     */
    protected function createComponentForm()
    {
	$form = new BaseForm();
	...
	return $form;
    }
}
interface IPageEditFormFactory
{
	/**
	 * @return \PageModule\Form\PageEditFormFactory
	 */
	function create();
}
class PagePresenter extends AdminPresenter
{
    /**
     *
     * @var PageModule\Form\IPageEditFormFactory
     * @inject
     */
    public $pageEditFormFactory;

    public function createComponentPageEditForm()
    {
        $form = $this->pageEditFormFactory->create();
        $presenter = $this;
        $form->onSuccess[] = function() use ($presenter) {
                $presenter->redirect('default');
            };

        return $form;
    }

v latte pak volám
{control pageEditForm}

a dostanu:
Cannot read an undeclared property PageModule\Form\PageEditFormFactory::$onSuccess.

když si dumpnu v presenteru tu proměnnou form, tak je to objekt: PageModule\Form\PageEditFormFactory , ale tady by to správně měl být už ten Form ne? A když udělám tedy

$form = $this->pageEditFormFactory->create()['form'];

tak při dumpu téhle proměnné už sice mám ten správný form, ale pro změnu skončím na exceptione: Component with name ‚pageEditForm‘ does not exist.

David Matějka
Moderator | 6445
+
0
-

prvni vec, nepojmenovavej to PageEditFormFactory je to matouci, nejedna se o tovarnu, ale o komponentu – lepsi je treba jen PageEditForm
ted k veci, v te verzi, co posilal enumag je chyba, v tom, co jsem posilal ja, je to uz opraveno, musis tam mit

$control['form']->onSuccess[] ..

ja osobne mam radsi vytvorit si vlastni onSuccess event v tom controlu, pak par veci v te ukazce od hosiplana chybi (ta komponenta potrebuje sablonu, resp render() metodu apod.)
takze:

class CategoryForm extends UI\Control
{
    private $database;

    public $onSuccess = array();

    public function __construct(Nette\Database\Connection $database)
    {
        parent::__construct();
        $this->database = $database;
    }

    protected function createComponentForm()
    {
        $form = new UI\Form;
        // mohu použít $this->database

        $form->addSubmit('send', 'Odeslat');
        $form->onSuccess[] = $this->processForm;

        return $form;
    }

    public function processForm($form)
    {
        // mohu použít $this->database
        // zpracovani formulare
        $this->onSuccess($form);
    }

    public function render()
    {
        $this->template->setFile(__DIR__ . '/categoryForm.latte');
        $this->template->render();
    }
}

v categoryForm.latte staci jen

{control form}

nebo ho muzes vykreslit manualne

s timhle zpusobem ti bude v presenteru fungovat to ->onSuccess jak jsi psal…

Jiří Nápravník
Člen | 710
+
0
-

prvni vec, nepojmenovavej to PageEditFormFactory je to matouci, nejedna se o tovarnu, ale o komponentu – lepsi je treba jen PageEditForm

jj, to určitě přejmenuji, to factory tam je ještě jako důsledek nedokončeného refaktoringu metody 1, co jsem používal.

ja osobne mam radsi vytvorit si vlastni onSuccess event v tom controlu, pak par veci v te ukazce od hosiplana chybi (ta komponenta potrebuje sablonu, resp render() metodu apod.)

  • ja mam taky přímo u toho contrulu onSuccess, kde pak volá přímo model apod, ale potřebuji ještě po zpracování přesměrovat, což mi přijde nejlepší v presenteru. nebo je možná jiná varianta?

jinak díky za doplnění, už to funguje. Můžeš ještě poradit, kdy je lepší použít variantu 1 a variantu 2? Mě osobně se více líbí ta druhá varianta, ale nevím ani proč:-)

David Matějka
Moderator | 6445
+
0
-

ja mam taky přímo u toho contrulu onSuccess, kde pak volá přímo model apod, ale potřebuji ještě po zpracování přesměrovat, což mi přijde nejlepší v presenteru. nebo je možná jiná varianta?

nevim, jestli jsme se uplne pochopili – ten onSuccess, o kterym mluvim, je vlastni event vytvoreny pres public $onSuccess = array(); a zavolany pres $this->onSuccess($form); – proste pri zpracovani formulare (pres onSuccess event formulare) zavolam vlastni vytvoreny onSuccess event komponenty (ktery uz nesouvisi s tim eventem formulare) :)) (snad je to srozumitelny)

presmerovani muzes provest i v komponente – a to bud v pres $this->presenter->redirect(), coz ale moc nedoporucuju, nebo pouze pres $this->redirect() (pouze presmerovani v ramci komponenty) – to je vhodne v pripade, ze komponenta je vice „samostatna“
pokud ta komponenta jen obaluje formular, je podle me lepsi flash message a redirect resit v onSuccess v presenteru

variantou 1 myslis jen tovarnu na formular? to jsem pouzival, neni to spatny reseni, ale ted uz na vse pouzivam vlastni komponentu – je to mnohem flexibilnejsi – muzes primo tam upravit vzhled formulare, vyresit snadnejc zpracovani a pracovat s ajaxem

EDIT: jeste k tomu zpracovani formulare, respektive co se mi nelibi na tom zpusobu, co ukazuje David s tou tovarnickou

  1. kdo by cekal, ze „Factory“ se bude starat o zpracovani formulare? to by se melo postarat pouze o jeho vytvoreni
  2. kdyz teda vezmu, ze factory ten formular muze zpracovavat, tak je celkem problem s prenosem nejakych stavovych promennych – v ukazce je $userId, coz se necha jeste celkem snadno prenest pres hidden pole (i kdyz se mi tenhle zpusob taky nelibi), ale je v podstate neresitelne, kdybys chtel predat primo entitu (respektive objekt) $user. v pripade formulare jako komponenty + tovarna predas $user to create() metody tovarny, ktera ho preda konstruktoru komponenty a ta si ho ulozi jako clenskou promennou

Editoval matej21 (11. 10. 2013 17:02)

Jiří Nápravník
Člen | 710
+
0
-

To s tím onSuccess nějak nechápu, ale to nevadí:)

Ad přesměrování, pro mě je lepší tohle opravdu v presenteru, protože ta komponenta obaluje v podstatě jen formulář a pokud bych použil v jiném preseneru musel bych tam if-ovat, kvůli redirectu.

Ano variantou 1 jsem myslel factory na formulář, co doporučoval David. Ok, ka jsme rád, že jsme se shodli, že je lepší komponenta, díky za důvody!

David Matějka
Moderator | 6445
+
0
-

EDIT: zmenil jsem nazev vlastni udalosti z onSuccess na onFormProcessed, aby bylo jasno, ktera je ktera

ok, tak jeste k tomu onSuccess :)
jedna se o vlastni udalost
formular ma v nette udalost onSuccess, na kterou muzu navazat ruzny callbacky.
v komponente si vytvorim vlastni udalost onFormProcessed, to udelam takto:

class PageEditForm extends Control
{
	public $onFormProcessed = array();
	...
}

ted budu moci pridavat na tuto udalost callbacky treba z presenteru:

//presenter
public function createComponentPageEditForm()
{
	$control = $this->pageEditFormFactory->create();
	$presenter = $this;
	$control->onFormProcessed[] = function() use ($presenter) {
		$presenter->redirect('default');
	};
	return $control;
}

(ta anonymni fce neni nyni navazana na onSuccess u formu, ale na moji vlastni udalost onFormProcessed u me vlastni komponenty, kterou budu muset spustit vlastnimi silami)

a posledni vec: mam vytvorenou udalost, mam na ni navazanej callback, ted uz jen staci tu udalost vyvolat, to bude nejlepsi az po zpracovani formulare (ulozeni do db atd). takze nyni na formular, ktery vytvarim v komponente navazu klasickou onSuccess udalost (ted je vazana na formular):

//komponenta
    protected function createComponentForm()
    {
        $form = new UI\Form;
        // mohu použít $this->database

        $form->addSubmit('send', 'Odeslat');
        $form->onSuccess[] = $this->processForm;

        return $form;
    }

nyni ve chvili, kdy Form spusti udalost onSuccess, tak se zavola callback $this->processForm, ten muze vypadat takto:

//komponenta
    public function processForm($form)
    {
        // mohu použít $this->database
        // zpracovani formulare
        $this->onFormProcessed($form);
    }

tam ulozim do databaze nebo cokoliv a pak – konecne onen moment :) – spustim moji definovanou udalost onFormProcessed a tim zavolam callback, ktery jsem navazal v presenteru

je to o par radku kodu vic (nechalo by se asi udelat BaseFormControl, ktery by resil ty udalosti apod a od kteryho bys dedil), ale oproti tomu, co pise hosiplan nemusim mit takovou znalost one komponenty, tedy nemusim vedet, ze obsahuje subkomponentu „form“ na kterou je potreba callback navazat – moji vlastni udalost onFormProcessed vidim na prvni pohled a je jasna. navic je kompatibilni s Kdyby\Events

Editoval matej21 (11. 10. 2013 18:03)

Jiří Nápravník
Člen | 710
+
0
-

Díky moc za upřesnění, to se mi hodně líbí a je to tak opravdu lepší. Budu rád používat..

Jiří Nápravník
Člen | 710
+
0
-

Tak měl bych ještě jeden dotaz, když chci nastavit defaultní hodnoty, případně nějak jinak manipulovat s formulářem. Máš nějaké lepší řešení, než si vytahovat přímo tu kompononentu v komponentně (tedy $this->getComponent(‚pageEditForm‘)->getComponent(‚form‘)) nebo si udělat nějakého společného předka pro control, od kterého pak budu dědit a tam ty metody budou?

kdyz teda vezmu, ze factory ten formular muze zpracovavat, tak je celkem problem s prenosem nejakych stavovych promennych – v ukazce je $userId, coz se necha jeste celkem snadno prenest pres hidden pole (i kdyz se mi tenhle zpusob taky nelibi), ale je v podstate neresitelne, kdybys chtel predat primo entitu (respektive objekt) $user. v pripade formulare jako komponenty + tovarna predas $user to create() metody tovarny, ktera ho preda konstruktoru komponenty a ta si ho ulozi jako clenskou promennou

a ještě kdybys mohl nějak upřesnit tohle, nějak se mi tam nedaří propašovat cokoli přes tu create() metodu…

Editoval Jiří Nápravník (12. 10. 2013 0:32)

David Matějka
Moderator | 6445
+
0
-

nejdriv k druhe otazce:

1. je treba do konstruktoru komponenty pridat jako parametr tu promennou, treba $page

class PageEditForm extends Control
{
...
	protected $page;

	public function __construct($page)
	{
		$this->page = $page;
	}
...

2. je potreba upravit to rozhrani pro tovarnicku

interface IPageEditFormFactory
{
    /**
     * @return \PageModule\Form\PageEditFormFactory
     */
    function create($page);
}

3. je potreba upravit config

services:
	pageEditFormFactory:
		implement: IPageEditFormFactory
		class: PageEditForm
		parameters: [page]
		arguments: [%page%]

(parameters jsou parametry ty tovarny (rozhrani) a arguments jsou do ty komponenty)

a kdyz si ted pri vytvareni ty komponenty v presenteru predas objekt s page

public function createComponentPageEditForm()
{
    $control = $this->pageEditFormFactory->create($this->page);
....
    return $control;
}

($this->page naplnis nekde v action* metode dle parametru presenteru)
tak budes moci s tim objektem pracovat v komponente a pouzit ho treba prave na naplneni defaultnich hodnot formulare

takze tim je vyreseno nastavovani defaultnich hodnot :)
kdyz bys potreboval jeste nejak manipulovat s formularem, tak ti asi nezbyde nez si ho vytahnou jak pises, respektive muzes pouzit zkratky:

$this['pageEditForm']['form'];
//nebo rovnou:
$this['pageEditForm-form'];

nebo lepe – napsat si do ty komponenty metody, ktery budou s tim formem manipulovat a ty je jen budes volat z presenteru. ale myslim, ze vetsinou vubec neni treba s formularem manipulovat z presenteru

Jiří Nápravník
Člen | 710
+
0
-

Díky za upřesnění, to vypadá pěkně. Když budu mít defaultní hodnoty ve formulářové komponentě tak v podstatě manipulovat v presenteru nebude většinou třeba, či-li ot řeší ten problém.

Jenom se mi příliš nelíbí jedna věc a to, že budu muset zavádět instanční proměnnou u presenteru, kterou budu plnit ty defaultní data, co pak půjdou do komponenty. Přeci jen ten atribut budu používat jen v jedné metodě kvůli přenesení dat, či-li v tom presenteru je taková trochu navíc a nevím jestli dostatečně čistý. Ale ono asi nic jiného tam vymyslet nejde co? Snad jen ještě setter komponenty, který bych volal v actionEdit()

A ještě potom, když mám ted v konstruktoru té komponenty:

public function __construct(PageFacade $pageFacade)

a chi aby mi to načítalo z DIC automaticky, jako doposud a zároveň tam mít nějak tu možnost posunout tam třeba ty defaultní hodnoty. to je nějak rozumně řešitelné?

Editoval Jiří Nápravník (12. 10. 2013 16:20)

mattes.gfx
Člen | 8
+
0
-

Řeším teď podobný problém a tohle řešení vypadá docela pěkně!