Best Practise: jak zpracovávat formuláře

- Achse
 - Člen | 44
 
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
 
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
 
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
 
@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
 
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
 
Co znamená tohle?
	$this->fireMyEvents($this->onSignatureFileNotFound);
onSignatureFileNotFound je callback ale co vrací do fireMyEvents?

- Achse
 - Člen | 44
 
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
 
@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
 
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
 
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

- Vojtěch Dobeš
 - Gold Partner | 1316
 
@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
 
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
 
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
 
- ApplyInvoiceForm.php: https://gist.github.com/…229b9488257e
		
- IApplyInvoiceFormFactory.php: https://gist.github.com/…d9624d6eda65
 - business.neon: https://gist.github.com/…4f7302d869f5
 
 

- Vojtěch Dobeš
 - Gold Partner | 1316
 
@Achse Ta tvoje továrnička neodpovídá konstruktoru tvé komponenty.

- Vojtěch Dobeš
 - Gold Partner | 1316
 
@Achse Ano, a ty máš v configu a v interfejsu uvedený nějaký parametr, který ale v konstruktoru té komponenty není.

- Achse
 - Člen | 44
 
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
 
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í.