Skip to content

Commit 51cadb4

Browse files
committed
form reuse: improved
1 parent 0dfc000 commit 51cadb4

File tree

16 files changed

+3552
-736
lines changed

16 files changed

+3552
-736
lines changed

best-practices/bg/form-reuse.texy

Lines changed: 223 additions & 47 deletions
Large diffs are not rendered by default.

best-practices/cs/form-reuse.texy

Lines changed: 215 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,208 @@ Znovupoužití formulářů na více místech
22
**************************************
33

44
.[perex]
5-
Jak použít stejný formulář na více místech a neduplikovat kód? To je v Nette opravdu snadné a máte na výběr víc způsobů.
5+
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.
66

77

8-
Továrna na formulář
9-
===================
8+
Továrna na formuláře
9+
====================
1010

11-
Vytvoříme si třídu, která umí formulář vyrobit. Takové třídě se říká továrna. V místě, kde budeme chtít formulář použít (např. v presenteru), si továrnu [vyžádáme jako závislosti|dependency-injection:passing-dependencies].
11+
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í.
1212

13-
Součástí továrny je i kód, který po úspěšném odeslaní formuláře předá data k dalšímu zpracování. Obvykle do modelové vrstvy. Zároveň zkontroluje, zda vše proběhlo v pořádku, a případné chyby [předá zpět |forms:validation#Chyby při zpracování] do formuláře. Model v následujícím příkladu reprezentuje třída `Facade`:
13+
Jako příklad si vytvoříme továrnu, která bude sestavovat editační formulář:
1414

1515
```php
1616
use Nette\Application\UI\Form;
1717

18-
class EditFormFactory
18+
class FormFactory
19+
{
20+
public function createEditForm(): Form
21+
{
22+
$form = new Form;
23+
$form->addText('title', 'Titulek:');
24+
// zde se přidávají další formulářová pole
25+
$form->addSubmit('send', 'Odeslat');
26+
return $form;
27+
}
28+
}
29+
```
30+
31+
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|dependency-injection:passing-dependencies]. Nejprve tedy třídu zapíšeme do konfiguračního souboru:
32+
33+
```neon
34+
services:
35+
- FormFactory
36+
```
37+
38+
A poté ji použijeme v presenteru:
39+
40+
41+
```php
42+
class MyPresenter extends Nette\Application\UI\Presenter
1943
{
2044
public function __construct(
21-
private Facade $facade,
45+
private FormFactory $formFactory,
46+
) {
47+
}
48+
49+
protected function createComponentEditForm(): Form
50+
{
51+
$form = $this->formFactory->createEditForm();
52+
$form->onSuccess[] = function () {
53+
// zpracování odeslaných dat
54+
};
55+
return $form;
56+
}
57+
}
58+
```
59+
60+
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:
61+
62+
```php
63+
class FormFactory
64+
{
65+
public function createForm(): Form
66+
{
67+
$form = new Form;
68+
return $form;
69+
}
70+
71+
public function createEditForm(): Form
72+
{
73+
$form = $this->createForm();
74+
$form->addText('title', 'Titulek:');
75+
// zde se přidávají další formulářová pole
76+
$form->addSubmit('send', 'Odeslat');
77+
return $form;
78+
}
79+
}
80+
```
81+
82+
Metoda `createForm()` zatím nedělá nic užitečného, ale to se rychle změní.
83+
84+
85+
Závislosti továrny
86+
==================
87+
88+
Č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|forms:rendering#Překládání]. 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:
89+
90+
```php
91+
use Nette\Localization\Translator;
92+
93+
class FormFactory
94+
{
95+
public function __construct(
96+
private Translator $translator,
2297
) {
2398
}
2499

25-
public function create(/* parametry */): Form
100+
public function createForm(): Form
26101
{
27102
$form = new Form;
103+
$form->setTranslator($this->translator);
104+
return $form;
105+
}
106+
107+
// ...
108+
}
109+
```
110+
111+
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é.
112+
113+
114+
Více továrních tříd
115+
===================
28116

29-
// přidáme prvky do formuláře
117+
Alternativně můžete vytvořit více tříd pro každý formulář, který chcete použít ve vaší aplikaci.
118+
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`.
30119

120+
```php
121+
class FormFactory
122+
{
123+
public function __construct(
124+
private Translator $translator,
125+
) {
126+
}
127+
128+
public function create(): Form
129+
{
130+
$form = new Form;
131+
$form->setTranslator($this->translator);
132+
return $form;
133+
}
134+
}
135+
136+
137+
// ✅ použití kompozice
138+
class EditFormFactory
139+
{
140+
public function __construct(
141+
private FormFactory $formFactory,
142+
) {
143+
}
144+
145+
public function create(): Form
146+
{
147+
$form = $this->formFactory->create();
148+
// zde se přidávají další formulářová pole
31149
$form->addSubmit('send', 'Odeslat');
32-
$form->onSuccess[] = [$this, 'processForm'];
150+
return $form;
151+
}
152+
}
153+
```
33154

155+
Velmi důležité je, aby vazba mezi třídami `FormFactory` a `EditFormFactory` byla realizována kompozicí, nikoliv objektovou dědičností:
156+
157+
```php
158+
// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ
159+
class EditFormFactory extends FormFactory
160+
{
161+
public function create(): Form
162+
{
163+
$form = parent::create();
164+
$form->addText('title', 'Titulek:');
165+
// zde se přidávají další formulářová pole
166+
$form->addSubmit('send', 'Odeslat');
34167
return $form;
35168
}
169+
}
170+
```
171+
172+
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é.
173+
Nebo při předávání závislosti do třídy `EditFormFactory` přes konstruktor. Nastala by situace, které říkáme [constructor hell |dependency-injection:passing-dependencies#Constructor hell].
174+
175+
Obecně je lepší dávat přednost kompozici před dědičností.
36176

37-
public function processForm(Form $form, array $values): void
177+
178+
Obsluha formuláře
179+
=================
180+
181+
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 |forms:validation#Chyby při zpracování] do formuláře. Model v následujícím příkladu reprezentuje třída `Facade`:
182+
183+
```php
184+
class EditFormFactory
185+
{
186+
public function __construct(
187+
private FormFactory $formFactory,
188+
private Facade $facade,
189+
) {
190+
}
191+
192+
public function create(): Form
193+
{
194+
$form = $this->formFactory->create();
195+
$form->addText('title', 'Titulek:');
196+
// zde se přidávají další formulářová pole
197+
$form->addSubmit('send', 'Odeslat');
198+
$form->onSuccess[] = [$this, 'processForm'];
199+
return $form;
200+
}
201+
202+
public function processForm(Form $form, array $data): void
38203
{
39204
try {
40-
// zpracování formuláře
41-
$this->facade->process($values);
205+
// zpracování odeslaných dat
206+
$this->facade->process($data);
42207

43208
} catch (AnyModelException $e) {
44209
$form->addError('...');
@@ -47,17 +212,7 @@ class EditFormFactory
47212
}
48213
```
49214

50-
Továrna může být samozřejmě parametrická, tj. může přijímat parametery, které ovlivní podobu vytvářeného formuláře.
51-
52-
Nyní si ukážeme předání továrny do presenteru. Nejprve ji zapíšeme do konfiguračního souboru:
53-
54-
```neon
55-
services:
56-
- EditFormFactory
57-
```
58-
59-
A poté vyžádáme v presenteru. Tam také následuje další krok zpracování odeslaného formuláře a tím je přesměrování na další stránku:
60-
215+
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.
61216

62217
```php
63218
class MyPresenter extends Nette\Application\UI\Presenter
@@ -70,23 +225,47 @@ class MyPresenter extends Nette\Application\UI\Presenter
70225
protected function createComponentEditForm(): Form
71226
{
72227
$form = $this->formFactory->create();
73-
74-
$form->onSuccess[] = function (Form $form) {
75-
$this->redirect('this');
228+
$form->onSuccess[] = function () {
229+
$this->flashMessage('Záznam byl uložen');
230+
$this->redirect('Homepage:');
76231
};
77-
78232
return $form;
79233
}
80234
}
81235
```
82236

83-
Tím, že přesměrování řeší až handler v presenteru, lze komponentu použít na více místech a na každém přesměrovat jinam.
237+
Toto řešení využívá vlastnost formulářů, že když se nad formulářem nebo jeho prvkem zavolá `addError()`, už další handler `onSuccess` se nevolá.
238+
239+
240+
Dědění od třídy Form
241+
====================
242+
243+
Sestavený formulář nemá být potomkem formuláře. Jinými slovy, nepoužívejte toto řešení:
244+
245+
```php
246+
// ⛔ TAKHLE NE! SEM DĚDIČNOST NEPATŘÍ
247+
class EditForm extends Form
248+
{
249+
public function __construct(Translator $translator)
250+
{
251+
parent::__construct();
252+
$form->addText('title', 'Titulek:');
253+
// zde se přidávají další formulářová pole
254+
$form->addSubmit('send', 'Odeslat');
255+
$form->setTranslator($translator);
256+
}
257+
}
258+
```
259+
260+
Místo sestavování formuláře v konstruktoru použijte továrnu.
261+
262+
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.
84263

85264

86265
Komponenta s formulářem
87266
=======================
88267

89-
Další možností je vytvořit novou [komponentu|application:components], jejíž součástí bude formulář. To nám dává možnost například renderovat formulář specifickým způsobem, neboť součástí komponenty je i šablona.
268+
Zcela jiný přístup představuje tvorba [komponenty|application:components], 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.
90269
Nebo lze využít signály pro AJAXovou komunikaci a donačítání informací do formuláře, například pro napovídání, atd.
91270

92271

@@ -105,35 +284,33 @@ class EditControl extends Nette\Application\UI\Control
105284
protected function createComponentForm(): Form
106285
{
107286
$form = new Form;
108-
109-
// přidáme prvky do formuláře
110-
287+
$form->addText('title', 'Titulek:');
288+
// zde se přidávají další formulářová pole
111289
$form->addSubmit('send', 'Odeslat');
112290
$form->onSuccess[] = [$this, 'processForm'];
113291

114292
return $form;
115293
}
116294

117-
public function processForm(Form $form, array $values): void
295+
public function processForm(Form $form, array $data): void
118296
{
119297
try {
120-
// zpracování formuláře
121-
$this->facade->process($values);
298+
// zpracování odeslaných dat
299+
$this->facade->process($data);
122300

123301
} catch (AnyModelException $e) {
124302
$form->addError('...');
125303
return;
126304
}
127305

128306
// vyvolání události
129-
$this->onSave($this, $values);
307+
$this->onSave($this, $data);
130308
}
131309
}
132310
```
133311

134312
Ještě vytvoříme továrnu, která bude tuto komponentu vyrábět. Stačí [zapsat její rozhraní|application:components#Komponenty se závislostmi]:
135313

136-
137314
```php
138315
interface EditControlFactory
139316
{
@@ -173,5 +350,4 @@ class MyPresenter extends Nette\Application\UI\Presenter
173350
}
174351
```
175352

176-
{{priority: -1}}
177353
{{sitename: Best Practices}}

0 commit comments

Comments
 (0)