Ponovna uporaba obrazcev na več mestih

V Nette imate več možnosti za ponovno uporabo istega obrazca na več mestih brez podvajanja kode. V tem članku bomo pregledali različne rešitve, vključno s tistimi, ki se jim morate izogniti.

Tovarna obrazcev

Osnovni pristop k uporabi iste komponente na več mestih je ustvarjanje metode ali razreda, ki generira komponento, in nato klicanje te metode na različnih mestih v aplikaciji. Takšna metoda ali razred se imenuje factory. Ne zamenjujte z načrtovalskim vzorcem tovarniška metoda, ki opisuje poseben način uporabe tovarn in ni povezan s to temo.

Kot primer ustvarimo tovarno, ki bo ustvarila obrazec za urejanje:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// tukaj so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Zdaj lahko to tovarno uporabite na različnih mestih v aplikaciji, na primer v predstavitvah ali komponentah. To storimo tako, da jo zahtevamo kot odvisnost. Najprej bomo razred zapisali v konfiguracijsko datoteko:

services:
	- FormFactory

Nato ga bomo uporabili v predstavitvi:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// obdelava poslanih podatkov
		};
		return $form;
	}
}

Tovarno obrazcev lahko razširite z dodatnimi metodami in tako ustvarite druge vrste obrazcev, ki ustrezajo vaši aplikaciji. Seveda lahko dodate tudi metodo, ki ustvari osnovni obrazec brez elementov, ki ga bodo uporabljale druge metode:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// tukaj so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Metoda createForm() še ne počne ničesar uporabnega, vendar se bo to hitro spremenilo.

Tovarniške odvisnosti

Sčasoma se bo izkazalo, da potrebujemo večjezične obrazce. To pomeni, da moramo za vse obrazce vzpostaviti prevajalnik. V ta namen spremenimo razred FormFactory tako, da v konstruktorju sprejme objekt Translator kot odvisnost in ga posreduje obrazcu:

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

	//...
}

Ker metodo createForm() kličejo tudi druge metode, ki ustvarjajo določene obrazce, moramo prevajalnik nastaviti le v tej metodi. In končali smo. Ni treba spreminjati kode predstavnika ali komponente, kar je odlično.

Več tovarniških razredov

Ustvarite lahko tudi več razredov za vsak obrazec, ki ga želite uporabiti v svoji aplikaciji. Ta pristop lahko poveča berljivost kode in olajša upravljanje obrazcev. Pustite prvotni FormFactory za ustvarjanje samo čistega obrazca z osnovno konfiguracijo (na primer s podporo za prevajanje) in ustvarite novo tovarno EditFormFactory za obrazec za urejanje.

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

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


// ✅ uporaba sestave
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// tu so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Zelo pomembno je, da se povezava med razredoma FormFactory in EditFormFactory izvaja s kompozicijo in ne z dedovanjem objektov:

// ⛔ NE! DEDIŠČINA NE SPADA SEM
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// tu so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Uporaba dedovanja bi bila v tem primeru popolnoma neproduktivna. Zelo hitro bi naleteli na težave. Na primer, če bi želeli metodi create() dodati parametre; PHP bi sporočil napako, ker se njen podpis razlikuje od podpisa nadrejene metode. Ali pa pri posredovanju odvisnosti razredu EditFormFactory prek konstruktorja. To bi povzročilo tako imenovani konstruktorski pekel.

Na splošno je bolje dati prednost sestavi pred dedovanjem.

Ravnanje z obrazci

Obvladovalnik obrazca, ki se pokliče po uspešni oddaji, je lahko tudi del tovarniškega razreda. Deloval bo tako, da bo predložene podatke posredoval modelu v obdelavo. Morebitne napake bo posredoval nazaj v obrazec. Model v naslednjem primeru predstavlja razred Facade:

class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
		private Facade $facade,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// tukaj so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// obdelava posredovanih podatkov
			$this->facade->process($data);

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

Naj predstavnik sam poskrbi za preusmeritev. Dogodku onSuccess bo dodal še eno izvajalko, ki bo izvedla preusmeritev. To bo omogočilo uporabo obrazca v različnih predstavitvenih programih, pri čemer lahko vsak od njih preusmeri na drugo mesto.

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

Ta rešitev izkorišča lastnost obrazcev, da se ob klicu addError() na obrazcu ali njegovem elementu ne prikliče naslednji izvajalec onSuccess.

Dedovanje iz razreda Form

Vgrajeni obrazec naj ne bi bil otrok obrazca. Z drugimi besedami, ne uporabljajte te rešitve:

// ⛔ NE! DEDIŠČINA NE SPADA SEM
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// tu so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

Namesto da bi obrazec zgradili v konstruktorju, uporabite tovarno.

Pomembno se je zavedati, da je razred Form predvsem orodje za sestavljanje obrazca, tj. gradnik obrazca. Sestavljeni obrazec pa lahko štejemo za njegov izdelek. Vendar izdelek ni poseben primer gradnika; med njima ni razmerja is a, ki je osnova dedovanja.

Komponenta obrazca

Povsem drugačen pristop je ustvarjanje komponente, ki vključuje obrazec. To daje nove možnosti, na primer prikazovanje obrazca na poseben način, saj komponenta vključuje predlogo. Ali pa se lahko signali uporabijo za komunikacijo AJAX in nalaganje informacij v obrazec, na primer za namige 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', 'Title:');
		// tukaj so dodana dodatna polja obrazca
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// obdelava posredovanih podatkov
			$this->facade->process($data);

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

		// priklic dogodka
		$this->onSave($this, $data);
	}
}

Ustvarimo tovarno, ki bo izdelala to komponento. Dovolj je, če napišemo njen vmesnik:

interface EditControlFactory
{
	function create(): EditControl;
}

in ga dodamo v konfiguracijsko datoteko:

services:
	- EditControlFactory

Zdaj lahko zahtevamo tovarno in jo uporabimo v predstavitvenem programu:

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');
			// ali preusmerite na rezultat urejanja, npr.:
			// $this->reirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}