Reutilizarea formularelor în mai multe locuri

În Nette aveți la dispoziție mai multe opțiuni pentru a utiliza același formular în mai multe locuri și a nu duplica codul. În acest articol vom prezenta diverse soluții, inclusiv cele pe care ar trebui să le evitați.

Fabrica de formulare

Una dintre abordările de bază pentru utilizarea aceleiași componente în mai multe locuri este crearea unei metode sau clase care generează această componentă și apoi apelarea acestei metode în diferite locuri ale aplicației. O astfel de metodă sau clasă se numește fabrică. Vă rugăm să nu confundați 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, vom crea 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', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		return $form;
	}
}

Acum puteți utiliza această fabrică în diferite locuri din aplicația dvs., de exemplu în presenteri sau componente. Și asta prin solicitarea ei ca dependență. Mai întâi, vom înregistra clasa în fișierul de configurare:

services:
	- FormFactory

Și apoi o vom folosi într-un presenter:

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

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

Puteți extinde fabrica de formulare cu alte metode pentru crearea altor tipuri de formulare, în funcție de nevoile aplicației dvs. Și, desigur, putem adăuga și o metodă care creează un formular de bază fără elemente, pe care celelalte metode o vor utiliza:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		return $form;
	}
}

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

Dependențele fabricii

Cu timpul, se va dovedi că avem nevoie ca formularele să fie multilingve. Acest lucru înseamnă că trebuie să setăm un așa-numit translator pentru toate formularele. În acest scop, vom modifica clasa FormFactory astfel încât să accepte obiectul Translator ca dependență în constructor și să-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 celelalte metode care creează formulare specifice, este suficient să setăm translatorul doar în ea. Și am terminat. Nu este nevoie să modificăm codul niciunui presenter sau componente, ceea ce este grozav.

Mai multe clase de fabrici

Alternativ, puteți crea mai multe clase pentru fiecare formular pe care doriți să-l utilizați în aplicația dvs. Această abordare poate crește lizibilitatea codului și facilita gestionarea formularelor. Vom lăsa FormFactory originală să creeze doar un formular curat cu configurația de bază (de exemplu, cu suport pentru traduceri) și vom crea 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ă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		return $form;
	}
}

Este foarte important ca legătura dintre clasele FormFactory și EditFormFactory să fie realizată prin compoziție, nu prin moștenire de obiecte:

// ⛔ NU AȘA! MOȘTENIREA NU APARȚINE AICI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		return $form;
	}
}

Utilizarea moștenirii ar fi complet contraproductivă în acest caz. Ați întâmpina probleme foarte rapid. De exemplu, în momentul în care ați dori să adăugați parametri metodei create(); PHP ar raporta o eroare că semnătura sa diferă de cea a părintelui. Sau la transmiterea dependențelor către clasa EditFormFactory prin constructor. Ar apărea o situație pe care o numim constructor hell.

În general, este mai bine să preferăm compoziția în detrimentul moștenirii.

Gestionarea formularului

Gestionarea formularului, care este apelată după trimiterea cu succes, poate fi, de asemenea, parte a clasei fabricii. Va funcționa prin transmiterea datelor trimise către model pentru procesare. Eventualele erori le va transmite înapoi formularului. 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', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		$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('...');
		}
	}
}

Redirecționarea în sine o vom lăsa însă pe seama presenterului. Acesta va adăuga evenimentului onSuccess un alt handler care va efectua redirecționarea. Datorită acestui fapt, va fi posibilă utilizarea formularului în diferiți presenteri și redirecționarea către locuri diferite în fiecare.

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('Înregistrarea a fost salvată');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Această soluție utilizează proprietatea formularelor că, atunci când se apelează addError() pe formular sau pe elementele sale, următorul handler onSuccess nu mai este apelat.

Moștenirea de la clasa Form

Formularul construit nu trebuie să fie un descendent al formularului. Cu alte cuvinte, nu utilizați această soluție:

// ⛔ NU AȘA! MOȘTENIREA NU APARȚINE AICI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$this->addSubmit('send', 'Trimite');
		$this->setTranslator($translator);
	}
}

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

Este necesar să realizăm că clasa Form este în primul rând un instrument pentru construirea unui formular, adică un form builder. Iar formularul construit poate fi considerat produsul său. Însă produsul nu este un caz specific al builder-ului, nu există între ele o legătură is a care stă la baza moștenirii.

Componenta cu formular

O abordare complet diferită este crearea unei componente, care include un formular. Acest lucru oferă noi posibilități, de exemplu, redarea formularului într-un mod specific, deoarece componenta include și un șablon. Sau se pot utiliza semnale pentru comunicarea AJAX și încărcarea suplimentară a informațiilor în formular, de exemplu pentru sugestii, 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', 'Titlu:');
		// aici se adaugă alte câmpuri de formular
		$form->addSubmit('send', 'Trimite');
		$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;
		}

		// declanșarea evenimentului
		$this->onSave($this, $data);
	}
}

Vom crea și o fabrică care va produce această componentă. Este suficient să înregistrăm interfața sa:

interface EditControlFactory
{
	function create(): EditControl;
}

Și să o adăugăm în fișierul de configurare:

services:
	- EditControlFactory

Și acum putem solicita fabrica și o putem utiliza în presenter:

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

		return $control;
	}
}