Űrlapok újrafelhasználása több helyen
A Nette-ben több lehetőség is rendelkezésre áll ugyanazon űrlap több helyen történő használatára a kód duplikálása nélkül. Ebben a cikkben különböző megoldásokat mutatunk be, beleértve azokat is, amelyeket érdemes elkerülni.
Űrlap Factory
Az egyik alapvető megközelítés ugyanazon komponens több helyen történő használatára egy olyan metódus vagy osztály létrehozása, amely ezt a komponenst generálja, majd ennek a metódusnak a meghívása az alkalmazás különböző pontjain. Egy ilyen metódust vagy osztályt factory-nak nevezünk. Kérjük, ne keverje össze a factory method tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához.
Példaként létrehozunk egy factory-t, amely egy szerkesztő űrlapot fog összeállítani:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
return $form;
}
}
Most már használhatja ezt a factory-t az alkalmazás különböző pontjain, például presenterekben vagy komponensekben. Ezt úgy teheti meg, hogy függőségként kérjük. Először tehát regisztráljuk az osztályt a konfigurációs fájlban:
services:
- FormFactory
Majd használjuk a presenterben:
class MyPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FormFactory $formFactory,
) {
}
protected function createComponentEditForm(): Form
{
$form = $this->formFactory->createEditForm();
$form->onSuccess[] = function () {
// beküldött adatok feldolgozása
};
return $form;
}
}
Az űrlap factory-t kibővítheti további metódusokkal más típusú űrlapok létrehozásához az alkalmazás igényei szerint. És természetesen hozzáadhatunk egy metódust is, amely létrehoz egy alap űrlapot elemek nélkül, és ezt a többi metódus fogja használni:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
return $form;
}
}
A createForm()
metódus egyelőre nem csinál semmi hasznosat, de ez hamarosan megváltozik.
A Factory függőségei
Idővel kiderül, hogy szükségünk van arra, hogy az űrlapok többnyelvűek legyenek. Ez azt jelenti, hogy minden űrlaphoz
be kell állítanunk az úgynevezett translator-t. Ebből a
célból módosítjuk a FormFactory
osztályt úgy, hogy a konstruktorban függőségként fogadja el a
Translator
objektumot, és átadjuk azt az űrlapnak:
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;
}
// ...
}
Mivel a createForm()
metódust a többi, specifikus űrlapokat létrehozó metódus is meghívja, elegendő a
translatort csak ebben beállítani. És készen is vagyunk. Nincs szükség egyetlen presenter vagy komponens kódjának
módosítására sem, ami nagyszerű.
Több Factory osztály
Alternatív megoldásként létrehozhat több osztályt minden egyes űrlaphoz, amelyet használni szeretne az
alkalmazásában. Ez a megközelítés növelheti a kód olvashatóságát és megkönnyítheti az űrlapok kezelését. Az
eredeti FormFactory
-t csak egy tiszta űrlap létrehozására hagyjuk meg alapkonfigurációval (például
fordítási támogatással), és a szerkesztő űrlaphoz létrehozunk egy új EditFormFactory
factory-t.
class FormFactory
{
public function __construct(
private Translator $translator,
) {
}
public function create(): Form
{
$form = new Form;
$form->setTranslator($this->translator);
return $form;
}
}
// ✅ kompozíció használata
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
return $form;
}
}
Nagyon fontos, hogy a FormFactory
és az EditFormFactory
osztályok közötti kapcsolat kompozícióval valósuljon meg, nem
pedig objektum
öröklődéssel:
// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
return $form;
}
}
Az öröklődés használata ebben az esetben teljesen kontraproduktív lenne. Nagyon gyorsan problémákba ütköznél.
Például abban a pillanatban, amikor paramétereket szeretnél hozzáadni a create()
metódushoz; a PHP hibát
jelezne, hogy a szignatúrája eltér a szülőétől. Vagy amikor függőséget adnál át az EditFormFactory
osztálynak a konstruktoron keresztül. Olyan helyzet állna elő, amelyet constructor hell-nek
nevezünk.
Általában jobb előnyben részesíteni a kompozíciót az öröklődéssel szemben.
Űrlapkezelés
Az űrlapkezelő, amely a sikeres beküldés után hívódik meg, szintén lehet a factory osztály része. Úgy fog működni,
hogy a beküldött adatokat átadja a modellnek feldolgozásra. Az esetleges hibákat visszaadja az űrlapnak. A modellt a következő
példában a Facade
osztály képviseli:
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
private Facade $facade,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
$form->addText('title', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// beküldött adatok feldolgozása
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Magát az átirányítást azonban a presenterre bízzuk. Az onSuccess
eseményhez hozzáad egy további handlert,
amely végrehajtja az átirányítást. Ennek köszönhetően az űrlapot különböző presenterekben lehet majd használni, és
mindegyikben máshová lehet átirányítani.
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('A rekord mentésre került');
$this->redirect('Homepage:');
};
return $form;
}
}
Ez a megoldás kihasználja az űrlapok azon tulajdonságát, hogy ha az űrlapon vagy annak egy elemén meghívják az
addError()
metódust, akkor a további onSuccess
handler már nem hívódik meg.
Öröklődés a Form osztályból
Az összeállított űrlapnak nem szabad az űrlap leszármazottjának lennie. Más szavakkal, ne használja ezt a megoldást:
// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$this->addSubmit('send', 'Küldés');
$this->setTranslator($translator);
}
}
Az űrlap konstruktorban történő összeállítása helyett használjon factory-t.
Fontos megérteni, hogy a Form
osztály elsősorban egy eszköz az űrlap összeállítására, tehát egy
form builder. Az összeállított űrlap pedig tekinthető annak termékének. Azonban a termék nem a builder specifikus
esete, nincs közöttük is a kapcsolat, amely az öröklődés alapját képezi.
Komponens űrlappal
Egy teljesen más megközelítés egy olyan komponens létrehozását jelenti, amelynek része egy űrlap. Ez új lehetőségeket kínál, például az űrlap specifikus módon történő renderelését, mivel a komponensnek része egy sablon is. Vagy használhatunk signálokat AJAX kommunikációhoz és információk betöltéséhez az űrlapba, például súgáshoz stb.
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', 'Cím:');
// itt adjuk hozzá a további űrlapmezőket
$form->addSubmit('send', 'Küldés');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// beküldött adatok feldolgozása
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// esemény kiváltása
$this->onSave($this, $data);
}
}
Még létrehozunk egy factory-t, amely ezt a komponenst fogja gyártani. Elég felírni az interfészét:
interface EditControlFactory
{
function create(): EditControl;
}
És hozzáadjuk a konfigurációs fájlhoz:
services:
- EditControlFactory
És most már kérhetjük a factory-t és használhatjuk a presenterben:
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');
// vagy átirányítunk a szerkesztés eredményére, pl.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}