Validação de formulários

Controlos obrigatórios

Marcamos os controlos obrigatórios com o método setRequired(), cujo argumento é o texto da mensagem de erro, que será exibida se o utilizador não preencher o controlo. 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');

Regras

Adicionamos regras de validação aos controlos usando o método addRule(). O primeiro parâmetro é a regra, o segundo é o texto da mensagem de erro e o terceiro é o argumento da regra de validação.

$form->addPassword('password', 'Senha:')
	->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8);

As regras de validação são verificadas apenas se o utilizador preencher o controlo.

Nette vem com uma série de regras predefinidas, cujos nomes são constantes da classe Nette\Forms\Form. Podemos usar estas regras para todos os controlos:

constante descrição tipo de argumento
Required controlo obrigatório, alias para setRequired()
Filled controlo obrigatório, alias para setRequired()
Blank o controlo não deve ser preenchido
Equal o valor é igual ao parâmetro mixed
NotEqual o valor não é igual ao parâmetro mixed
IsIn o valor é igual a um dos itens no array array
IsNotIn o valor não é igual a nenhum item no array array
Valid o controlo está preenchido corretamente? (para Condições)

Entradas de texto

Para os controlos addText(), addPassword(), addTextArea(), addEmail(), addInteger(), addFloat(), algumas das seguintes regras também podem ser usadas:

MinLength comprimento mínimo do texto int
MaxLength comprimento máximo do texto int
Length comprimento no intervalo ou comprimento exato par [int, int] ou int
Email endereço de e-mail válido
URL URL absoluta
Pattern corresponde à expressão regular string
PatternInsensitive como Pattern, mas insensível a maiúsculas/minúsculas string
Integer valor inteiro
Numeric alias para Integer
Float número
Min valor mínimo do controlo numérico int|float
Max valor máximo do controlo numérico int|float
Range valor no intervalo par [int|float, int|float]

As regras de validação Integer, Numeric e Float convertem diretamente o valor para inteiro ou float, respetivamente. Além disso, a regra URL também aceita um endereço sem esquema (por exemplo, nette.org) e adiciona o esquema (https://nette.org). A expressão em Pattern e PatternIcase deve corresponder a todo o valor, ou seja, como se estivesse envolvida pelos caracteres ^ e $.

Número de itens

Para os controlos addMultiUpload(), addCheckboxList(), addMultiSelect(), as seguintes regras também podem ser usadas para limitar o número de itens selecionados ou ficheiros enviados:

MinLength número mínimo int
MaxLength número máximo int
Length número no intervalo ou número exato par [int, int] ou int

Upload de ficheiros

Para os controlos addUpload(), addMultiUpload(), as seguintes regras também podem ser usadas:

MaxFileSize tamanho máximo do ficheiro em bytes int
MimeType tipo MIME, curingas permitidos ('video/*') string|string[]
Image imagem JPEG, PNG, GIF, WebP, AVIF
Pattern nome do ficheiro corresponde à expressão regular string
PatternInsensitive como Pattern, mas insensível a maiúsculas/minúsculas string

MimeType e Image exigem a extensão PHP fileinfo. Elas detetam se um ficheiro ou imagem é do tipo desejado com base na sua assinatura e não verificam a integridade de todo o ficheiro. Se uma imagem não está danificada pode ser verificado, por exemplo, tentando carregá-la.

Mensagens de erro

Todas as regras predefinidas, exceto Pattern e PatternInsensitive, têm uma mensagem de erro padrão, então ela pode ser omitida. No entanto, fornecer e formular todas as mensagens sob medida tornará o formulário mais amigável ao utilizador.

Pode alterar as mensagens padrão na configuração, editando os textos no array Nette\Forms\Validator::$messages ou usando um tradutor.

No texto das mensagens de erro, podem ser usadas as seguintes strings de placeholder:

%d substitui sequencialmente pelos argumentos da regra
%n$d substitui pelo n-ésimo argumento da regra
%label substitui pelo rótulo do controlo (sem dois pontos)
%name substitui pelo nome do controlo (por exemplo, name)
%value substitui pelo valor inserido pelo utilizador
$form->addText('name', 'Nome:')
	->setRequired('Preencha por favor %label');

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'pelo menos %d e no máximo %d', [5, 10]);

$form->addInteger('id', 'ID:')
	->addRule($form::Range, 'no máximo %2$d e pelo menos %1$d', [5, 10]);

Condições

Além das regras, também é possível adicionar condições. Elas são escritas de forma semelhante às regras, mas em vez de addRule(), usamos o método addCondition() e, obviamente, não fornecemos nenhuma mensagem de erro (a condição apenas pergunta):

$form->addPassword('password', 'Senha:')
	// se a senha não tiver mais de 8 caracteres
	->addCondition($form::MaxLength, 8)
		// então deve conter um dígito
		->addRule($form::Pattern, 'Deve conter um dígito', '.*[0-9].*');

A condição também pode ser vinculada a outro controlo que não o atual, usando addConditionOn(). Como primeiro parâmetro, fornecemos uma referência ao controlo. Neste exemplo, o e-mail será obrigatório apenas se a caixa de seleção for marcada (o seu valor será true):

$form->addCheckbox('newsletters', 'enviar-me newsletters');

$form->addEmail('email', 'E-mail:')
	// se a caixa de seleção estiver marcada
	->addConditionOn($form['newsletters'], $form::Equal, true)
		// então exija o e-mail
		->setRequired('Insira o endereço de e-mail');

É possível criar estruturas complexas a partir de condições usando elseCondition() e endCondition():

$form->addText(/* ... */)
	->addCondition(/* ... */) // se a primeira condição for atendida
		->addConditionOn(/* ... */) // e a segunda condição em outro controlo
			->addRule(/* ... */) // exija esta regra
		->elseCondition() // se a segunda condição não for atendida
			->addRule(/* ... */) // exija estas regras
			->addRule(/* ... */)
		->endCondition() // voltamos à primeira condição
		->addRule(/* ... */);

Em Nette, é muito fácil reagir ao cumprimento ou não cumprimento de uma condição também no lado do JavaScript usando o método toggle(), veja JavaScript dinâmico.

Referência a outro controlo

Como argumento de uma regra ou condição, também é possível passar outro controlo do formulário. A regra então usará o valor inserido posteriormente pelo utilizador no navegador. Desta forma, é possível, por exemplo, validar dinamicamente que o controlo password contém a mesma string que o controlo password_confirm:

$form->addPassword('password', 'Senha');
$form->addPassword('password_confirm', 'Confirme a senha')
    ->addRule($form::Equal, 'As senhas inseridas não coincidem', $form['password']);

Regras e condições personalizadas

Ocasionalmente, chegamos a uma situação em que as regras de validação incorporadas em Nette não são suficientes e precisamos validar os dados do utilizador à nossa maneira. Em Nette, isso é muito simples!

Aos métodos addRule() ou addCondition(), é possível passar qualquer callback como primeiro parâmetro. Ele recebe o próprio controlo como primeiro parâmetro e retorna um valor booleano indicando se a validação foi bem-sucedida. Ao adicionar uma regra usando addRule(), é possível fornecer argumentos adicionais, que são então passados como segundo parâmetro.

Podemos criar o nosso próprio conjunto de validadores como uma classe com métodos estáticos:

class MyValidators
{
	// testa se o valor é divisível pelo argumento
	public static function validateDivisibility(BaseControl $input, $arg): bool
	{
		return $input->getValue() % $arg === 0;
	}

	public static function validateEmailDomain(BaseControl $input, $domain)
	{
		// outros validadores
	}
}

O uso é então muito simples:

$form->addInteger('num')
	->addRule(
		[MyValidators::class, 'validateDivisibility'],
		'O valor deve ser um múltiplo de %d',
		8,
	);

Regras de validação personalizadas também podem ser adicionadas ao JavaScript. A condição é que a regra seja um método estático. O seu nome para o validador JavaScript é formado pela junção do nome da classe sem barras invertidas \, um sublinhado _ e o nome do método. Por exemplo, App\MyValidators::validateDivisibility é escrito como AppMyValidators_validateDivisibility e adicionado ao objeto Nette.validators:

Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => {
	return val % args === 0;
};

Evento onValidate

Após o envio do formulário, a validação é realizada, onde as regras individuais adicionadas via addRule() são verificadas e, em seguida, o evento onValidate é disparado. O seu handler pode ser usado para validação adicional, tipicamente para verificar a combinação correta de valores em múltiplos controlos do formulário.

Se um erro for detetado, passamos para o formulário usando o método addError(). Ele pode ser chamado num controlo específico ou diretamente no formulário.

protected function createComponentSignInForm(): Form
{
	$form = new Form;
	// ...
	$form->onValidate[] = [$this, 'validateSignInForm'];
	return $form;
}

public function validateSignInForm(Form $form, \stdClass $data): void
{
	if ($data->foo > 1 && $data->bar > 5) {
		$form->addError('Esta combinação não é possível.');
	}
}

Erros durante o processamento

Em muitos casos, descobrimos um erro apenas no momento em que estamos a processar um formulário válido, por exemplo, ao inserir um novo item no banco de dados e encontrar uma duplicidade de chaves. Nesse caso, passamos novamente o erro para o formulário usando o método addError(). Ele pode ser chamado num controlo específico ou diretamente no formulário:

try {
	$data = $form->getValues();
	$this->user->login($data->username, $data->password);
	$this->redirect('Home:');

} catch (Nette\Security\AuthenticationException $e) {
	if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) {
		$form->addError('Senha inválida.');
	}
}

Se possível, recomendamos anexar o erro diretamente ao controlo do formulário, pois ele será exibido ao lado dele ao usar o renderizador padrão.

$form['date']->addError('Desculpe, mas esta data já está ocupada.');

Pode chamar addError() repetidamente para passar várias mensagens de erro ao formulário ou controlo. Pode obtê-las usando getErrors().

Atenção, $form->getErrors() retorna um resumo de todas as mensagens de erro, incluindo aquelas que foram passadas diretamente para controlos individuais, não apenas diretamente para o formulário. Mensagens de erro passadas apenas para o formulário podem ser obtidas via $form->getOwnErrors().

Modificação da entrada

Usando o método addFilter(), podemos modificar o valor inserido pelo utilizador. Neste exemplo, toleraremos e removeremos espaços no código postal:

$form->addText('zip', 'Código Postal:')
	->addFilter(function ($value) {
		return str_replace(' ', '', $value); // removemos espaços do código postal
	})
	->addRule($form::Pattern, 'Código Postal não está no formato de cinco dígitos', '\d{5}');

O filtro é integrado entre as regras de validação e condições, portanto, a ordem dos métodos importa, ou seja, o filtro e a regra são chamados na mesma ordem que os métodos addFilter() e addRule().

Validação JavaScript

A linguagem para formular condições e regras é muito poderosa. Todas as construções funcionam tanto no lado do servidor quanto no lado do JavaScript. Elas são transferidas em atributos HTML data-nette-rules como JSON. A validação em si é então realizada por um script que captura o evento submit do formulário, percorre os controlos individuais e executa a validação apropriada.

Esse script é netteForms.js e está disponível em várias fontes possíveis:

Pode inserir o script diretamente na página HTML a partir de um CDN:

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

Ou copiá-lo localmente para a pasta pública do projeto (por exemplo, de vendor/nette/forms/src/assets/netteForms.min.js):

<script src="/path/to/netteForms.min.js"></script>

Ou instalar via npm:

npm install nette-forms

E, em seguida, carregar e executar:

import netteForms from 'nette-forms';
netteForms.initOnLoad();

Alternativamente, pode carregá-lo diretamente da pasta vendor:

import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js';
netteForms.initOnLoad();

JavaScript dinâmico

Quer exibir os campos para inserir o endereço apenas se o utilizador escolher enviar o produto pelo correio? Sem problemas. A chave é o par de métodos addCondition() & toggle():

$form->addCheckbox('send_it')
	->addCondition($form::Equal, true)
		->toggle('#address-container');

Este código diz que quando a condição é atendida, ou seja, quando a caixa de seleção está marcada, o elemento HTML #address-container será visível. E vice-versa. Assim, colocamos os controlos do formulário com o endereço do destinatário num contêiner com este ID e, ao clicar na caixa de seleção, eles serão ocultados ou exibidos. Isso é garantido pelo script netteForms.js.

Como argumento do método toggle(), é possível passar qualquer seletor. Por razões históricas, uma string alfanumérica sem outros caracteres especiais é entendida como o ID do elemento, ou seja, da mesma forma que se fosse precedida pelo caractere #. O segundo parâmetro opcional permite inverter o comportamento, ou seja, se usássemos toggle('#address-container', false), o elemento seria exibido apenas se a caixa de seleção não estivesse marcada.

A implementação padrão em JavaScript altera a propriedade hidden dos elementos. No entanto, podemos facilmente alterar o comportamento, por exemplo, adicionando uma animação. Basta sobrescrever o método Nette.toggle em JavaScript com a sua própria solução:

Nette.toggle = (selector, visible, srcElement, event) => {
	document.querySelectorAll(selector).forEach((el) => {
		// ocultamos ou exibimos 'el' de acordo com o valor 'visible'
	});
};

Desativação da validação

Às vezes, pode ser útil desativar a validação. Se o pressionamento de um botão de envio não deve realizar a validação (adequado para botões Cancelar ou Visualizar), desativamo-la com o método $submit->setValidationScope([]). Se deve realizar apenas validação parcial, podemos especificar quais campos ou contêineres de formulário devem ser validados.

$form->addText('name')
	->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')
	->setRequired('age');
$details->addInteger('age2')
	->setRequired('age2');

$form->addSubmit('send1'); // Valida o formulário inteiro
$form->addSubmit('send2')
	->setValidationScope([]); // Não valida nada
$form->addSubmit('send3')
	->setValidationScope([$form['name']]); // Valida apenas o controlo name
$form->addSubmit('send4')
	->setValidationScope([$form['details']['age']]); // Valida apenas o controlo age
$form->addSubmit('send5')
	->setValidationScope([$form['details']]); // Valida o contêiner details

setValidationScope não afeta o evento onValidate no formulário, que será chamado sempre. O evento onValidate num contêiner será disparado apenas se este contêiner estiver marcado para validação parcial.

versão: 4.0