Reutilização de formulários em vários lugares
No Nette, você tem várias opções para usar o mesmo formulário em vários lugares sem duplicar o código. Neste artigo, mostraremos diferentes soluções, incluindo aquelas que você deve evitar.
Fábrica de formulários
Uma das abordagens básicas para usar o mesmo componente em vários lugares é criar um método ou classe que gera esse componente e, em seguida, chamar esse método em diferentes lugares da aplicação. Tal método ou classe é chamado de fábrica. Por favor, não confunda com o padrão de projeto factory method, que descreve uma forma específica de usar fábricas e não está relacionado a este tópico.
Como exemplo, criaremos uma fábrica que construirá um formulário de edição:
use Nette\Application\UI\Form;
class FormFactory
{
public function createEditForm(): Form
{
$form = new Form;
$form->addText('title', 'Título:');
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
return $form;
}
}
Agora você pode usar esta fábrica em diferentes lugares da sua aplicação, por exemplo, em presenters ou componentes. E isso é feito solicitando-a como dependência. Primeiro, registramos a classe no arquivo de configuração:
services:
- FormFactory
E depois a usamos no 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 () {
// processamento dos dados enviados
};
return $form;
}
}
Você pode estender a fábrica de formulários com outros métodos para criar outros tipos de formulários de acordo com as necessidades da sua aplicação. E, claro, também podemos adicionar um método que cria um formulário básico sem elementos, e os outros métodos o utilizarão:
class FormFactory
{
public function createForm(): Form
{
$form = new Form;
return $form;
}
public function createEditForm(): Form
{
$form = $this->createForm();
$form->addText('title', 'Título:');
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
return $form;
}
}
O método createForm()
ainda não faz nada útil, mas isso mudará rapidamente.
Dependências da fábrica
Com o tempo, percebe-se que precisamos que os formulários sejam multilíngues. Isso significa que precisamos definir
o chamado tradutor para todos os formulários. Para isso,
modificaremos a classe FormFactory
para que ela aceite o objeto Translator
como dependência no
construtor e o passe 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 formulários específicos, basta
definir o tradutor apenas nele. E está feito. Não é necessário alterar o código de nenhum presenter ou componente, o que
é ótimo.
Múltiplas classes de fábrica
Alternativamente, você pode criar várias classes para cada formulário que deseja usar em sua aplicação. Essa abordagem
pode aumentar a legibilidade do código e facilitar o gerenciamento dos formulários. Deixaremos a FormFactory
original criar apenas um formulário limpo com configuração básica (por exemplo, com suporte a traduções) e criaremos 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 de composição
class EditFormFactory
{
public function __construct(
private FormFactory $formFactory,
) {
}
public function create(): Form
{
$form = $this->formFactory->create();
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
return $form;
}
}
É muito importante que a relação entre as classes FormFactory
e EditFormFactory
seja realizada por
composição, e não por herança de objetos:
// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI
class EditFormFactory extends FormFactory
{
public function create(): Form
{
$form = parent::create();
$form->addText('title', 'Título:');
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
return $form;
}
}
O uso de herança seria completamente contraproducente neste caso. Você encontraria problemas muito rapidamente. Por exemplo,
no momento em que quisesse adicionar parâmetros ao método create()
; o PHP relataria um erro de que sua assinatura
difere da do pai. Ou ao passar dependências para a classe EditFormFactory
através do construtor. Ocorreria uma
situação que chamamos de constructor hell.
Em geral, é melhor preferir composição em vez de herança.
Manipulação do formulário
O manipulador do formulário, que é chamado após o envio bem-sucedido, também pode fazer parte da classe de fábrica. Ele
funcionará passando os dados enviados para o modelo para processamento. Eventuais erros são passados 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', 'Título:');
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// processamento dos dados enviados
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
}
}
}
No entanto, deixaremos o redirecionamento em si para o presenter. Ele adicionará outro manipulador ao evento
onSuccess
, que realizará o redirecionamento. Graças a isso, será possível usar o formulário em diferentes
presenters e redirecionar para um local diferente em cada um deles.
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('O registro foi salvo');
$this->redirect('Homepage:');
};
return $form;
}
}
Esta solução utiliza a propriedade dos formulários de que, quando addError()
é chamado no formulário ou em
seu elemento, o próximo manipulador onSuccess
não é chamado.
Herança da classe Form
Um formulário construído não deve ser um descendente da classe Form
. Em outras palavras, não use esta
solução:
// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI
class EditForm extends Form
{
public function __construct(Translator $translator)
{
parent::__construct();
$this->addText('title', 'Título:');
// aqui são adicionados outros campos do formulário
$this->addSubmit('send', 'Enviar');
$this->setTranslator($translator);
}
}
Em vez de construir o formulário no construtor, use uma fábrica.
É preciso perceber que a classe Form
é, antes de tudo, uma ferramenta para construir um formulário, ou seja, um
form builder. E o formulário construído pode ser entendido como seu produto. Mas o produto não é um caso
específico do builder, não há entre eles uma relação is a que forma a base da herança.
Componente com formulário
Uma abordagem completamente diferente é a criação de componentes que incluem um formulário. Isso oferece novas possibilidades, como renderizar o formulário de uma maneira específica, já que o componente também inclui um template. Ou é possível usar sinais para comunicação AJAX e carregamento de informações no formulário, por exemplo, para sugestões, 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:');
// aqui são adicionados outros campos do formulário
$form->addSubmit('send', 'Enviar');
$form->onSuccess[] = [$this, 'processForm'];
return $form;
}
public function processForm(Form $form, array $data): void
{
try {
// processamento dos dados enviados
$this->facade->process($data);
} catch (AnyModelException $e) {
$form->addError('...');
return;
}
// dispara o evento
$this->onSave($this, $data);
}
}
Criaremos também uma fábrica que produzirá este componente. Basta registrar sua interface:
interface EditControlFactory
{
function create(): EditControl;
}
E adicionar ao arquivo de configuração:
services:
- EditControlFactory
E agora podemos solicitar a fábrica e usá-la no 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');
// ou redirecionamos para o resultado da edição, por exemplo:
// $this->redirect('detail', ['id' => $data->id]);
};
return $control;
}
}