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í.