Znovupoužití formulářů na více místech
V Nette máte k dispozici několik možností, jak použít stejný formulář na více místech a neduplikovat kód. V tomto článku si ukážeme různá řešení, včetně těch, kterým byste se měli vyhnout.
Továrna na formuláře
Jedním ze základních přístupů k použití stejné komponenty na více místech je vytvoření metody nebo třídy, která tuto komponentu generuje, a následné volání této metody na různých místech aplikace. Takové metodě nebo třídě se říká továrna. Nezaměňujte prosím s návrhovým vzorem factory method, který popisuje specifický způsob využití továren a s tímto tématem nesouvisí.
Jako příklad si vytvoříme továrnu, která bude sestavovat editační formulář:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
return $form;
}
}
Nyní můžete použít tuto továrnu na různých místech ve vaší aplikaci, například v presenterech nebo komponentách. A to tak, že si ji vyžádáme jako závislost. Nejprve tedy třídu zapíšeme do konfiguračního souboru:
services:
- FormFactory
A poté ji použijeme v presenteru:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->createEditForm();
$form->onSuccess[] = function () {
// zpracování odeslaných dat
};
return $form;
}
}
Formulářovou továrnu můžete rozšířit o další metody pro vytváření dalších druhů formulářů podle potřeby vaší aplikace. A samozřejmě můžeme přidat i metodu, která vytvoří základní formulář bez prvků, a tu budou ostatní metody využívat:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
return $form;
}
}
Metoda createForm()
zatím nedělá nic užitečného, ale to se rychle změní.
Závislosti továrny
Časem se ukáže, že potřebujeme, aby formuláře byly multijazyčné. To znamená, že všem formulářům musíme nastavit
tzv. translator. Za tím účelem upravíme třídu
FormFactory
tak, aby přijímala objekt Translator
jako závislost v konstruktoru, a předáme jej
formuláři:
use Nette\Localization\Translator;
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function createForm(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
// ...
}
Jelikož metodu createForm()
volají i ostatní metody tvořící specifické formuláře, stačí translator
nastavit jen v ní. A máme hotovo. Není potřeba měnit kód žádného presenteru nebo komponenty, což je skvělé.
Více továrních tříd
Alternativně můžete vytvořit více tříd pro každý formulář, který chcete použít ve vaší aplikaci. Tento
přístup může zvýšit čitelnost kódu a usnadnit správu formulářů. Původní FormFactory
necháme vytvářet
jen čistý formulář se základní konfigurací (například s podporou překladů) a pro editační formulář vytvoříme
novou továrnu EditFormFactory
.
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ použití kompozice
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
return $form;
}
}
Velmi důležité je, aby vazba mezi třídami FormFactory
a EditFormFactory
byla realizována kompozicí, nikoliv objektovou dědičností:
// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
return $form;
}
}
Použití dedičnosti by bylo v tomto případě zcela kontraproduktivní. Na problémy byste narazili velmi rychle. Třeba ve
chvíli, kdybyste chtěli přidat metodě create()
parametry; PHP by zahlásilo chybu, že se její signatura liší
od rodičovské. Nebo při předávání závislosti do třídy EditFormFactory
přes konstruktor. Nastala by
situace, které říkáme constructor hell.
Obecně je lepší dávat přednost kompozici před dědičností.
Obsluha formuláře
Obsluha formuláře, která se zavolá po úspěšném odeslání, může být také součástí tovární třídy. Bude
fungovat tak, že odeslaná data předá modelu ke zpracování. Případné chyby předá zpět do formuláře. Model
v následujícím příkladu reprezentuje třída Facade
:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// zpracování odeslaných dat
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Samotné přesměrování ale necháme na presenteru. Ten přidá události onSuccess
další handler, který
přesmerování provede. Díky tomu bude možné formulář použít v různých presenterech a v každém
přesměrovat jinam.
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditFormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->create();
$form->onSuccess[] = function () {
$this->flashMessage('Záznam byl uložen');
$this->redirect('Homepage:');
};
return $form;
}
}
Toto řešení využívá vlastnost formulářů, že když se nad formulářem nebo jeho prvkem zavolá
addError()
, už další handler onSuccess
se nevolá.
Dědění od třídy Form
Sestavený formulář nemá být potomkem formuláře. Jinými slovy, nepoužívejte toto řešení:
// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
$form->setTranslator($translator);
}
}
Místo sestavování formuláře v konstruktoru použijte továrnu.
Je potřeba si uvědomit, že třída Form
je v první řadě nástrojem pro sestavení formuláře, tedy
form builder. A sestavený formulář lze chápat jako její produkt. Jenže produkt není specifickým případem
builderu, není mezi nimi vazba is a tvořící základ dědičnosti.
Komponenta s formulářem
Zcela jiný přístup představuje tvorba komponenty, jejíž součástí je formulář. To dává nové možnosti, například renderovat formulář specifickým způsobem, neboť součástí komponenty je i šablona. Nebo lze využít signály pro AJAXovou komunikaci a donačítání informací do formuláře, například pro napovídání, atd.
use Nette\Application\UI\Form;
class EditControl extends Nette\Application\UI\Control
{
public array $onSave = [];
public function __construct(
private Facade $facade,
) {
}
protected function createComponentForm(): Form
{
$form = new Form;
$form->addText('title', 'Titulek:');
// zde se přidávají další formulářová pole
$form->addSubmit('send', 'Odeslat');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// zpracování odeslaných dat
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// vyvolání události
$this->onSave($this, $data);
}
}
Ještě vytvoříme továrnu, která bude tuto komponentu vyrábět. Stačí zapsat její rozhraní:
interface EditControlFactory
{
function create(): EditControl;
}
A přidat do konfiguračního souboru:
services:
- EditControlFactory
A nyní už můžeme továrnu vyžádat a použít v presenteru:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private EditControlFactory $controlFactory,
) {
}
protected function createComponentEditForm(): EditControl
{
$control = $this->controlFactory->create();
$control->onSave[] = function (EditControl $control, $data) {
$this->redirect('this');
// nebo přesměrujeme na výsledek editace, např.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}