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 llama factory. Por favor, no confundir con el patrón de diseño método de fábrica, que describe una forma específica de utilizar fábricas y no está relacionado con este tema.

Como ejemplo, vamos a crear 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', '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 en este caso sería totalmente contraproducente. Se encontraría con problemas muy rápidamente. Por ejemplo, si quisiera agregar parámetros al método create(); PHP reportaría un error de que su firma es diferente a la del padre. O al pasar una dependencia a la clase EditFormFactory a través del constructor. Esto causaría lo que llamamos el infierno del constructor.

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

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 creado no debe ser hijo de un formulario. En otras palabras, no utilice esta solución:

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

En lugar de construir el formulario en el constructor, utilice la fábrica.

Es importante darse cuenta de que la clase Form es principalmente una herramienta para ensamblar un formulario, es decir, un constructor de formularios. Y el formulario ensamblado puede considerarse su producto. Sin embargo, el producto no es un caso específico del constructor; no existe una relación es a entre ellos, que constituye la base de la 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(): EditControl
	{
		$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;
	}
}