Wielokrotne użycie formularzy w wielu miejscach
W Nette masz do dyspozycji kilka opcji, jak użyć tego samego formularza w wielu miejscach i nie duplikować kodu. W tym artykule pokażemy różne rozwiązania, w tym te, których powinieneś unikać.
Fabryka formularzy
Jednym z podstawowych podejść do użycia tego samego komponentu w wielu miejscach jest utworzenie metody lub klasy, która generuje ten komponent, a następnie wywoływanie tej metody w różnych miejscach aplikacji. Taka metoda lub klasa nazywana jest fabryką. Proszę nie mylić z wzorcem projektowym factory method, który opisuje specyficzny sposób wykorzystania fabryk i nie jest związany z tym tematem.
Jako przykład stworzymy fabrykę, która będzie budować formularz edycyjny:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
return $form;
}
}
Teraz możesz użyć tej fabryki w różnych miejscach w swojej aplikacji, na przykład w presenterach lub komponentach. A to tak, że zażądamy jej jako zależności. Najpierw więc zapiszemy klasę do pliku konfiguracyjnego:
services:
- FormFactory
A potem użyjemy jej w prezenterze:
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 wysłanych danych
};
return $form;
}
}
Fabrykę formularzy możesz rozszerzyć o kolejne metody do tworzenia innych rodzajów formularzy zgodnie z potrzebami Twojej aplikacji. I oczywiście możemy dodać również metodę, która stworzy podstawowy formularz bez elementów, a tę będą wykorzystywać inne metody:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
return $form;
}
}
Metoda createForm()
na razie nie robi nic użytecznego, ale to się szybko zmieni.
Zależności fabryki
Z czasem okaże się, że potrzebujemy, aby formularze były wielojęzyczne. Oznacza to, że wszystkim formularzom musimy
ustawić tzw. translator. W tym celu zmodyfikujemy klasę
FormFactory
tak, aby przyjmowała obiekt Translator
jako zależność w konstruktorze, i przekażemy go
formularzowi:
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ż metodę createForm()
wywołują również inne metody tworzące specyficzne formularze, wystarczy
translator ustawić tylko w niej. I gotowe. Nie ma potrzeby zmieniać kodu żadnego presentera ani komponentu, co jest
świetne.
Wiele klas fabryk
Alternatywnie możesz utworzyć 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. Pierwotną FormFactory
pozostawimy do
tworzenia tylko czystego formularza z podstawową konfiguracją (na przykład ze wsparciem tłumaczeń), a dla formularza
edycyjnego stworzymy nową fabrykę EditFormFactory
.
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();
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
return $form;
}
}
Bardzo ważne jest, aby powiązanie między klasami FormFactory
i EditFormFactory
było realizowane
przez kompozycję, a nie przez
dziedziczenie obiektowe:
// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
return $form;
}
}
Użycie dziedziczenia byłoby w tym przypadku całkowicie kontrproduktywne. Na problemy napotkałbyś bardzo szybko. Na
przykład w chwili, gdy chciałbyś dodać parametry do metody create()
; PHP zgłosiłoby błąd, że jej sygnatura
różni się od rodzicielskiej. Lub przy przekazywaniu zależności do klasy EditFormFactory
przez konstruktor.
Powstałaby sytuacja, którą nazywamy constructor hell.
Ogólnie lepiej jest preferować kompozycję nad dziedziczeniem.
Obsługa formularza
Obsługa formularza, która jest wywoływana po pomyślnym wysłaniu, może być również częścią klasy fabryki. Będzie
działać tak, że przekaże wysłane dane do modelu w celu przetworzenia. Ewentualne błędy przekazuje z powrotem do formularza. Model w
poniższym przykładzie reprezentuje klasa Facade
:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// przetwarzanie wysłanych danych
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Samo przekierowanie pozostawimy jednak prezenterowi. Ten doda do zdarzenia onSuccess
kolejny handler, który
wykona przekierowanie. Dzięki temu będzie można użyć formularza w różnych prezenterach i w każdym przekierować gdzie
indziej.
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('Rekord został zapisany');
$this->redirect('Homepage:');
};
return $form;
}
}
To rozwiązanie wykorzystuje właściwość formularzy, że gdy na formularzu lub jego elemencie zostanie wywołane
addError()
, kolejne handlery onSuccess
nie są już wywoływane.
Dziedziczenie od klasy Form
Zbudowany formularz nie powinien być potomkiem formularza. Innymi słowy, nie używaj tego rozwiązania:
// ⛔ TAK NIE! DZIEDZICZENIE TU NIE PASUJE
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$this->addSubmit('send', 'Wyślij');
$this->setTranslator($translator);
}
}
Zamiast budować formularz w konstruktorze, użyj fabryki.
Należy zdać sobie sprawę, że klasa Form
jest przede wszystkim narzędziem do budowania formularza, czyli
form builder. A zbudowany formularz można rozumieć jako jej produkt. Jednak produkt nie jest specyficznym przypadkiem
buildera, nie ma między nimi relacji is a stanowiącej podstawę dziedziczenia.
Komponent z formularzem
Całkowicie inne podejście stanowi tworzenie komponentu, którego częścią jest formularz. Daje to nowe możliwości, na przykład renderowanie formularza w specyficzny sposób, ponieważ częścią komponentu jest również szablon. Lub można wykorzystać sygnały do komunikacji AJAX i doładowywania informacji do formularza, na przykład do podpowiadania, itd.
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', 'Tytuł:');
// tutaj dodawane są kolejne pola formularza
$form->addSubmit('send', 'Wyślij');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// przetwarzanie wysłanych danych
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// wywołanie zdarzenia
$this->onSave($this, $data);
}
}
Stworzymy jeszcze fabrykę, która będzie produkować ten komponent. Wystarczy zapisać jej interfejs:
interface EditControlFactory
{
function create(): EditControl;
}
I dodać do pliku konfiguracyjnego:
services:
- EditControlFactory
A teraz już możemy zażądać fabryki i użyć jej w prezenterze:
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 przekierowujemy na wynik edycji, np.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}