Ponowne wykorzystanie formularzy w wielu miejscach
Nette oferuje kilka sposobów, jak używać tego samego formularza w wielu miejscach bez duplikowania kodu. W tym artykule omówimy różne rozwiązania, w tym te, których powinieneś unikać.
Fabryka formularzy
Podstawowym podejściem do ponownego wykorzystania komponentu w wielu miejscach jest utworzenie metody albo klasy, która ten komponent tworzy. Taką metodę wywołujemy potem z różnych miejsc aplikacji. Taka metoda albo klasa nazywa się fabryką. Nie myl tego proszę ze wzorcem projektowym metoda fabrykująca, który opisuje szczególny sposób użycia fabryk i nie jest bezpośrednio związany z tym tematem.
Jako przykład utwórzmy fabrykę budującą 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', 'Zapisz');
return $form;
}
}
Teraz możesz tej fabryki używać w różnych częściach aplikacji, na przykład w presenterach albo komponentach. Zrobisz to, prosząc o nią jako o zależność. Najpierw zarejestruj klasę w pliku konfiguracyjnym:
services:
- FormFactory
A potem użyj jej w presenterze:
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 tworzące inne rodzaje formularzy, zależnie od potrzeb Twojej aplikacji. I naturalnie możemy dodać metodę tworzącą podstawowy formularz bez elementów, którą pozostałe metody potem wykorzystają:
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', 'Zapisz');
return $form;
}
}
Metoda createForm() nie robi jeszcze nic szczególnie pożytecznego, ale to się wkrótce zmieni.
Zależności fabryki
Z czasem może się okazać, że formularze mają być wielojęzyczne. Oznacza to, że wszystkim formularzom trzeba ustawić
translator. Żeby to osiągnąć, zmodyfikuj klasę
FormFactory tak, by przyjmowała obiekt Translator jako zależność w konstruktorze i przekazywała go
tworzonemu 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ą także pozostałe metody tworzące konkretne formularze, wystarczy
ustawić translator właśnie tutaj. I gotowe. Nie trzeba modyfikować kodu żadnego presentera ani komponentu, co jest
znakomite.
Więcej klas fabryk
Alternatywnie możesz utworzyć osobne klasy fabryk dla każdego formularza, którego zamierzasz używać w aplikacji. To
podejście może poprawić czytelność kodu i uprościć zarządzanie formularzami. Niech pierwotna FormFactory
tworzy tylko podstawowy formularz z podstawową konfiguracją (na przykład wsparciem dla tłumaczeń), a dla formularza
edycyjnego utwórz 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', 'Zapisz');
return $form;
}
}
Bardzo ważne jest, żeby relacja między klasami FormFactory i EditFormFactory była zrealizowana
przez kompozycję, a nie przez
dziedziczenie obiektowe:
// ⛔ 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', 'Zapisz');
return $form;
}
}
Użycie dziedziczenia byłoby tu całkowicie kontrproduktywne. Bardzo szybko napotkałbyś problemy. Na przykład gdybyś
chciał dodać do metody create() parametry, PHP zgłosiłoby błąd, bo jej sygnatura różniłaby się od sygnatury
rodzica. Albo przy przekazywaniu zależności do klasy EditFormFactory przez konstruktor. Doszłoby do sytuacji,
którą nazywamy constructor
hell.
Generalnie lepiej preferować kompozycję nad dziedziczeniem.
Obsługa formularza
Handler formularza wywoływany po udanym wysłaniu też może być częścią klasy fabryki. Działa tak, że przekazuje
wysłane dane do przetworzenia warstwie modelu. Ewentualne błędy przetwarzania przekazuje z powrotem do formularza. W poniższym
przykładzie model 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', 'Zapisz');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private function processForm(Form $form, array $data): void
{
try {
// przetwarzanie wysłanych danych
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Samo przekierowanie zostawmy jednak presenterowi. Ten doda do zdarzenia onSuccess kolejny handler, który
przekierowanie wykona. Dzięki temu formularza będzie można używać w różnych presenterach, a każdy z nich po sukcesie
przekieruje 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, zgodnie z którą jeśli na formularzu albo którymś z jego
elementów zostanie wywołane addError(), kolejne handlery onSuccess nie zostaną wywołane.
Dziedziczenie po klasie Form
Zbudowany formularz nie powinien być potomkiem klasy Form. Innymi słowy, unikaj takiego podejścia:
// ⛔ 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', 'Zapisz');
$this->setTranslator($translator);
}
}
Zamiast budować formularz w konstruktorze, użyj fabryki.
Ważne jest, żeby zdać sobie sprawę, że klasa Form to przede wszystkim narzędzie do budowania formularzy,
czyli form builder. Zbudowany formularz można uznać za jej produkt. Produkt nie jest jednak szczególnym rodzajem
buildera; nie zachodzi między nimi relacja is a, która jest podstawą dziedziczenia.
Komponent z formularzem
Zupełnie inne podejście polega na utworzeniu komponentu, który zawiera w sobie formularz. Otwiera to nowe możliwości, na przykład renderowanie formularza w szczególny sposób, bo komponent ma własny szablon. Albo można wykorzystać sygnały do komunikacji AJAX-owej i dynamicznego wczytywania informacji do formularza, na przykład dla podpowiedzi 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', 'Zapisz');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private 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);
}
}
Utwórzmy jeszcze fabrykę, która ten komponent będzie produkować. Wystarczy zdefiniować jej interfejs:
interface EditControlFactory
{
function create(): EditControl;
}
I dodać go do pliku konfiguracyjnego:
services:
- EditControlFactory
Teraz możemy poprosić o fabrykę i użyć jej w presenterze:
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');
// albo przekierowanie na wynik edycji, np.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}