Best Practise: jak zpracovávat formuláře

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Achse
Člen | 44
+
+2
-

Vytvořil jsem formulář podle: https://doc.nette.org/…s/form-reuse

Podle návrhu který je tam zmíněný se onSuccess dají dvě metody. Jedna která formulář zpracuje (už v továrničce) a druhá která vyřeší přesměrování.

Jenže: Co dělat když potřebuji výstup z první metody nějak zpracovat a reflektovat podle něj chování presenteru?

– Představa toho, že se v presenteru úplně vyhnu volání modelu pro zpracování formuláře mi přijde děsně cool a nechci se ji vzdávat.
 – Potřebuji ale zpracovat výstup do: flashMessages / rozdílné redirecty (zejména na základě chyby) atd…

Ukázka kódu:

public function processFormX(Form $form) {
    if ($form->isValid()) {
        $values = $form->getValues();
            try {
                $this->injectedService->doY($values, $this->user);
                $this->flashMessage("Hell Yeah!", 'success');

            } catch (SignatureFileNotFoundException $e) {
                $this->flashMessage("... #1"), 'danger');

            } catch (NoActivitiesException $e) {
                $this->flashMessage("... #2"), 'danger');

            } catch (EntityAlreadyExistsException $e) {
                $this->flashMessage("... #3"), 'danger');

            } catch (InvalidStateException $e) {
                $this->flashMessage("... #4"), 'danger');
            }
        }
        $this->redirect('this');
    }

Nějaký nápad jak to navrhnout lepé? Nebo je toto správné řešení a hold se volání processForm v presenteru nelze vyhnout?

Editoval Achse (16. 6. 2014 9:52)

Vojtěch Dobeš
Gold Partner | 1316
+
+6
-

Osobně to řeším tak, že každý můj formulář (říkejme mu obecně komponenta) dědí od UI\Control, a formulář má jako subkomponentu. Uvnitř komponenty mám i modelové zpracování formuláře. Komponenta pak definuje onSuccess (nebo třeba onArticlePublished – fantazii se meze nekladou), do kterého se už věší jen presenter s nějakým tím přesměrováním, flash messages apod.

use Nette\Application\UI;
use Nette\Security\User;

class ArticlePublishingForm extends UI\Control
{

	/** @var callable[] */
	public $onArticlePublished = [];

	/** @var ArticlePublisher */
	private $articlePublisher;

	/** @var User */
	private $user;

	/** @var Article */
	private $article;


	public function __construct(Article $article, ArticlePublisher $articlePublisher, User $user)
	{
		$this->article = $article;
		$this->articlePublisher = $articlePublisher;
		$this->user = $user;
	}


	protected function createComponentForm()
	{
		$form = new UI\Form;
		// $form-> ...
		$form->onSuccess[] = $this->processForm;
		return $form;
	}


	public function processForm(UI\Form $form, $values)
	{
		// processing values by model
		$this->onArticlePublished($article);
	}

}
interface ArticlePublishingFormFactory
{
	/** @return ArticlePublishingForm */
	function create($article);
}
services:
	- implement: ArticlePublishingFormFactory
	  parameters: [article]
	  arguments: [%article%]
class ArticleComposePresenter
{

	/** @var ArticlePublishingFormFactory @inject */
	public $articlePublishingFormFactory;


	protected function createComponentForm()
	{
		$form = $this->articlePublishingFormFactory->create($this->article);
		$form->onArticlePublished[] = function ($article) {
			$this->flashMessage("Article {$article->name} was published.");
			$this->redirect('default');
		};
	}

}

Editoval vojtech.dobes (16. 6. 2014 13:37)

Achse
Člen | 44
+
0
-

Chápu, prostě uděláš N hook-pointů typu: onEventOccured. A pak je (když například nastane vyjímka) pustíš.

Kód by tedy pakvypadal:

class FormComponentX extend Object {

    // array of callbacks
    public $onSignatureFileNotFound;

    public function processFormX(Form $form) {
        if ($form->isValid()) {
            $values = $form->getValues();
                try {
                    $this->injectedService->doY($values, $this->user);
                    $this->flashMessage("Hell Yeah!", 'success');

                } catch (SignatureFileNotFoundException $e) {
                    $this->fireMyEvents($this->onSignatureFileNotFound);

                } // catch ....
               // ... atd...
        }
    }

Ono, pro dosažení této funkčnosti ani není třeba formulář obalovat imho. Rád bych se vyhnul over-engineering tam kde servis takhle mocného řešení vlastně nepotřebuji.

Každopádně díky za tip, ten se mi líbí → jdu to vyzkoušet napsat.

Editoval Achse (16. 6. 2014 10:11)

Jan Suchánek
Člen | 404
+
0
-

@Oli: Výhoda je, že celej formulář má vlastní latte a vlastní model separe mimo presenter a je to pak pěkně přehledné.

Achse
Člen | 44
+
0
-

Yep, ale když píšu nějaký backend, kde všechno je bootstrap a nemusím nic (zhuleně frontendovýho) řešit, vystačím si s pát úravami v rendereru.

Osobně mám takovou konvenci: kde to jenom trochu jde, manuálnímu renderingu se vyhni. Duplikuje to totiž kód (každá změna se pak musí delat na dvou místech).

Jan Suchánek
Člen | 404
+
0
-

Ok, tak to máš pravdu.

Jan Suchánek
Člen | 404
+
0
-

Co znamená tohle?

	$this->fireMyEvents($this->onSignatureFileNotFound);

onSignatureFileNotFound je callback ale co vrací do fireMyEvents?

Achse
Člen | 44
+
0
-

Teď jsem při implementaci narazil na další problém:

– Pro zpracování formuláře je zapotřebí dat z presenteru. Presenter::$user a podobně. Data která se načítají v action* (a používají se na více místech – např. template). Jakým způsobem je dostanu do metody FormContainer::Process aby to bylo čisté?

Mě jako první napadlo to posílat settery přímo do formuláře.

public function create($parent, $name, User $user, TimeEntriesContainer $timeEntriesToPay, $limitUserHours) {

        $form = new ApplyInvoiceForm($parent, $name);
        $form->setUser($user);
        $form->setLimitRealHours($limitUserHours);
	...
	...

A pak v process:

try {
	$this->invoicePresenterFacade->applyInvoice($values, $form->getUser(), $form->getLimitRealHours());
} catch // ...
// ...

Je to takto korektní? Připadá mi to trošku jako workaround.
Achse
Člen | 44
+
0
-

@jenicek:

public function createComponentApplyInvoiceForm($name) {
	$form = $this->applyInvoiceFormFactory->create($this, $name, $this->user, $this->timeEntriesToPay, $this->limitRealHours);
	$form->onSuccess[] = function () { /* flash & redirect*/};
	$form->onSignatureFileNotFound[] = function () { /* flash & redirect*/ };
	// ...

fireMyEvents nevrací nic, jenom provede včechny callbacky pro daný hook.

Editoval Achse (16. 6. 2014 10:49)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

Zaktualizoval jsem svou odpověď, nyní je tam komplexní ukázka včetně architektonicky čistého zpřístupnění Nette\Security\User v modelovém zpracování formuláře.

Achse
Člen | 44
+
0
-

Díky moc. Tenhle přístup (autocreating podle interfacu) jsem už viděl – du to implementovat :). Ještě – je to už hodně OT založil jsem tedy další questin: https://forum.nette.org/…k-presenteru

Achse
Člen | 44
+
0
-

@vojtech.dobes: z toho co píšeš sice krásně všechno nainjectuji z konfigu, ale data (proměnou předávanou hodnotou) z presenteru stále do továrničky / formuláře dostat neumím? Jak toto vyřešit?

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@Achse opět jsem zaktualizoval ukázku, nyní se tam předává hypotetická entita $article přímo z presenteru skrze metodu create().

Achse
Člen | 44
+
0
-

Díky moc, mohl by jsi to kdyžtak zmínit zde: https://doc.nette.org/…s/form-reuse ? Je to tam mírně zastaralé / neúplné. Udělal bych to já, ale musel bych nejprve hledat jak se to dělá (vím, že o tom David někde psal že to má někde repozitáře). Navíc jsi určitě zasvědcenější. Mě bude ještě chvíli trvat než to domimplemetuji (=pochopím, procvičím :)) Ještě jednou díky moc za help.

brabijan
Člen | 8
+
-5
-

Já to dělám tak, že si nastavím výsledek do samotné továrničky toho formuláře.

Formulář:

<?php

/**
 * @property $comment
 */
class SetCommentForm extends Nette\Object {

	/** @var CommentManager */
	private $commentManager;

	/** @var Comment */
	private $comment;

	public function __construct(CommentManager $commentManager) {
		$this->commentManager = $commentManager;
	}

	public function setComment(Comment $comment) {
		$this->comment = $comment;
	}

	public function getComment() {
		return $this->comment;
	}

	public function create() {
		$form = new Nette\Application\UI\Form;
		$form->addText("text", "Comment:");
		$form->addSubmit("send", $this->comment ? "Edit comment" : "Add comment");

		if($this->comment) {
			$form->onSuccess[] = $this->processEdit;
		} else {
			$form->onSuccess[] = $this->processAdd;
		}

		return $form;
	}

	public function processAdd(Nette\Application\UI\Form $form) {
		$this->comment = $this->commentManager->addComment($form->values->text);
	}

	public function processEdit(Nette\Application\UI\Form $form) {
		$this->comment->text = $form->values->text;
		$this->commentManager->save($this->comment);
	}

}

Presenter:

<?php

class CommentPresenter extends Presenter {

	use Kdyby\Autowired\AutowireProperties;
	use Kdyby\Autowired\AutowireComponentFactories;

	protected function createComponentAddCommentForm(SetCommentForm $factory) {
		$form = $factory->create();
		$form->onSuccess[] = function() use ($factory) {
			$this->flashMessage("Comment #{$factory->comment->id} was added");
			$this->redirect("this");
		}
		return $form;
	}

}
Achse
Člen | 44
+
0
-

No jenže pak má factory v sobe informace typu „last-modified-record“. A to je podle mě dost proti všem dobrým mravům v návrhu. Factorky by podle mě žádná data uchvoávat neměla, resp. mela jenom ta která potřebuje k vytvoření objektu.

Jan Suchánek
Člen | 404
+
0
-

Při odchytávání vyjímek dáváte chybovou odpověď do $onArticleError?

Achse
Člen | 44
+
0
-

@vojtech.dobes: z nejakého důvodu mi to odmítá nainjectovat Usera.

Argument 1 passed to App\Form\Invoice\ApplyInvoiceForm::__construct() must be an instance of Nette\Security\User, null given, called in ...

Je User vůbec jako služba dostupný?

Editoval Achse (16. 6. 2014 17:18)

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@Achse Určitě je. Hoď prosím kód třeba na gist a odkaž ho tu.

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@Achse Ta tvoje továrnička neodpovídá konstruktoru tvé komponenty.

Achse
Člen | 44
+
0
-

Asi bohužel nerozumím, továrničku generuje Nette na základě interfejsu ne?

Vojtěch Dobeš
Gold Partner | 1316
+
0
-

@Achse Ano, a ty máš v configu a v interfejsu uvedený nějaký parametr, který ale v konstruktoru té komponenty není.

Achse
Člen | 44
+
0
-

Ah, už tomu začínám rozumět. Továrnička generovaná podle interfejsu do kontruktoru toho co generuje natlačí podle neon všechno co jí dám metodou create + natlačí ještě to co umí sama vyřešit autowiringem z DI containeru.

– Jenom jedna podstatná věc, co mě hodně zdržela bylo, že ten argument musí být první! což pro mě bylo fakt překvápko. Padalo to na:

Service '38': Parameter $limit in App\Form\Invoice\ApplyInvoiceForm::__construct() has no type hint, so its value must be specified.

Fakt by asi bylo cool to všechno nějak zadokumentovat. :)

Editoval Achse (17. 6. 2014 9:34)

Michal Vyšinský
Člen | 608
+
0
-

On ten argument nemusí být první, lze to napsat i takto:

implement: XXX
  parameters: [limit]
  arguments: [..., %limit%]

Pak můžeš mít v konstruktoru autowired službu jako první. Ale vem si, když budeš mít 5 služeb, které ta komponenta potřebuje :) – takže takové nepsané pravidlo: argumenty co se musí předat v create metodě dávat jako první.