Formulários em Apresentadores

Os Formulários Nette facilitam drasticamente a criação e o processamento de formulários web. Neste capítulo, você aprenderá como utilizar os formulários dentro dos apresentadores.

Se você estiver interessado em utilizá-los completamente autônomos sem o restante da estrutura, existe um guia para formulários autônomos.

Primeiro Formulário

Tentaremos escrever um simples formulário de registro. Seu código terá este aspecto:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];

e no navegador o resultado deve ser parecido com este:

A forma no apresentador é um objeto da classe Nette\Application\UI\Form, seu predecessor Nette\Forms\Form é destinado ao uso autônomo. Acrescentamos os campos nome, senha e botão de envio. Finalmente, a linha com $form->onSuccess diz que após a submissão e validação bem sucedida, o método $this->formSucceeded() deve ser chamado.

Do ponto de vista do apresentador, a forma é um componente comum. Portanto, ele é tratado como um componente e incorporado ao apresentador usando o método de fábrica. Será parecido com isto:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// aqui processaremos os dados enviados pelo formulário
		// $data->name contém nome
		// $data->password contém a senha
		$this->flashMessage('Você se inscreveu com sucesso');
		$this->redirect('Home:');
	}
}

E a renderização em modelo é feita usando a tag {control}:

<h1>Registration</h1>

{control registrationForm}

E isso é tudo :-) Temos uma forma funcional e perfeitamente segura.

Agora você provavelmente está pensando que foi muito rápido, perguntando-se como é possível que o método formSucceeded() seja chamado e quais são os parâmetros que ele obtém. Claro, você está certo, isto merece uma explicação.

A Nette vem com um mecanismo legal, que chamamos de estilo hollywoodiano. Em vez de ter que perguntar constantemente se algo aconteceu (“o formulário foi submetido?”, “foi submetido validamente?” ou “não foi forjado?”), você diz à estrutura “quando o formulário for validamente preenchido, chame este método” e deixe mais trabalho sobre ele. Se você programar em JavaScript, você está familiarizado com 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.

É assim que o código do apresentador acima é construído. O Array $form->onSuccess representa a lista de ligações de retorno do PHP que a Nette chamará quando o formulário for enviado e preenchido corretamente. Dentro do ciclo de vida do apresentador é um chamado sinal, por isso são chamados depois do método action* e antes do método render*. E passa para cada chamada de retorno o próprio formulário no primeiro parâmetro e os dados enviados como objeto ArrayHash no segundo. Você pode omitir o primeiro parâmetro se não precisar do objeto formulário. O segundo parâmetro pode ser ainda mais útil, mas sobre isso mais tarde.

O objeto $data contém as propriedades name e password com os dados inseridos pelo usuário. Normalmente, enviamos os dados diretamente para processamento posterior, que podem ser, por exemplo, inseridos no banco de dados. Entretanto, pode ocorrer um erro durante o processamento, por exemplo, o nome de usuário já está tomado. Neste caso, passamos o erro de volta ao formulário usando addError() e deixamos que ele seja redesenhado, com uma mensagem de erro:

$form->addError('Sorry, username is already in use.');

Além de onSuccess, há também onSubmit: as chamadas de retorno são sempre feitas após o envio do formulário, mesmo que este não seja preenchido corretamente. E finalmente onError: as chamadas de retorno são chamadas somente se a submissão não for válida. Elas são mesmo chamadas se invalidarmos o formulário em onSuccess ou onSubmit, usando addError().

Após o processamento do formulário, redirecionaremos para a página seguinte. Isto evita que o formulário seja reapresentado involuntariamente clicando no botão refresh, back, ou movendo o histórico do navegador.

Tente adicionar mais controles de formulário.

Acesso aos controles

O formulário é um componente do apresentador, em nosso caso chamado registrationForm (após o nome do método de fábrica createComponentRegistrationForm), portanto, em qualquer lugar do apresentador você pode chegar ao formulário usando o mesmo:

$form = $this->getComponent('registrationForm');
// sintaxe alternativa: $form = $this['registrationForm'];

Também os controles de formulários individuais são componentes, para que você possa acessá-los da mesma forma:

$input = $form->getComponent('name'); // ou $input = $form['name'];
$button = $form->getComponent('send'); // ou $button = $form['send'];

Os controles são removidos com o uso de controles não ajustados:

unset($form['name']);

Regras de validação

A palavra valido foi usada várias vezes, mas o formulário ainda não tem regras de validação. Vamos corrigi-lo.

O nome será obrigatório, portanto o marcaremos com o método setRequired(), cujo argumento é o texto da mensagem de erro que será exibida se o usuário não a preencher. Se não for apresentado nenhum argumento, será utilizada a mensagem de erro padrão.

$form->addText('name', 'Name:')
	->setRequired('Please fill your name.');

Tente enviar o formulário sem o nome preenchido e você verá que uma mensagem de erro é exibida e o navegador ou servidor irá rejeitá-lo até que você o preencha.

Ao mesmo tempo, você não poderá enganar o sistema digitando apenas espaços na entrada, por exemplo. De jeito nenhum. A Nette apara automaticamente os espaços em branco à esquerda e à direita. Experimente. É algo que você deve sempre fazer com cada entrada de linha, mas muitas vezes é esquecido. A Nette o faz automaticamente. (Você pode tentar enganar os formulários e enviar uma cadeia de várias linhas como o nome. Mesmo aqui, Nette não será enganado e as quebras de linha mudarão para espaços).

O formulário é sempre validado no lado do servidor, mas a validação JavaScript também é gerada, o que é rápido e o usuário sabe do erro imediatamente, sem ter que enviar o formulário para o servidor. Isto é tratado pelo script netteForms.js. Insira-o no modelo de layout:

<script src="https://unpkg.com/nette-forms@3"></script>

Se você olhar no código fonte da página com formulário, você pode notar que Nette insere os campos necessários em elementos com uma classe CSS required. Tente adicionar o seguinte estilo ao modelo, e a etiqueta “Name” (Nome) será vermelha. Elegantemente, marcamos os campos requeridos para os usuários:

<style>
.required label { color: maroon }
</style>

Regras de validação adicionais serão adicionadas pelo método addRule(). O primeiro parâmetro é a regra, o segundo é novamente o texto da mensagem de erro e o argumento da regra de validação opcional pode seguir. O que isso significa?

O formulário terá outra entrada opcional idade com a condição, que deve ser um número (addInteger()) e em certos limites ($form::Range). E aqui usaremos o terceiro argumento de addRule(), a própria faixa:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);

Se o usuário não preencher o campo, as regras de validação não serão verificadas, pois o campo é opcional.

Obviamente, há espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são listados em duplicata, o que não é o ideal. Se estivéssemos criando um formulário multilíngüe e a mensagem contendo números tivesse que ser traduzida para vários idiomas, seria mais difícil mudar os valores. Por este motivo, podem ser utilizados caracteres substitutos %d:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Vamos voltar ao campo password, torná-lo requerido, e verificar o comprimento mínimo da senha ($form::MinLength), novamente usando os caracteres substitutos na mensagem:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);

Adicionaremos um campo passwordVerify ao formulário, onde o usuário digita a senha novamente, para verificação. Usando regras de validação, verificamos se as duas senhas são as mesmas ($form::Equal). E como argumento, fazemos uma referência à primeira senha usando colchetes:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();

Usando setOmitted(), marcamos um elemento cujo valor realmente não nos interessa e que existe apenas para validação. Seu valor não é passado para $data.

Temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação da Nette são muito mais amplas, você pode criar condições, exibir e ocultar partes de uma página de acordo com elas, etc. Você pode descobrir tudo no capítulo sobre validação de formulários.

Valores por default

Muitas vezes definimos valores padrão para controles de formulários:

$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);

Muitas vezes é útil definir valores padrão para todos os controles ao mesmo tempo. Por exemplo, quando o formulário é usado para editar registros. Nós lemos o registro do banco de dados e o definimos como valores padrão:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Ligue para setDefaults() após definir os controles.

Renderização do formulário

Por padrão, o formulário é apresentado como uma tabela. Os controles individuais seguem as diretrizes básicas de acessibilidade da web. Todas as etiquetas são geradas como <label> e estão associados às suas entradas, clicando na etiqueta move o cursor sobre a entrada.

Podemos definir quaisquer atributos HTML para cada elemento. Por exemplo, acrescente um espaço reservado:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

Há realmente muitas maneiras de renderizar uma forma, por isso é um capítulo dedicado à renderização.

Mapeamento para as classes

Voltemos ao método formSucceeded(), que no segundo parâmetro $data recebe os dados enviados como um objeto ArrayHash. Como esta é uma classe genérica, algo como stdClass, nos faltará alguma conveniência ao trabalhar com ela, como a complementação de código para propriedades em editores ou análise de código estático. Isto poderia ser resolvido tendo uma classe específica para cada formulário, cujas propriedades representam os controles individuais. Por exemplo, a classe

class RegistrationFormData
{
	public string $name;
	public int $age;
	public string $password;
}

Como alternativa, 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 enums e serão mapeadas automaticamente.

Como dizer à Nette para retornar dados como objetos desta classe? Mais fácil do que você pensa. Tudo que você precisa fazer é especificar a classe como o tipo do parâmetro $data no manipulador:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name é instância de RegistrationFormData
	$name = $data->name;
	// ...
}

Você também pode especificar array como o tipo e então ele passará os dados como uma matriz.

De maneira semelhante, você pode usar o método getValues(), que passamos como nome de classe ou objeto para hidratar como parâmetro:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Se as formas consistem em uma estrutura de vários níveis composta de recipientes, crie uma classe separada para cada um deles:

$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 sabe pelo tipo de propriedade $person que deve mapear o container para a classe PersonFormData. Se a propriedade contiver um conjunto de containers, forneça o tipo array e passe a classe a ser mapeada diretamente para o container:

$person->setMappedType(PersonFormData::class);

Você pode gerar uma proposta para a classe de dados de um formulário usando o método Nette\Forms\Blueprint::dataClass($form), que a imprimirá na página do navegador. Em seguida, basta clicar para selecionar e copiar o código em seu projeto.

Botões de submissão múltipla

Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Podemos criar uma função própria para cada botão. Configure-o como um manipulador para o evento onClick:

$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

Estes manipuladores também são chamados apenas no formulário do caso é válido, como no caso do evento onSuccess. A diferença é que o primeiro parâmetro pode ser o objeto do botão submeter em vez do formulário, dependendo do tipo especificado:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Quando um formulário é apresentado com a chave Enter, ele é tratado como se tivesse sido apresentado com o primeiro botão.

Evento na Âncora

Quando você constrói um formulário em um método de fábrica (como createComponentRegistrationForm), ele ainda não sabe se foi submetido ou se os dados com os quais foi submetido. Mas há casos em que precisamos saber os valores apresentados, talvez dependa deles, como será o formulário, ou são usados para caixas de seleção dependentes, etc.

Portanto, você pode ter o código que constrói o formulário chamado quando ele está ancorado, ou seja, já está ligado ao apresentador e conhece seus dados apresentados. Colocaremos tal código na matriz $onAnchor:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// esta função será chamada quando o formulário souber os dados com os quais foi submetido
	// para que você possa usar o método getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Proteção contra vulnerabilidades

Nette Framework coloca um grande esforço para ser seguro e como os formulários são a entrada mais comum dos usuários, os formulários Nette são tão bons quanto impenetráveis. Tudo é mantido de forma dinâmica e transparente, nada tem que ser ajustado manualmente.

Além de proteger os formulários contra ataques direcionados a vulnerabilidades bem conhecidas como o Cross-Site Scripting (XSS) e o Cross-Site Request Forgery (CSRF), ele faz muitas pequenas tarefas de segurança que 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 que os itens selecionados foram realmente dos oferecidos e que não houve falsificação. Já mencionamos que para entrada de texto de uma linha, ele remove caracteres de fim de linha que um atacante poderia enviar para lá. Para entradas de várias linhas, ele normaliza os caracteres de fim de linha. E assim por diante.

Nette corrige as vulnerabilidades de segurança para você que a maioria dos programadores não tem idéia de que existem.

O referido ataque do CSRF é que um agressor atrai a vítima para visitar uma página que silenciosamente executa um pedido no navegador da vítima ao servidor onde a vítima está atualmente logada, e o servidor acredita que o pedido foi feito pela vítima à sua vontade. Portanto, a Nette impede que o formulário seja submetido via POST a partir de outro domínio. Se por algum motivo você quiser desligar a proteção e permitir que o formulário seja submetido a partir de outro domínio, use:

$form->allowCrossOrigin(); // ATENÇÃO! Desligue a proteção!

Esta proteção utiliza um cookie SameSite chamado _nss. A proteção do cookie SameSite pode não ser 100% confiável, por isso é uma boa idéia ativar a proteção simbólica:

$form->addProtection();

É fortemente recomendado aplicar esta proteção aos formulários em uma parte administrativa de seu pedido que altera dados sensíveis. A estrutura protege contra um ataque CSRF gerando e validando o token de autenticação que é armazenado em uma sessão (o argumento é a mensagem de erro mostrada se o token tiver expirado). É por isso que é necessário ter uma sessão iniciada antes de exibir o formulário. Na parte administrativa do site, a sessão normalmente já é iniciada, devido ao login do usuário. Caso contrário, inicie a sessão com o método Nette\Http\Session::start().

Usando um formulário em vários apresentadores

Se você precisar usar um formulário em mais de um apresentador, recomendamos que você crie uma fábrica para isso, que depois você passa para o apresentador. Um local adequado para tal classe é, por exemplo, o diretório app/Forms.

A classe da fábrica pode se parecer com esta:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}

Pedimos à classe que produza o formulário no método de fábrica para componentes no apresentador:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// podemos mudar o formulário, aqui por exemplo, mudamos a etiqueta no botão
	$form['login']->setCaption('Continuar');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // e adicionar manipulador
	return $form;
}

O manipulador de processamento de formulários também pode ser entregue a partir da fábrica:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		$form->onSuccess[] = function (Form $form, $data): void {
			// processamos aqui nosso formulário submetido
		};
		return $form;
	}
}

Portanto, temos uma rápida introdução aos formulários em Nette. Tente procurar no diretório de exemplos na distribuição para obter mais inspiração.

versão: 4.0