Reutilización de formularios en múltiples lugares

En Nette, tienes varias opciones para usar el mismo formulario en múltiples lugares sin duplicar código. En este artículo, mostraremos diferentes soluciones, incluidas aquellas que deberías evitar.

Fábrica de formularios

Uno de los enfoques básicos para usar el mismo componente en múltiples lugares es crear un método o clase que genere este componente y luego llamar a este método en diferentes lugares de la aplicación. Tal método o clase se llama fábrica. Por favor, no lo confundas con el patrón de diseño factory method, que describe una forma específica de usar fábricas y no está relacionado con este tema.

Como ejemplo, crearemos una fábrica que construirá un formulario de edición:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Título:');
		// aquí se añaden otros campos del formulario
		$form->addSubmit('send', 'Enviar');
		return $form;
	}
}

Ahora puedes usar esta fábrica en diferentes lugares de tu aplicación, por ejemplo, en Presenters o componentes. Y lo haces solicitándola como dependencia. Primero, registramos la clase en el archivo de configuración:

services:
	- FormFactory

Y luego la usamos en 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 () {
			// procesamiento de los datos enviados
		};
		return $form;
	}
}

Puedes extender la fábrica de formularios con métodos adicionales para crear otros tipos de formularios según las necesidades de tu aplicación. Y, por supuesto, también podemos añadir un método que cree un formulario base sin elementos, que los otros métodos utilizarán:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Título:');
		// aquí se añaden otros campos del formulario
		$form->addSubmit('send', 'Enviar');
		return $form;
	}
}

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

Dependencias de la fábrica

Con el tiempo, resultará que necesitamos que los formularios sean multilingües. Esto significa que debemos establecer el llamado traductor para todos los formularios. Para ello, modificaremos la clase FormFactory para que acepte un objeto Translator como dependencia en el constructor y lo pasaremos 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;
	}

	// ...
}

Dado que el método createForm() también es llamado por otros métodos que crean formularios específicos, basta con establecer el traductor solo en él. Y hemos terminado. No es necesario cambiar el código de ningún Presenter o componente, lo cual es genial.

Múltiples clases de fábrica

Alternativamente, puedes crear múltiples clases para cada formulario que quieras usar en tu aplicación. Este enfoque puede aumentar la legibilidad del código y facilitar la gestión de los formularios. Dejaremos que la FormFactory original cree solo un formulario limpio con la configuración básica (por ejemplo, con soporte para traducciones) y crearemos 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 composición
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

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

Es muy importante que la relación entre las clases FormFactory y EditFormFactory se realice mediante composición, y no mediante herencia de objetos:

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

Usar la herencia en este caso sería completamente contraproducente. Te encontrarías con problemas muy rápidamente. Por ejemplo, en el momento en que quisieras añadir parámetros al método create(); PHP informaría de un error indicando que su firma difiere de la del padre. O al pasar dependencias a la clase EditFormFactory a través del constructor. Se produciría una situación que llamamos constructor hell.

En general, es mejor preferir la composición sobre la herencia.

Manejo del formulario

El manejador del formulario, que se llama después de un envío exitoso, también puede ser parte de la clase de fábrica. Funcionará pasando los datos enviados al modelo para su procesamiento. Los posibles errores se pasarán de vuelta 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', 'Título:');
		// aquí se añaden otros campos del formulario
		$form->addSubmit('send', 'Enviar');
		$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('...');
		}
	}
}

Sin embargo, dejaremos la redirección real al Presenter. Este añadirá otro manejador al evento onSuccess que realizará la redirección. Gracias a esto, será posible usar el formulario en diferentes Presenters y redirigir a un lugar diferente en cada uno.

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('El registro ha sido guardado');
			$this->redirect('Homepage:');
		};
		return $form;
	}
}

Esta solución aprovecha la propiedad de los formularios de que cuando se llama a addError() en el formulario o en uno de sus elementos, el siguiente manejador onSuccess ya no se llama.

Herencia de la clase Form

Un formulario construido no debe ser un descendiente del formulario. En otras palabras, no uses esta solución:

// ⛔ ¡ASÍ NO! LA HERENCIA NO PERTENECE AQUÍ
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$this->addText('title', 'Título:');
		// aquí se añaden otros campos del formulario
		$this->addSubmit('send', 'Enviar');
		$this->setTranslator($translator);
	}
}

En lugar de construir el formulario en el constructor, usa una fábrica.

Es necesario darse cuenta de que la clase Form es, ante todo, una herramienta para construir un formulario, es decir, un form builder. Y el formulario construido puede entenderse como su producto. Pero el producto no es un caso específico del constructor, no hay una relación es un entre ellos que forme la base de la herencia.

Componente con formulario

Un enfoque completamente diferente es la creación de un componente que incluya un formulario. Esto ofrece nuevas posibilidades, por ejemplo, renderizar el formulario de una manera específica, ya que el componente también incluye una plantilla. O se pueden usar señales para la comunicación AJAX y la carga de 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', 'Título:');
		// aquí se añaden otros campos del formulario
		$form->addSubmit('send', 'Enviar');
		$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;
		}

		// disparar el evento
		$this->onSave($this, $data);
	}
}

También crearemos una fábrica que producirá este componente. Basta con escribir su interfaz:

interface EditControlFactory
{
	function create(): EditControl;
}

Y añadirla al archivo de configuración:

services:
	- EditControlFactory

Y ahora podemos solicitar la fábrica y usarla en el 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 redirigimos al resultado de la edición, p.ej.:
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}