Reutilización de formularios en múltiples lugares
Nette ofrece varias formas de reutilizar el mismo formulario en varios sitios sin duplicar código. En este artículo veremos distintas soluciones, incluidas aquellas que debería evitar.
Factory de formularios
Un enfoque básico para reutilizar un componente en varios lugares es crear un método o una clase que genere ese componente. Ese método se llama después desde distintos puntos de la aplicación. A un método o clase así se le llama factory. No lo confunda con el patrón de diseño factory method, que describe una forma concreta de usar las factories y no está directamente relacionado con este tema.
Como ejemplo, creemos una factory que construya 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 más campos del formulario
$form->addSubmit('send', 'Save');
return $form;
}
}
Ahora puede usar esta factory en distintas partes de su aplicación, por ejemplo en presenters o componentes. Lo hace pidiéndola como dependencia. Primero registre la clase en el archivo de configuración:
services:
- FormFactory
Y después úsela en el 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;
}
}
Puede ampliar la factory de formularios con más métodos para crear otros tipos de formularios, según lo que necesite su aplicación. Y, naturalmente, podemos añadir un método que cree un formulario básico sin elementos, que los demás métodos podrán aprovechar:
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 más campos del formulario
$form->addSubmit('send', 'Save');
return $form;
}
}
El método createForm() todavía no hace nada demasiado útil, pero eso cambiará enseguida.
Dependencias de la factory
Con el tiempo puede surgir la necesidad de que los formularios sean multilingües. Eso significa establecer un traductor para todos los formularios. Para conseguirlo,
modifique la clase FormFactory de modo que acepte el objeto Translator como dependencia en el
constructor y se lo pase al formulario creado:
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() lo llaman también los demás métodos que crean formularios concretos, basta con
establecer aquí el traductor. Y listo. No hace falta modificar el código de ningún presenter ni componente, lo cual es
estupendo.
Más clases factory
Otra opción es crear una clase factory separada para cada formulario que quiera usar en su aplicación. Este enfoque puede
mejorar la legibilidad del código y simplificar la gestión de los formularios. Deje que la FormFactory original
cree solo un formulario básico con la configuración fundamental (como el soporte de traducción) y cree una nueva factory,
EditFormFactory, específicamente 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;
}
}
// ✅ usando composición
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// aquí se añaden más campos del formulario
$form->addSubmit('send', 'Save');
return $form;
}
}
Es crucial que la relación entre las clases FormFactory y EditFormFactory se realice mediante composición, no mediante herencia de objetos:
// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Title:');
// aquí se añaden más campos del formulario
$form->addSubmit('send', 'Save');
return $form;
}
}
Usar aquí la herencia sería del todo contraproducente. Se toparía con problemas muy pronto. Por ejemplo, si quisiera añadir
parámetros al método create(), PHP daría un error porque su firma diferiría de la del antecesor. O al pasar
dependencias a la clase EditFormFactory mediante el constructor. Eso llevaría a lo que se conoce como infierno del
constructor.
En general, es mejor preferir la composición a la herencia.
Procesamiento del formulario
El manejador del formulario, que se invoca tras un envío correcto, también puede formar parte de la clase factory. Funciona
pasando los datos enviados a la capa del modelo para su procesamiento. Los posibles errores de procesamiento se devuelven de vuelta al formulario. En el siguiente
ejemplo, el modelo 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 más campos del formulario
$form->addSubmit('send', 'Save');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private function processForm(Form $form, array $data): void
{
try {
// procesamiento de los datos enviados
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
De la redirección, en cambio, deje que se ocupe el propio presenter. Añade al evento onSuccess otro manejador
que realiza la redirección. Gracias a eso, el formulario se puede usar en distintos presenters, y cada uno redirigirá a un lugar
distinto tras el éxito.
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('Record was saved');
$this->redirect('Homepage:');
};
return $form;
}
}
Esta solución aprovecha la característica de los formularios según la cual, si se llama a addError() sobre el
formulario o sobre alguno de sus elementos, los siguientes manejadores onSuccess ya no se invocan.
Heredar de la clase Form
Un formulario ya montado no debería ser descendiente de la clase Form. Dicho de otro modo, evite este
enfoque:
// ⛔ ¡NO! LA HERENCIA NO PINTA NADA AQUÍ
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Title:');
// aquí se añaden más campos del formulario
$this->addSubmit('send', 'Save');
$this->setTranslator($translator);
}
}
En lugar de montar el formulario en el constructor, use una factory.
Es importante darse cuenta de que la clase Form es ante todo una herramienta para construir formularios, es decir,
un form builder. El formulario montado puede considerarse su producto. Pero un producto no es un tipo concreto de
constructor; entre ellos no existe la relación es un, que es el fundamento de la herencia.
Componente de formulario
Un enfoque completamente distinto consiste en crear un componente que encapsule el formulario. Eso abre nuevas posibilidades, como renderizar el formulario de una manera concreta, ya que el componente incluye su propia plantilla. O se pueden usar señales para la comunicación AJAX y cargar dinámicamente 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 más campos del formulario
$form->addSubmit('send', 'Save');
$form->onSuccess[] = $this->processForm(...);
return $form;
}
private 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 del evento
$this->onSave($this, $data);
}
}
A continuación, creemos una factory que produzca este componente. Basta con definir su interfaz:
interface EditControlFactory
{
function create(): EditControl;
}
Y añadirla al archivo de configuración:
services:
- EditControlFactory
Ahora ya podemos pedir la factory 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 redirigir al resultado de la edición, p. ej.:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}