Reutilização de formulários em vários lugares

Em Nette, você tem várias opções para reutilizar a mesma forma em vários lugares sem duplicar o código. Neste artigo, analisaremos as diferentes soluções, inclusive as que você deve evitar.

Fábrica de formulários

Uma abordagem básica para usar o mesmo componente em vários lugares é criar um método ou classe que gere o componente, e depois chamar esse método em lugares diferentes na aplicação. Tal método ou classe é chamado de fábrica. Por favor, não confunda com o padrão de projeto método de fábrica, que descreve uma forma específica de usar fábricas e não está relacionado a este tópico.

Como exemplo, vamos criar uma fábrica que irá construir um formulário de edição:

use Nette\Application\UI\Form;

class FormFactory
{
	public function createEditForm(): Form
	{
		$form = new Form;
		$form->addText('title', 'Title:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Agora você pode usar esta fábrica em diferentes lugares em sua aplicação, por exemplo, em apresentadores ou componentes. E o fazemos solicitando-o como uma dependência. Portanto, primeiro, escreveremos a classe no arquivo de configuração:

services:
	- FormFactory

E depois a usamos no apresentador:

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

	protected function createComponentEditForm(): Form
	{
		$form = $this->formFactory->createEditForm();
		$form->onSuccess[] = function () {
			// processamento dos dados enviados
		};
		return $form;
	}
}

Você pode ampliar a fábrica de formulários com métodos adicionais para criar outros tipos de formulários que se adaptem à sua aplicação. E, é claro, você pode adicionar um método que cria um formulário básico sem elementos, que os outros métodos utilizarão:

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

	public function createEditForm(): Form
	{
		$form = $this->createForm();
		$form->addText('title', 'Title:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

O método createForm() ainda não faz nada de útil, mas isso vai mudar rapidamente.

Dependências de Fábrica

Com o tempo, tornar-se-á evidente que precisamos de formulários para sermos multilíngues. Isto significa que precisamos criar um tradutor para todos os formulários. Para fazer isso, modificamos a classe FormFactory para aceitar o objeto Translator como uma dependência no construtor, e o passamos para o formulário:

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 o método createForm() também é chamado por outros métodos que criam formas específicas, só precisamos colocar o tradutor nesse método. E estamos terminados. Não há necessidade de alterar nenhum apresentador ou código de componente, o que é ótimo.

Mais Classes de Fábrica

Alternativamente, você pode criar múltiplas classes para cada formulário que deseja utilizar em sua aplicação. Esta abordagem pode aumentar a legibilidade do código e tornar os formulários mais fáceis de gerenciar. Deixe o original FormFactory para criar apenas um formulário puro com configuração básica (por exemplo, com suporte a tradução) e crie uma nova fábrica EditFormFactory para o formulário de edição.

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

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


// ✅ uso da composição
class EditFormFactory
{
	public function __construct(
		private FormFactory $formFactory,
	) {
	}

	public function create(): Form
	{
		$form = $this->formFactory->create();
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

É muito importante que a vinculação entre as classes FormFactory e EditFormFactory seja implementada por composição, e não por herança de objetos:

// ⛔ NÃO! A HERANÇA NÃO PERTENCE AQUI
class EditFormFactory extends FormFactory
{
	public function create(): Form
	{
		$form = parent::create();
		$form->addText('title', 'Title:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		return $form;
	}
}

Usar a herança neste caso seria completamente contraproducente. Você se depararia com problemas muito rapidamente. Por exemplo, se você quisesse adicionar parâmetros ao método create(); o PHP relataria um erro de que sua assinatura era diferente da dos pais. Ou ao passar uma dependência para a classe EditFormFactory através do construtor. Isto causaria o que chamamos de inferno do construtor.

Em geral, é melhor preferir a composição à herança.

Manuseio de formulários

O manipulador de formulários que é chamado após uma apresentação bem-sucedida também pode fazer parte de uma classe de fábrica. Ele funcionará passando os dados enviados para o modelo para processamento. Ele passará quaisquer erros de volta para o formulário. O modelo no exemplo a seguir é representado pela classe Facade:

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

	public function create(): Form
	{
		$form = $this->formFactory->create();
		$form->addText('title', 'Title:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];
		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// processamento dos dados apresentados
			$this->facade->process($data);

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

Deixe o apresentador cuidar do redirecionamento em si. Ele adicionará outro manipulador ao evento onSuccess, que realizará o redirecionamento. Isto permitirá que o formulário seja utilizado em diferentes apresentadores, e cada um poderá ser redirecionado para um local 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 solução aproveita a propriedade dos formulários que, quando addError() é chamado em um formulário ou em seu elemento, o próximo manipulador onSuccess não é invocado.

Herdando da classe do formulário

Uma forma construída não deve ser uma criança de uma forma. Em outras palavras, não use esta solução:

// ⛔ NÃO! A HERANÇA NÃO PERTENCE AQUI
class EditForm extends Form
{
	public function __construct(Translator $translator)
	{
		parent::__construct();
		$form->addText('title', 'Title:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		$form->setTranslator($translator);
	}
}

Em vez de construir a forma na construtora, use a fábrica.

É importante perceber que a classe Form é principalmente uma ferramenta para montagem de um formulário, ou seja, um construtor de formulários. E a forma montada pode ser considerada seu produto. Entretanto, o produto não é um caso específico do construtor; não há é uma relação entre eles, o que forma a base da herança.

Componente do formulário

Uma abordagem completamente diferente é criar um componente que inclua uma forma. Isto dá novas possibilidades, por exemplo, de tornar o formulário de uma forma específica, uma vez que o componente inclui um modelo. Ou sinais podem ser usados para comunicação AJAX e carregamento de informações no formulário, por exemplo, para insinuação, 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:');
		// campos adicionais do formulário são adicionados aqui
		$form->addSubmit('send', 'Save');
		$form->onSuccess[] = [$this, 'processForm'];

		return $form;
	}

	public function processForm(Form $form, array $data): void
	{
		try {
			// processamento dos dados apresentados
			$this->facade->process($data);

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

		// invocação de eventos
		$this->onSave($this, $data);
	}
}

Vamos criar uma fábrica que produzirá este componente. É o suficiente para escrever sua interface:

interface EditControlFactory
{
	function create(): EditControl;
}

E adicione-a ao arquivo de configuração:

services:
	- EditControlFactory

E agora podemos solicitar a fábrica e utilizá-la no apresentador:

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');
			// ou redirecionar para o resultado da edição, por exemplo
			// $this->redirect('detail', ['id' => $data->id]);
		};

		return $control;
	}
}