Formanyomtatványok újrafelhasználása több helyen

A Nette-ben több lehetőséged is van arra, hogy ugyanazt az űrlapot több helyen újra felhasználd anélkül, hogy a kódot duplikálnád. Ebben a cikkben áttekintjük a különböző megoldásokat, beleértve azokat is, amelyeket érdemes elkerülni.

Form Factory

Egy komponens több helyen történő használatának egyik alapvető megközelítése, hogy létrehozunk egy metódust vagy osztályt, amely létrehozza a komponenst, majd ezt a metódust az alkalmazás különböző helyein hívjuk meg. Az ilyen metódust vagy osztályt gyárnak nevezzük. Kérjük, ne keverjük össze a factory method tervezési mintával, amely a gyárak használatának egy speciális módját írja le, és nem kapcsolódik ehhez a témához.

Példaként hozzunk létre egy gyárat, amely egy szerkesztési űrlapot készít:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// itt további űrlapmezők kerülnek hozzáadásra
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Ezt a gyárat az alkalmazás különböző helyein használhatja, például prezenterekben vagy komponensekben. Ezt pedig úgy tesszük, hogy függőségként kérjük be. Tehát először írjuk be az osztályt a konfigurációs fájlba:

services:
	- FormFactory

És aztán használjuk a prezenterben:

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// a küldött adatok feldolgozása
		};
		return $form;
	}
}

A form factory-t további metódusokkal bővíthetjük, hogy más típusú űrlapokat hozzunk létre az alkalmazásunknak megfelelően. És természetesen hozzáadhat egy olyan metódust is, amely egy elemek nélküli alap űrlapot hoz létre, amelyet a többi metódus használni fog:

class FormFactory
{
	public function createForm(): Form
	{
		$form = new Form;
		return $form;
	}

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// itt további űrlapmezők kerülnek hozzáadásra
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

A createForm() metódus egyelőre semmi hasznosat nem csinál, de ez hamarosan megváltozik.

Gyári függőségek

Idővel nyilvánvalóvá válik, hogy az űrlapoknak többnyelvűnek kell lenniük. Ez azt jelenti, hogy minden űrlaphoz be kell állítanunk egy fordítót. Ehhez módosítjuk a FormFactory osztályt, hogy a konstruktorban függőségként elfogadja a Translator objektumot, és átadja 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 más, konkrét űrlapokat létrehozó metódusok is meghívják, a fordítót csak ebben a metódusban kell beállítanunk. És kész is vagyunk. Nem kell semmilyen prezenter vagy komponens kódot módosítani, ami nagyszerű.

További gyári osztályok

Alternatívaként több osztályt is létrehozhat minden egyes űrlaphoz, amelyet az alkalmazásban használni szeretne. Ez a megközelítés növelheti a kód olvashatóságát és megkönnyítheti az űrlapok kezelését. Hagyja meg az eredeti FormFactory címet, hogy csak egy tiszta űrlapot hozzon létre alapvető konfigurációval (például fordítástámogatással), és hozzon létre egy új gyári EditFormFactory címet a szerkesztési űrlaphoz.

class FormFactory
{
	public function __construct(
		private Translator $translator,
	) {
	}

	public function create(): Form
	{
		$form = new Form;
		$form->setTranslator($this->translator);
		return $form;
	}
}


// ✅ a kompozíció használata
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// itt további űrlapmezők kerülnek hozzáadásra
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Nagyon fontos, hogy a FormFactory és a EditFormFactory osztályok közötti kötés kompozícióval, nem pedig objektumörökléssel valósul meg:

// ⛔ NO! AZ ÖRÖKSÉG NEM TARTOZIK IDE
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// további űrlapmezők kerülnek ide
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Az öröklés használata ebben az esetben teljesen kontraproduktív lenne. Nagyon gyorsan problémákba ütközne. Például, ha paramétereket akarnánk hozzáadni a create() metódushoz; a PHP hibát jelezne, hogy a metódus aláírása eltér a szülőétől. Vagy ha a konstruktoron keresztül átadnánk egy függőséget a EditFormFactory osztálynak. Ez azt okozná, amit mi konstruktor pokolnak hívunk.

Általában jobb, ha a kompozíciót előnyben részesítjük az örökléssel szemben.

Form kezelés

A sikeres elküldés után meghívott űrlapkezelő lehet egy gyári osztály része is. Ez úgy fog működni, hogy a beküldött adatokat átadja a modellnek feldolgozásra. Az esetleges hibákat visszaadja az űrlapnak. A következő példában a modellt 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', 'Title:');
		// itt további űrlapmezők kerülnek hozzáadásra
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// a beküldött adatok feldolgozása
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
		}
	}
}

Hagyjuk, hogy a prezenter maga kezelje az átirányítást. A onSuccess eseményhez hozzáad egy másik kezelőt, amely elvégzi az átirányítást. Ez lehetővé teszi, hogy az űrlapot különböző prezenterekben lehessen használni, és mindegyik más helyre irányíthasson át.

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

Ez a megoldás kihasználja az űrlapok azon tulajdonságát, hogy amikor a addError() meghívásra kerül egy űrlapon vagy annak elemén, a következő onSuccess kezelő nem hívódik meg.

Öröklés a Form osztályból

Egy épített űrlap nem lehet egy űrlap gyermeke. Más szóval, ne használja ezt a megoldást:

// ⛔ NO! AZ ÖRÖKSÉG NEM TARTOZIK IDE
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// további űrlapmezők kerülnek ide
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

Ahelyett, hogy az űrlapot a konstruktorban építenéd fel, használd a gyárat.

Fontos tisztában lenni azzal, hogy a Form osztály elsősorban egy űrlap összeállításának eszköze, azaz egy űrlapépítő. Az összerakott űrlap pedig a termékének tekinthető. A termék azonban nem az építő speciális esete; nincs köztük is a kapcsolat, ami az öröklés alapját képezi.

Form komponens

Egy teljesen más megközelítés egy olyan komponens létrehozása, amely egy űrlapot tartalmaz. Ez új lehetőségeket ad, például az űrlap meghatározott módon történő megjelenítésére, mivel a komponens tartalmaz egy sablont. Vagy jeleket lehet használni az AJAX-kommunikációhoz és az információk betöltéséhez az űrlapba, például a hintinghez 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', 'Title:');
		// itt további űrlapmezők kerülnek hozzáadásra
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// a beküldött adatok feldolgozása
			$this->facade->process($data);

		} catch (AnyModelException $e) {
			$form->addError('...');
			return;
		}

		// eseményhívás
		$this->onSave($this, $data);
	}
}

Hozzunk létre egy gyárat, amely ezt a komponenst fogja előállítani. Elég, ha megírjuk az interfészét:

interface EditControlFactory
{
	function create(): EditControl;
}

És adjuk hozzá a konfigurációs fájlhoz:

services:
	- EditControlFactory

És most már kérhetjük a gyárat, és használhatjuk a prezenterben:

class MyPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private EditControlFactory $controlFactory,
	) {
	}

	protected function createComponentEditForm(): Form
	{
		$control = $this->controlFactory->create();

		$control->onSave[] = function (EditControl $control, $data) {
			$this->redirect('this');
			// vagy átirányítás a szerkesztés eredményére, pl.:
			// $this->redirect('részlet', ['id' => $data->id]);
		};

		return $control;
	}
}