Reutilización de formularios en varios sitios

En Nette, tienes varias opciones para reutilizar el mismo formulario en múltiples lugares sin duplicar código. En este artículo, repasaremos las diferentes soluciones, incluyendo las que deberías evitar.

Fábrica de formularios

Un enfoque básico para utilizar el mismo componente en múltiples lugares es crear un método o clase que genere el componente, y luego llamar a ese método en diferentes lugares de la aplicación. Este método o clase se denomina fábrica.

Por ejemplo, podemos crear una clase que genere un formulario de edición:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// aquí se añaden campos de formulario adicionales
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Ahora puedes usar esta fábrica en diferentes lugares de tu aplicación, por ejemplo en presentadores o componentes. Y hacemos esto solicitándola como una dependencia. Así que primero, escribiremos la clase en el archivo de configuración:

services:
	- FormFactory

Y luego la usamos en el presentador:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// tratamiento de los datos enviados
		};
		return $form;
	}
}

Puedes extender la fábrica de formularios con métodos adicionales para crear otros tipos de formularios que se adapten a tu aplicación. Y, por supuesto, puedes añadir un método que cree un formulario básico sin elementos, que utilizarán los demás métodos:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// aquí se añaden campos de formulario adicionales
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

El método createForm() no hace nada útil todavía, pero eso cambiará rápidamente.

Dependencias de fábrica

Con el tiempo, se hará evidente que necesitamos que los formularios sean multilingües. Esto significa que necesitamos configurar un traductor para todos los formularios. Para ello, modificamos la clase FormFactory para que acepte el objeto Translator como dependencia en el constructor, y lo pase al formulario:

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

	//...
}

Como el método createForm() también es llamado por otros métodos que crean formularios específicos, sólo necesitamos establecer el traductor en ese método. Y ya está. No hay necesidad de cambiar ningún código de presentador o componente, lo cual es genial.

Más clases de fábrica

Alternativamente, puede crear múltiples clases para cada formulario que desee utilizar en su aplicación. Este enfoque puede aumentar la legibilidad del código y hacer que los formularios sean más fáciles de gestionar. Deje el original FormFactory para crear sólo un formulario puro con configuración básica (por ejemplo, con soporte de traducción) y cree una nueva fábrica EditFormFactory para el formulario de edición.

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

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


// ✅ uso de la composición
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// aquí se añaden campos de formulario adicionales
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Es muy importante que la unión entre las clases FormFactory y EditFormFactory se implemente por composición, no por herencia de objetos:

// ⛔ ¡NO! LA HERENCIA NO PERTENECE AQUÍ
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// los campos de formulario adicionales se añaden aquí
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Utilizar la herencia sería completamente contraproducente y te encontrarías con problemas muy rápidamente, por ejemplo si quisieras utilizar parámetros en uno de los métodos de create(). Pruébalo a propósito.

Manejo de Formularios

El manejador de formularios que es llamado después de un envío exitoso también puede ser parte de una clase fábrica. Funcionará pasando los datos enviados al modelo para su procesamiento. Devolverá cualquier error al formulario. El modelo en el siguiente ejemplo está representado por la clase Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// aquí se añaden campos de formulario adicionales
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// tratamiento de los datos enviados
			$this->facade->process($data);

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

Deje que el presentador se encargue de la redirección. Añadirá otro manejador al evento onSuccess, que realizará la redirección. Esto permitirá utilizar el formulario en diferentes presentadores, y cada uno puede redirigir a una ubicación diferente.

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

Esta solución aprovecha la propiedad de los formularios de que, cuando se llama a addError() sobre un formulario o su elemento, no se invoca al siguiente manejador onSuccess.

Heredando de la clase Form

Un formulario especializado no debe ser descendiente de un formulario. En otras palabras, nunca debe utilizar una solución de este tipo:

// ⛔ ¡NO! LA HERENCIA NO PERTENECE AQUÍ
class EditForm extends Form
{
	public function __construct(ITranslator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// los campos de formulario adicionales se añaden aquí
		$form->addSubmit('send', 'Save');
	}
}

Heredar de la clase Form lleva a mezclar responsabilidades entre clases y dificulta el mantenimiento del código. Por lo tanto, utilice siempre fábrica en lugar de herencia.

Componente Form

Un enfoque completamente diferente es crear un componente que incluya un formulario. Esto da nuevas posibilidades, por ejemplo para renderizar el formulario de una manera específica, ya que el componente incluye una plantilla. O se pueden utilizar señales para la comunicación AJAX y cargar información en el formulario, por ejemplo para sugerencias, 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:');
		// aquí se añaden campos de formulario adicionales
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// procesamiento de los datos enviados
			$this->facade->process($data);

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

		// invocación de eventos
		$this->onSave($this, $data);
	}
}

Vamos a crear una fábrica que producirá este componente. Basta con escribir su interfaz:

interface EditControlFactory
{
	function create(): EditControl;
}

Y añadirla al fichero de configuración:

services:
	- EditControlFactory

Y ahora podemos solicitar la fábrica y utilizarla en el presentador:

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

	protected function createComponentEditForm(): Form
	{
		$control = $this->controlFactory->create();

		$control->onSave[] = function (EditControl $control, $data) {
			$this->redirect('this');
			// o redirigir al resultado de la edición, por ejemplo
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}