Ű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;
	}
}