Formulários em Presenters
Nette Forms facilitam enormemente a criação e processamento de formulários web. Neste capítulo, você aprenderá a usar formulários dentro de presenters.
Se você está interessado em como usá-los de forma totalmente independente do resto do framework, o guia para uso independente é para você.
Primeiro formulário
Vamos tentar escrever um formulário de registro simples. Seu código será o seguinte:
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Nome:');
$form->addPassword('password', 'Senha:');
$form->addSubmit('send', 'Registrar');
$form->onSuccess[] = [$this, 'formSucceeded'];
e no navegador será exibido assim:

O formulário no presenter é um objeto da classe Nette\Application\UI\Form
, seu predecessor
Nette\Forms\Form
é destinado ao uso independente. Adicionamos a ele os chamados elementos nome, senha e botão de
envio. E, finalmente, a linha com $form->onSuccess
diz que após o envio e validação bem-sucedida, o método
$this->formSucceeded()
deve ser chamado.
Do ponto de vista do presenter, o formulário é um componente comum. Portanto, ele é tratado como um componente e incorporado ao presenter usando métodos de fábrica. Ficará assim:
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addPassword('password', 'Senha:');
$form->addSubmit('send', 'Registrar');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// aqui processamos os dados enviados pelo formulário
// $data->name contém o nome
// $data->password contém a senha
$this->flashMessage('Você foi registrado com sucesso.');
$this->redirect('Home:');
}
}
E no template, renderizamos o formulário com a tag {control}
:
<h1>Registro</h1>
{control registrationForm}
E isso é basicamente tudo :-) Temos um formulário funcional e perfeitamente seguro.
E agora você provavelmente está pensando que foi muito rápido, imaginando como é possível que o método
formSucceeded()
seja chamado e quais são os parâmetros que ele recebe. Certamente, você está certo, isso merece
uma explicação.
Nette introduz um mecanismo inovador que chamamos de estilo Hollywood. Em vez de você, como desenvolvedor, ter que perguntar constantemente se algo aconteceu (“o formulário foi enviado?”, “foi enviado validamente?” e “não foi falsificado?”), você diz ao framework “quando o formulário estiver validamente preenchido, chame este método” e deixa o trabalho restante para ele. Se você programa em JavaScript, conhece bem este estilo de programação. Você escreve funções que são chamadas quando um determinado evento ocorre. E a linguagem passa os argumentos apropriados para elas.
É exatamente assim que o código do presenter acima é construído. O array $form->onSuccess
representa uma
lista de callbacks PHP que o Nette chama no momento em que o formulário é enviado e preenchido corretamente (ou seja, é
válido). Dentro do ciclo de vida do
presenter, isso é chamado de sinal, eles são chamados após o método action*
e antes do método
render*
. E para cada callback, ele passa como primeiro parâmetro o próprio formulário e como segundo os dados
enviados na forma de um objeto ArrayHash por padrão (ou uma
classe/array mapeado). Você pode omitir o primeiro parâmetro se não precisar do objeto do formulário. E o segundo parâmetro
pode ser mais inteligente, mas falaremos sobre isso mais tarde.
O objeto $data
contém as chaves name
e password
com os dados que o usuário
preencheu. Geralmente, enviamos os dados diretamente para processamento adicional, que pode ser, por exemplo, inserção no banco
de dados. Durante o processamento, no entanto, pode ocorrer um erro, por exemplo, o nome de usuário já está em uso. Nesse
caso, passamos o erro de volta para o formulário usando addError()
e o deixamos renderizar novamente, com a
mensagem de erro.
$form->addError('Desculpe, o nome de usuário já está em uso.');
Além de onSuccess
, existe também onSubmit
: os callbacks são chamados sempre após o envio do
formulário, mesmo que não esteja preenchido corretamente. E também onError
: os callbacks são chamados apenas se
o envio não for válido. Eles são chamados mesmo se invalidarmos o formulário em onSuccess
ou
onSubmit
usando addError()
.
Após processar o formulário, redirecionamos para a próxima página. Isso evita o reenvio indesejado do formulário pelo botão atualizar, voltar ou movimento no histórico do navegador.
Tente adicionar outros elementos de formulário.
Acesso aos elementos
O formulário é um componente do presenter, em nosso caso chamado registrationForm
(pelo nome do método de
fábrica createComponentRegistrationForm
), então em qualquer lugar no presenter você pode acessar o formulário
usando:
$form = $this->getComponent('registrationForm');
// sintaxe alternativa: $form = $this['registrationForm'];
Os elementos individuais do formulário também são componentes, então você pode acessá-los da mesma maneira:
$input = $form->getComponent('name'); // ou $input = $form['name'];
$button = $form->getComponent('send'); // ou $button = $form['send'];
Os elementos são removidos usando unset:
unset($form['name']);
Regras de validação
A palavra válido foi mencionada, mas o formulário ainda não tem regras de validação. Vamos corrigir isso.
O nome será obrigatório, então o marcamos com o método setRequired()
, cujo argumento é o texto da
mensagem de erro que será exibida se o usuário não preencher o nome. Se o argumento não for fornecido, a mensagem de erro
padrão será usada.
$form->addText('name', 'Nome:')
->setRequired('Por favor, insira o nome');
Tente enviar o formulário sem preencher o nome e você verá que uma mensagem de erro será exibida e o navegador ou servidor o rejeitará até que você preencha o campo.
Ao mesmo tempo, você não pode enganar o sistema escrevendo apenas espaços no campo. De jeito nenhum. O Nette remove automaticamente os espaços à esquerda e à direita. Experimente. É algo que você deve fazer com cada entrada de linha única, mas muitas vezes é esquecido. O Nette faz isso automaticamente. (Você pode tentar enganar o formulário e enviar uma string multilinha como nome. Mesmo aqui, o Nette não se deixa enganar e transforma as quebras de linha em espaços.)
O formulário é sempre validado no lado do servidor, mas também é gerada uma validação JavaScript, que ocorre
instantaneamente e o usuário é informado sobre o erro imediatamente, sem a necessidade de enviar o formulário ao servidor.
Isso é feito pelo script netteForms.js
. Insira-o no template de layout:
<script src="https://unpkg.com/nette-forms@3"></script>
Se você olhar o código-fonte da página com o formulário, poderá notar que o Nette insere os elementos obrigatórios em
elementos com a classe CSS required
. Tente adicionar a seguinte folha de estilo ao template e o rótulo “Nome”
ficará vermelho. Elegantemente, marcamos os elementos obrigatórios para os usuários:
<style>
.required label { color: maroon }
</style>
Outras regras de validação são adicionadas com o método addRule()
. O primeiro parâmetro é a regra,
o segundo é novamente o texto da mensagem de erro e pode ainda seguir um argumento da regra de validação. O que isso
significa?
Vamos estender o formulário com um novo campo opcional “idade”, que deve ser um número inteiro
(addInteger()
) e, além disso, dentro de um intervalo permitido (Form::Range
). E aqui usaremos
o terceiro parâmetro do método addRule()
, pelo qual passamos o intervalo necessário ao validador como um par
[de, até]
:
$form->addInteger('age', 'Idade:')
->addRule($form::Range, 'A idade deve ser entre 18 e 120', [18, 120]);
Se o usuário não preencher o campo, as regras de validação não serão verificadas, pois o elemento é opcional.
Aqui surge espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são mencionados
duplicadamente, o que não é ideal. Se estivéssemos criando formulários multilíngues e a mensagem contendo números fosse
traduzida para vários idiomas, dificultaria uma possível alteração dos valores. Por esse motivo, é possível usar os
marcadores %d
e o Nette completará os valores:
->addRule($form::Range, 'A idade deve ser entre %d e %d anos', [18, 120]);
Voltemos ao elemento password
, que também tornaremos obrigatório e verificaremos o comprimento mínimo da senha
($form::MinLength
), novamente usando o marcador:
$form->addPassword('password', 'Senha:')
->setRequired('Escolha uma senha')
->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8);
Adicionaremos ao formulário ainda o campo passwordVerify
, onde o usuário digita a senha novamente, para
verificação. Usando regras de validação, verificamos se ambas as senhas são iguais ($form::Equal
). E como
parâmetro, damos uma referência à primeira senha usando colchetes:
$form->addPassword('passwordVerify', 'Senha para verificação:')
->setRequired('Por favor, digite a senha novamente para verificação')
->addRule($form::Equal, 'As senhas não coincidem', $form['password'])
->setOmitted();
Usando setOmitted()
, marcamos o elemento cujo valor realmente não nos importa e que existe apenas para fins de
validação. O valor não é passado para $data
.
Com isso, temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação do Nette são muito mais amplas, é possível criar condições, deixar partes da página serem exibidas e ocultadas com base nelas, etc. Tudo será explicado no capítulo sobre validação de formulários.
Valores padrão
Normalmente, definimos valores padrão para os elementos do formulário:
$form->addEmail('email', 'E-mail')
->setDefaultValue($lastUsedEmail);
Muitas vezes, é útil definir valores padrão para todos os elementos de uma vez. Por exemplo, quando o formulário serve para editar registros. Lemos o registro do banco de dados e definimos os valores padrão:
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Chame setDefaults()
após definir os elementos.
Renderização do formulário
Por padrão, o formulário é renderizado como uma tabela. Os elementos individuais cumprem a regra básica de
acessibilidade – todos os rótulos são escritos como <label>
e vinculados ao elemento de formulário
correspondente. Ao clicar no rótulo, o cursor aparece automaticamente no campo do formulário.
Podemos definir quaisquer atributos HTML para cada elemento. Por exemplo, adicionar um placeholder:
$form->addInteger('age', 'Idade:')
->setHtmlAttribute('placeholder', 'Por favor, preencha a idade');
Existem realmente muitas maneiras de renderizar um formulário, então há um capítulo separado sobre renderização dedicado a isso.
Mapeamento para classes
Voltemos ao método formSucceeded()
, que no segundo parâmetro $data
recebe os dados enviados como um
objeto ArrayHash
. Como é uma classe genérica, algo como stdClass
, sentiremos falta de certo conforto
ao trabalhar com ela, como sugestão de propriedades nos editores ou análise estática de código. Isso poderia ser resolvido
tendo uma classe específica para cada formulário, cujas propriedades representam os elementos individuais. Por exemplo:
class RegistrationFormData
{
public string $name;
public ?int $age;
public string $password;
}
Alternativamente, você pode usar o construtor:
class RegistrationFormData
{
public function __construct(
public string $name,
public ?int $age,
public string $password,
) {
}
}
As propriedades da classe de dados também podem ser enumerações e serão mapeadas automaticamente.
Como dizer ao Nette para nos retornar os dados como objetos desta classe? Mais fácil do que você pensa. Basta apenas indicar
a classe como o tipo do parâmetro $data
no método manipulador:
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name é uma instância de RegistrationFormData
$name = $data->name;
// ...
}
Como tipo, também pode ser indicado array
e então os dados são passados como um array.
Da mesma forma, pode-se usar o método getValues()
, ao qual o nome da classe ou o objeto a ser hidratado é
passado como parâmetro:
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Se os formulários formarem uma estrutura multinível composta por contêineres, crie uma classe separada para cada um:
$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */
class PersonFormData
{
public string $firstName;
public string $lastName;
}
class RegistrationFormData
{
public PersonFormData $person;
public ?int $age;
public string $password;
}
O mapeamento então, a partir do tipo da propriedade $person
, reconhece que deve mapear o contêiner para a
classe PersonFormData
. Se a propriedade contivesse um array de contêineres, indique o tipo array
e
passe a classe para mapeamento diretamente para o contêiner:
$person->setMappedType(PersonFormData::class);
Você pode ter o design da classe de dados do formulário gerado usando o método
Nette\Forms\Blueprint::dataClass($form)
, que o imprime na página do navegador. O código então só precisa ser
clicado, marcado e copiado para o projeto.
Vários botões
Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Podemos criar nossa
própria função manipuladora para cada botão. Definimo-la como um handler para o evento onClick
:
$form->addSubmit('save', 'Salvar')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Excluir')
->onClick[] = [$this, 'deleteButtonPressed'];
Esses handlers são chamados apenas no caso de um formulário validamente preenchido, assim como no caso do evento
onSuccess
. A diferença é que, como primeiro parâmetro, em vez do formulário, pode ser passado o botão de
envio, dependendo do tipo que você indicar:
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Quando o formulário é enviado com a tecla Enter, considera-se como se tivesse sido enviado pelo primeiro botão.
Evento onAnchor
Quando, no método de fábrica (como createComponentRegistrationForm
), montamos o formulário, ele ainda não
sabe se foi enviado, nem com quais dados. Mas há casos em que precisamos conhecer os valores enviados, talvez a forma adicional
do formulário dependa deles, ou precisemos deles para selectboxes dependentes, etc.
Portanto, a parte do código que monta o formulário pode ser deixada para ser chamada apenas no momento em que ele está, por
assim dizer, ancorado, ou seja, já está conectado ao presenter e conhece seus dados enviados. Tal código é passado para
o array $onAnchor
:
$country = $form->addSelect('country', 'País:', $this->model->getCountries());
$city = $form->addSelect('city', 'Cidade:');
$form->onAnchor[] = function () use ($country, $city) {
// esta função será chamada quando o formulário souber se foi enviado e com quais dados
// portanto, é possível usar o método getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Proteção contra vulnerabilidades
O Nette Framework dá grande ênfase à segurança e, portanto, cuida meticulosamente da boa segurança dos formulários. Faz isso de forma totalmente transparente e não requer nenhuma configuração manual.
Além de proteger os formulários contra ataques de Cross Site Scripting (XSS) e Cross-Site Request Forgery (CSRF), ele realiza muitas pequenas proteções nas quais você não precisa mais pensar.
Por exemplo, ele filtra todos os caracteres de controle das entradas e verifica a validade da codificação UTF-8, para que os dados do formulário estejam sempre limpos. Para caixas de seleção e listas de rádio, ele verifica se os itens selecionados estavam realmente entre os oferecidos e não foram falsificados. Já mencionamos que, para entradas de texto de linha única, ele remove os caracteres de fim de linha que um invasor poderia ter enviado. Para entradas multilinha, ele normaliza os caracteres de fim de linha. E assim por diante.
O Nette resolve para você riscos de segurança que muitos programadores nem sabem que existem.
O ataque CSRF mencionado consiste em um invasor atrair a vítima para uma página que, discretamente no navegador da vítima, executa uma requisição ao servidor no qual a vítima está logada, e o servidor acredita que a requisição foi feita pela vítima por vontade própria. Portanto, o Nette impede o envio de formulários POST de outro domínio. Se, por algum motivo, você quiser desativar a proteção e permitir o envio do formulário de outro domínio, use:
$form->allowCrossOrigin(); // ATENÇÃO! Desativa a proteção!
Esta proteção utiliza um cookie SameSite chamado _nss
. A proteção usando o cookie SameSite pode não ser
100% confiável, por isso é aconselhável ativar também a proteção por token:
$form->addProtection();
Recomendamos proteger assim os formulários na parte administrativa do site, que alteram dados sensíveis na aplicação.
O framework se defende contra o ataque CSRF gerando e verificando um token de autorização, que é armazenado na sessão.
Portanto, é necessário ter a sessão aberta antes de exibir o formulário. Na parte administrativa do site, a sessão
geralmente já está iniciada devido ao login do usuário. Caso contrário, inicie a sessão com o método
Nette\Http\Session::start()
.
Mesmo formulário em vários presenters
Se você precisar usar o mesmo formulário em vários presenters, recomendamos criar uma fábrica para ele, que você então
passará para o presenter. Um local adequado para tal classe é, por exemplo, o diretório app/Forms
.
A classe de fábrica pode parecer assim:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addSubmit('send', 'Entrar');
return $form;
}
}
Pedimos à classe para produzir o formulário no método de fábrica para componentes no presenter:
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// podemos modificar o formulário, aqui por exemplo mudamos o rótulo no botão
$form['send']->setCaption('Continuar');
$form->onSuccess[] = [$this, 'signInFormSuceeded']; // e adicionamos o handler
return $form;
}
O handler para processamento do formulário também pode ser fornecido pela fábrica:
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Nome:');
$form->addSubmit('send', 'Entrar');
$form->onSuccess[] = function (Form $form, $data): void {
// aqui realizamos o processamento do formulário
};
return $form;
}
}
Então, tivemos uma introdução rápida aos formulários no Nette. Tente dar uma olhada no diretório examples na distribuição, onde encontrará mais inspiração.