Riutilizzare i form in più punti
Nette offre diverse possibilità per usare lo stesso form in più punti senza duplicare il codice. In questo articolo mostreremo le varie soluzioni, comprese quelle che dovreste evitare.
Factory di form
Un approccio di base per usare lo stesso componente in più punti è creare un metodo o una classe che genera questo componente, e poi richiamarlo dai vari punti dell'applicazione. Un metodo o una classe del genere si chiama factory. Non confondetela per favore con il design pattern metodo factory, che descrive un modo particolare di usare le factory e con questo tema non ha nulla a che vedere.
Come esempio creiamo una factory che costruisce un form di modifica:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Titolo:');
// qui si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
return $form;
}
}
Ora potete usare questa factory in vari punti della vostra applicazione, per esempio nei presenter o nei componenti. E lo fate facendovela passare come dipendenza. Per prima cosa quindi registriamo la classe nel file di configurazione:
services:
- FormFactory
E poi la usiamo 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;
}
}
La factory di form la potete estendere con altri metodi per creare gli altri tipi di form di cui la vostra applicazione ha bisogno. E naturalmente possiamo aggiungere anche un metodo che crea un form di base senza elementi, che gli altri metodi poi useranno:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Titolo:');
// qui si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
return $form;
}
}
Il metodo createForm() non fa ancora nulla di utile, ma questo cambierà presto.
Dipendenze della factory
Con il tempo può emergere l'esigenza che i form siano multilingue. Il che significa che a tutti i form dobbiamo impostare un
translator. Per farlo modifichiamo la classe
FormFactory in modo che si faccia passare nel costruttore l'oggetto Translator e lo imposti al form
creato:
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 form concreti, basta impostare
il translator solo qui. E abbiamo finito. Non serve modificare il codice di nessun presenter o componente, il che è ottimo.
Più classi factory
In alternativa potete creare più classi per ogni form che volete usare nell'applicazione. Questo approccio può aumentare la
leggibilità del codice e rendere più semplice la gestione dei form. Lasciamo che la FormFactory originale crei solo
un form puro con la configurazione di base (per esempio con il supporto alla traduzione) e per il form 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 si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
return $form;
}
}
È molto importante che il legame tra le classi FormFactory e EditFormFactory sia realizzato con la
composizione e non con l'ereditarietà tra oggetti:
// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Titolo:');
// qui si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
return $form;
}
}
Usare qui l'ereditarietà sarebbe del tutto controproducente. Incontrereste dei problemi molto in fretta. Per esempio, nel
momento in cui voleste aggiungere dei parametri al metodo create(), PHP segnalerebbe un errore perché la sua firma
sarebbe diversa da quella dell'antenato. Oppure nel momento in cui passaste alla classe EditFormFactory una
dipendenza tramite il costruttore. Nascerebbe quello che si chiama inferno dei
costruttori.
In generale è meglio preferire la composizione all'ereditarietà.
Gestione del form
Anche il gestore del form, che viene richiamato dopo l'invio riuscito, può far parte della classe factory. Funzionerà
passando i dati inviati al modello perché li elabori. Gli eventuali errori li restituirà al form. 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 si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private function processForm(Form $form, array $data): void
{
try {
// elaborazione dei dati inviati
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
Il redirect vero e proprio lo lasciamo però al presenter. Esso aggiunge all'evento onSuccess un altro gestore,
che esegue il redirect. Grazie a questo si potrà usare il form in presenter diversi e ognuno reindirizzerà altrove.
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 form per cui, se sul form o su un suo elemento viene chiamato
addError(), i gestori onSuccess successivi non vengono più richiamati.
Ereditare dalla classe Form
Un form assemblato non dovrebbe essere un discendente della classe Form. In altre parole, non usate questa
soluzione:
// ⛔ NO! L'EREDITARIETÀ QUI NON C'ENTRA
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Titolo:');
// qui si aggiungono gli altri campi del form
$this->addSubmit('send', 'Salva');
$this->setTranslator($translator);
}
}
Invece di assemblare il form nel costruttore, usate una factory.
È importante rendersi conto che la classe Form è anzitutto uno strumento per assemblare i form, cioè un
form builder. E il form assemblato si può considerare il suo prodotto. Ma il prodotto non è un caso particolare del
builder, tra loro non c'è la relazione is a su cui si fonda l'ereditarietà.
Componente con form
Un approccio del tutto diverso consiste nel creare un componente che contiene un form. Questo dà nuove possibilità, per esempio renderizzare il form in un modo particolare, dato che il componente ha un proprio template. Oppure si possono usare i segnali per la comunicazione AJAX e per caricare informazioni nel form, per esempio per i suggerimenti, e così via.
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 si aggiungono gli altri campi del form
$form->addSubmit('send', 'Salva');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private function processForm(Form $form, array $data): void
{
try {
// elaborazione dei dati inviati
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// richiamo dell'evento
$this->onSave($this, $data);
}
}
Creiamo poi una factory che produrrà questo componente. Basta definirne l'interfaccia:
interface EditControlFactory
{
function create(): EditControl;
}
E aggiungerla al file di configurazione:
services:
- EditControlFactory
E ora possiamo farci passare la factory e usarla 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');
// oppure redirect al risultato della modifica, per esempio:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}