-
-
Notifications
You must be signed in to change notification settings - Fork 281
/
form-reuse.texy
353 lines (277 loc) · 10.6 KB
/
form-reuse.texy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
Ponowne użycie formularzy w wielu miejscach
*******************************************
.[perex]
W Nette masz kilka opcji, aby ponownie wykorzystać ten sam formularz w wielu miejscach bez duplikowania kodu. W tym artykule omówimy różne rozwiązania, w tym te, których powinieneś unikać.
Fabryka formularzy .[#toc-form-factory]
=======================================
Jednym z podstawowych podejść do używania tego samego komponentu w wielu miejscach jest stworzenie metody lub klasy, która generuje komponent, a następnie wywołanie tej metody w różnych miejscach w aplikacji. Taka metoda lub klasa nazywana jest *factory*. Proszę nie mylić z wzorcem projektowym *factory method*, który opisuje specyficzny sposób korzystania z fabryk i nie jest związany z tym tematem.
Jako przykład, stwórzmy fabrykę, która zbuduje formularz edycji:
```php
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Title:');
// dodatkowe pola formularza są dodane tutaj
$form->addSubmit('send', 'Save');
return $form;
}
}
```
Teraz można użyć tej fabryki w różnych miejscach w aplikacji, na przykład w prezenterach lub komponentach. A zrobimy to poprzez [zażądanie jej jako zależności |dependency-injection:passing-dependencies]. Więc najpierw zapiszemy klasę do pliku konfiguracyjnego:
```neon
services:
- FormFactory
```
A potem używamy jej w prezenterze:
```php
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->createEditForm();
$form->onSuccess[] = function () {
// przetwarzanie przesłanych danych
};
return $form;
}
}
```
Możesz rozszerzyć fabrykę formularzy o dodatkowe metody, aby stworzyć inne typy formularzy, aby dopasować je do swojej aplikacji. I, oczywiście, możesz dodać metodę, która tworzy podstawowy formularz bez elementów, z którego będą korzystać inne metody:
```php
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Title:');
// dodatkowe pola formularza są dodane tutaj
$form->addSubmit('send', 'Save');
return $form;
}
}
```
Metoda `createForm()` nie robi jeszcze nic użytecznego, ale to się szybko zmieni.
Zależności fabryczne .[#toc-factory-dependencies]
=================================================
Z czasem okaże się, że potrzebujemy, aby formularze były wielojęzyczne. Oznacza to, że musimy skonfigurować [translator |forms:rendering#Translating] dla wszystkich formularzy. Aby to zrobić, modyfikujemy klasę `FormFactory`, aby zaakceptowała obiekt `Translator` jako zależność w konstruktorze i przekazała go do formularza:
```php
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;
}
//...
}
```
Ponieważ metoda `createForm()` jest wywoływana również przez inne metody tworzące konkretne formularze, musimy ustawić translator tylko w tej metodzie. I gotowe. Nie trzeba zmieniać żadnego kodu prezentera lub komponentu, co jest świetne.
Więcej klas fabrycznych .[#toc-more-factory-classes]
====================================================
Alternatywnie, możesz stworzyć wiele klas dla każdego formularza, który chcesz użyć w swojej aplikacji.
Takie podejście może zwiększyć czytelność kodu i ułatwić zarządzanie formularzami. Pozostaw oryginalną `FormFactory`, aby stworzyć tylko czysty formularz z podstawową konfiguracją (na przykład z obsługą tłumaczeń) i utwórz nową fabrykę `EditFormFactory` dla formularza edycji.
```php
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ użycie kompozycji
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// dodatkowe pola formularza są dodawane tutaj
$form->addSubmit('send', 'Save');
return $form;
}
}
```
Bardzo ważne jest, aby powiązanie między klasami `FormFactory` i `EditFormFactory` było realizowane [przez kompozycję |nette:introduction-to-object-oriented-programming#composition], a nie [dziedziczenie obiektów |https://doc.nette.org/en/introduction-to-object-oriented-programming#inheritance]:
```php
// ⛔ NIE! DZIEDZICZENIE NIE NALEŻY DO TEGO MIEJSCA
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Title:');
// tutaj dodaje się dodatkowe pola formularza
$form->addSubmit('send', 'Save');
return $form;
}
}
```
Używanie dziedziczenia w tym przypadku byłoby całkowicie przeciwne do zamierzonego. Bardzo szybko napotkałbyś problemy. Na przykład, gdybyś chciał dodać parametry do metody `create()`; PHP zgłosiłoby błąd, że jej podpis jest inny niż rodzica.
Albo podczas przekazywania zależności do klasy `EditFormFactory` poprzez konstruktor. To spowodowałoby coś, co nazywamy [piekłem konstru |dependency-injection:passing-dependencies#Constructor hell]ktora.
Ogólnie rzecz biorąc, lepiej jest preferować [kompozycję niż dziedziczenie |dependency-injection:faq#Why composition is preferred over inheritance].
Obsługa formularzy .[#toc-form-handling]
========================================
Obsługa formularza, która jest wywoływana po pomyślnym przesłaniu danych, może być również częścią klasy fabrycznej. Jego działanie będzie polegało na przekazaniu przesłanych danych do modelu w celu ich przetworzenia. Wszelkie błędy zostaną przekazane z [powrotem do |forms:validation#Processing Errors] formularza. Model w poniższym przykładzie jest reprezentowany przez klasę `Facade`:
```php
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Title:');
// tutaj dodaje się dodatkowe pola formularza
$form->addSubmit('send', 'Save');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// przetwarzanie przesłanych danych
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
```
Niech prezenter sam zajmie się przekierowaniem. Doda on kolejny handler do zdarzenia `onSuccess`, który wykona przekierowanie. Dzięki temu formularz będzie mógł być używany w różnych prezenterach, a każdy z nich może przekierować do innej lokalizacji.
```php
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;
}
}
```
Rozwiązanie to wykorzystuje właściwość formularzy polegającą na tym, że po wywołaniu `addError()` na formularzu lub jego elemencie nie jest wywoływany następny handler `onSuccess`.
Dziedziczenie po klasie Form .[#toc-inheriting-from-the-form-class]
===================================================================
Zbudowany formularz nie powinien być dzieckiem formularza. Innymi słowy, nie używaj tego rozwiązania:
```php
// ⛔ NIE! DZIEDZICZENIE NIE NALEŻY DO TEGO MIEJSCA
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$form->addText('title', 'Title:');
// tutaj dodaje się dodatkowe pola formularza
$form->addSubmit('send', 'Save');
$form->setTranslator($translator);
}
}
```
Zamiast budować formularz w konstruktorze, użyj fabryki.
Ważne jest, aby zdać sobie sprawę, że klasa `Form` jest przede wszystkim narzędziem do składania formularza, czyli konstruktorem formularzy. A złożony formularz można uznać za jej produkt. Produkt nie jest jednak szczególnym przypadkiem konstruktora; nie ma między nimi relacji *is a*, która stanowi podstawę dziedziczenia.
Komponent formularza .[#toc-form-component]
===========================================
Zupełnie innym podejściem jest stworzenie [komponentu |application:components], który zawiera formularz. Daje to nowe możliwości, na przykład renderowanie formularza w określony sposób, ponieważ komponent zawiera szablon.
Albo sygnały mogą być użyte do komunikacji AJAX i ładowania informacji do formularza, na przykład do podpowiedzi itp.
```php
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', 'Title:');
// tutaj dodaje się dodatkowe pola formularza
$form->addSubmit('send', 'Save');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// przetwarzanie przesłanych danych
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// wywoływanie zdarzeń
$this->onSave($this, $data);
}
}
```
Stwórzmy fabrykę, która będzie produkowała ten komponent. Wystarczy, że [napiszemy jej interfejs |application:components#Components with Dependencies]:
```php
interface EditControlFactory
{
function create(): EditControl;
}
```
I dodać go do pliku konfiguracyjnego:
```neon
services:
- EditControlFactory
```
A teraz możemy zażądać fabryki i użyć jej w prezenterze:
```php
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');
// lub przekierować na wynik edycji, np:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}
```
{{sitename: Najlepsze praktyki}}