Riutilizzo dei moduli in più punti

In Nette avete diverse opzioni per utilizzare lo stesso modulo in più punti senza duplicare il codice. In questo articolo mostreremo diverse soluzioni, comprese quelle che dovreste evitare.

Factory per moduli

Uno degli approcci fondamentali per utilizzare lo stesso componente in più punti è creare un metodo o una classe che genera questo componente e successivamente chiamare questo metodo in diversi punti dell'applicazione. Tale metodo o classe viene chiamato factory. Si prega di non confondere con il pattern di progettazione factory method, che descrive un modo specifico di utilizzare le factory e non è correlato a questo argomento.

Come esempio, creiamo una factory che costruirà un modulo di modifica:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		return $form;
	}
}

Ora potete utilizzare questa factory in diversi punti della vostra applicazione, ad esempio nei presenter o nei componenti. E questo richiedendola come dipendenza. Prima di tutto, quindi, registriamo la classe nel file di configurazione:

services:
	- FormFactory

E poi la utilizziamo nel 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 () {
			// elaborazione dei dati inviati
		};
		return $form;
	}
}

Potete estendere la factory dei moduli con altri metodi per creare altri tipi di moduli secondo le esigenze della vostra applicazione. E naturalmente possiamo aggiungere anche un metodo che crei un modulo base senza elementi, e che gli altri metodi utilizzeranno:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		return $form;
	}
}

Il metodo createForm() per ora non fa nulla di utile, ma questo cambierà rapidamente.

Dipendenze della factory

Col tempo si scoprirà che abbiamo bisogno che i moduli siano multilingue. Ciò significa che a tutti i moduli dobbiamo impostare il cosiddetto translator. A tal fine, modifichiamo la classe FormFactory in modo che accetti l'oggetto Translator come dipendenza nel costruttore e lo passiamo al modulo:

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

	// ...
}

Poiché il metodo createForm() viene chiamato anche dagli altri metodi che creano moduli specifici, è sufficiente impostare il translator solo lì. E abbiamo finito. Non è necessario modificare il codice di nessun presenter o componente, il che è fantastico.

Più classi factory

In alternativa, potete creare più classi per ogni modulo che volete utilizzare nella vostra applicazione. Questo approccio può aumentare la leggibilità del codice e facilitare la gestione dei moduli. Lasciamo che la FormFactory originale crei solo un modulo pulito con la configurazione di base (ad esempio con il supporto alle traduzioni) e per il modulo di modifica creiamo una nuova factory EditFormFactory.

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

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


// ✅ uso della composizione
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		return $form;
	}
}

È molto importante che la relazione tra le classi FormFactory e EditFormFactory sia realizzata tramite composizione, e non tramite ereditarietà degli oggetti:

// ⛔ NON COSÌ! L'EREDITARIETÀ NON APPARTIENE QUI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		return $form;
	}
}

L'uso dell'ereditarietà sarebbe in questo caso del tutto controproducente. Incontrereste problemi molto rapidamente. Ad esempio, nel momento in cui voleste aggiungere parametri al metodo create(); PHP segnalerebbe un errore indicando che la sua firma differisce da quella del genitore. Oppure passando una dipendenza alla classe EditFormFactory tramite il costruttore. Si verificherebbe una situazione che chiamiamo constructor hell.

In generale, è meglio preferire la composizione all'ereditarietà.

Gestione del modulo

La gestione del modulo, che viene chiamata dopo l'invio riuscito, può anche far parte della classe factory. Funzionerà passando i dati inviati al modello per l'elaborazione. Eventuali errori verranno restituiti al modulo. Il modello nell'esempio seguente è rappresentato dalla classe Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// elaborazione dei dati inviati
			$this->facade->process($data);

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

Tuttavia, lasceremo il reindirizzamento effettivo al presenter. Aggiungerà un altro gestore all'evento onSuccess, che eseguirà il reindirizzamento. Grazie a ciò, sarà possibile utilizzare il modulo in diversi presenter e reindirizzare a un luogo diverso in ciascuno di essi.

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('Il record è stato salvato');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Questa soluzione sfrutta la proprietà dei moduli secondo cui, quando viene chiamato addError() sul modulo o su un suo elemento, il successivo gestore onSuccess non viene più chiamato.

Ereditarietà dalla classe Form

Il modulo costruito non deve essere un discendente del modulo. In altre parole, non utilizzate questa soluzione:

// ⛔ NON COSÌ! L'EREDITARIETÀ NON APPARTIENE QUI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$this->addSubmit('send', 'Invia');
		$this->setTranslator($translator);
	}
}

Invece di costruire il modulo nel costruttore, utilizzate una factory.

È necessario rendersi conto che la classe Form è principalmente uno strumento per costruire un modulo, ovvero un form builder. E il modulo costruito può essere considerato come il suo prodotto. Ma il prodotto non è un caso specifico del builder, non c'è tra loro una relazione is a che costituisce la base dell'ereditarietà.

Componente con modulo

Un approccio completamente diverso è la creazione di un componente, che include un modulo. Questo offre nuove possibilità, ad esempio renderizzare il modulo in modo specifico, poiché il componente include anche un template. Oppure è possibile utilizzare i segnali per la comunicazione AJAX e il caricamento dinamico di informazioni nel modulo, ad esempio per i suggerimenti, ecc.

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', 'Titolo:');
		// qui vengono aggiunti altri campi del modulo
		$form->addSubmit('send', 'Invia');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// elaborazione dei dati inviati
			$this->facade->process($data);

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

		// attivazione dell'evento
		$this->onSave($this, $data);
	}
}

Creeremo anche una factory che produrrà questo componente. È sufficiente scrivere la sua interfaccia:

interface EditControlFactory
{
	function create(): EditControl;
}

E aggiungerla al file di configurazione:

services:
	- EditControlFactory

E ora possiamo richiedere la factory e utilizzarla nel 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');
			// o reindirizziamo al risultato della modifica, ad esempio:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}