Reutilizarea formularelor în mai multe locuri

În Nette, aveți mai multe opțiuni pentru a reutiliza același formular în mai multe locuri fără a duplica codul. În acest articol, vom trece în revistă diferitele soluții, inclusiv pe cele pe care ar trebui să le evitați.

Fabrica de formulare

O abordare de bază pentru utilizarea aceleiași componente în mai multe locuri este de a crea o metodă sau o clasă care generează componenta și apoi de a apela acea metodă în diferite locuri din aplicație. O astfel de metodă sau clasă se numește factory. Vă rugăm să nu faceți confuzie cu modelul de proiectare factory method, care descrie un mod specific de utilizare a fabricilor și nu are legătură cu acest subiect.

Ca exemplu, să creăm o fabrică care va construi un formular de editare:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// câmpurile suplimentare ale formularului sunt adăugate aici
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Acum puteți utiliza această fabrică în diferite locuri din aplicația dumneavoastră, de exemplu în prezentări sau componente. Și facem acest lucru solicitând-o ca dependență. Deci, mai întâi, vom scrie clasa în fișierul de configurare:

services:
	- FormFactory

Și apoi o vom folosi în prezentator:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// prelucrarea datelor trimise
		};
		return $form;
	}
}

Puteți extinde fabrica de formulare cu metode suplimentare pentru a crea alte tipuri de formulare care să se potrivească aplicației dumneavoastră. Și, bineînțeles, puteți adăuga o metodă care să creeze un formular de bază fără elemente, pe care celelalte metode îl vor utiliza:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// câmpurile suplimentare ale formularului sunt adăugate aici
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Metoda createForm() nu face încă nimic util, dar acest lucru se va schimba rapid.

Dependențe de fabrică

În timp, va deveni evident că avem nevoie ca formularele să fie multilingve. Acest lucru înseamnă că trebuie să configurăm un traducător pentru toate formularele. Pentru a face acest lucru, modificăm clasa FormFactory pentru a accepta obiectul Translator ca dependență în constructor și îl transmitem formularului:

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

	//...
}

Deoarece metoda createForm() este apelată și de alte metode care creează formulare specifice, trebuie să setăm translatorul doar în acea metodă. Și am terminat. Nu este nevoie să modificăm niciun cod de prezentator sau de componentă, ceea ce este minunat.

Mai multe clase fabrică

Alternativ, puteți crea mai multe clase pentru fiecare formular pe care doriți să îl utilizați în aplicația dumneavoastră. Această abordare poate crește lizibilitatea codului și face ca formularele să fie mai ușor de gestionat. Lăsați originalul FormFactory pentru a crea doar un formular pur cu o configurație de bază (de exemplu, cu suport pentru traducere) și creați o nouă fabrică EditFormFactory pentru formularul de editare.

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

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


// ✅ utilizarea compoziției
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// aici se adaugă câmpuri suplimentare de formular
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Este foarte important ca legătura dintre clasele FormFactory și EditFormFactory să fie implementată prin compoziție, nu prin moștenirea obiectelor:

// ⛔ NU! MOȘTENIREA NU ARE CE CĂUTA AICI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// câmpurile suplimentare ale formularului se adaugă aici
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Utilizarea moștenirii în acest caz ar fi complet contraproductivă. Ați întâmpina foarte repede probleme. De exemplu, dacă ați dori să adăugați parametri la metoda create(), PHP ar raporta o eroare deoarece semnătura acesteia este diferită de cea a metodei părinte. Sau atunci când treceți o dependență clasei EditFormFactory prin intermediul constructorului. Acest lucru ar cauza ceea ce numim " iadul constructorilor".

În general, este mai bine să se prefere compoziția decât moștenirea.

Gestionarea formularelor

Gestionatorul de formulare care este apelat după o trimitere reușită poate fi, de asemenea, parte a unei clase fabrică. Acesta va funcționa prin transmiterea datelor trimise către model pentru procesare. El va transmite orice eroare înapoi la formular. Modelul din exemplul următor este reprezentat de clasa Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// câmpurile suplimentare ale formularului sunt adăugate aici
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// procesarea datelor trimise
			$this->facade->process($data);

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

Lăsați prezentatorul să se ocupe singur de redirecționare. Acesta va adăuga un alt gestionar la evenimentul onSuccess, care va efectua redirecționarea. Acest lucru va permite ca formularul să fie utilizat în prezentatori diferiți, iar fiecare poate redirecționa către o locație diferită.

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

Această soluție profită de proprietatea formularelor conform căreia, atunci când addError() este apelat pe un formular sau pe un element al acestuia, nu este invocat următorul procesator onSuccess.

Moștenirea din clasa Form

Un formular construit nu ar trebui să fie un copil al unui formular. Cu alte cuvinte, nu utilizați această soluție:

// ⛔ NU! MOȘTENIREA NU ARE CE CĂUTA AICI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// câmpurile suplimentare ale formularului se adaugă aici
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

În loc să construiți formularul în constructor, utilizați fabrica.

Este important să realizăm că clasa Form este în primul rând un instrument de asamblare a unui formular, adică un constructor de formulare. Iar formularul asamblat poate fi considerat produsul său. Cu toate acestea, produsul nu este un caz specific al constructorului; nu există o relație este a între ele, care stă la baza moștenirii.

Componenta Form

O abordare complet diferită constă în crearea unei componente care include un formular. Acest lucru oferă noi posibilități, de exemplu, pentru a reda formularul într-un mod specific, deoarece componenta include un șablon. Sau pot fi utilizate semnale pentru comunicarea AJAX și încărcarea de informații în formular, de exemplu pentru indicii etc.

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:');
		// câmpurile suplimentare ale formularului sunt adăugate aici
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// procesarea datelor trimise
			$this->facade->process($data);

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

		// invocarea unui eveniment
		$this->onSave($this, $data);
	}
}

Să creăm o fabrică care va produce această componentă. Este suficient să îi scriem interfața:

interface EditControlFactory
{
	function create(): EditControl;
}

și să o adăugăm la fișierul de configurare:

services:
	- EditControlFactory

Și acum putem solicita fabrica și o putem folosi în prezentator:

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');
			// sau redirecționarea către rezultatul editării, de exemplu:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}