jQuery Validate e PHP com as mesmas regras: uma fonte só, duas verificações
Defina as regras do formulário uma vez no PHP, entregue ao jQuery Validate como JSON e faça o servidor recusar o mesmo dado com a mesma mensagem, com ou sem JavaScript.
O formulário de contato da Casa Pinho pintava de vermelho o e-mail inválido e ninguém se preocupava. Até que a tabela de mensagens apareceu com 4 mil linhas de nome vazio e e-mail "a": um robô mandava POST direto, sem abrir a página, e o jQuery nunca rodou. A resposta curta: a regra que vale é a do servidor; a do navegador só antecipa o aviso. E as duas devem vir da mesma definição, para não divergirem com o tempo.
O guia sobre plugin de validação que não basta explica o princípio. Aqui o foco é a prática com jQuery Validate e PHP: escrever as regras uma vez, usar nos dois lados e devolver o erro do servidor no mesmo lugar da tela.
Por que a validação do navegador não protege nada
Tudo que roda no navegador está nas mãos de quem usa. Basta desligar o JavaScript, editar o HTML no DevTools ou mandar o pedido com curl. O servidor recebe o que vier, e a única pergunta que importa é se ele aceita.
O segundo problema é mais sutil: duas regras escritas em dois lugares se afastam. Alguém aumenta o limite da mensagem no JavaScript para 5 mil caracteres e esquece o PHP, que continua cortando em 2 mil. A pessoa passa pela validação da tela, envia, e recebe um erro que não entende. Duas regras que divergem são piores que uma regra só no servidor, porque ensinam a pessoa a confiar num aviso que não corresponde ao que o sistema aceita.
Como manter uma fonte única de regras
- Escreva as regras e mensagens num arquivo PHP, no formato que o jQuery Validate entende.
- Implemente no servidor um validador que lê esse mesmo arquivo.
- Entregue as regras para a página como JSON.
- No navegador, configure o plugin com essas regras e envie por AJAX.
- Quando o servidor recusar, devolva 422 com os erros por campo e mostre cada um ao lado do campo certo.
- Sem JavaScript, o mesmo endpoint redesenha o formulário com os erros.
As regras, escritas uma vez
<?php
// regras-contato.php
return [
'regras' => [
'nome' => ['required' => true, 'minlength' => 2, 'maxlength' => 80],
'email' => ['required' => true, 'email' => true, 'maxlength' => 120],
'mensagem' => ['required' => true, 'minlength' => 10, 'maxlength' => 2000],
],
'mensagens' => [
'nome' => [
'required' => 'Informe seu nome.',
'minlength' => 'O nome precisa de pelo menos 2 letras.',
'maxlength' => 'Use no máximo 80 caracteres no nome.',
],
'email' => [
'required' => 'Informe seu e-mail.',
'email' => 'Esse e-mail não parece válido.',
'maxlength' => 'Use no máximo 120 caracteres no e-mail.',
],
'mensagem' => [
'required' => 'Escreva sua mensagem.',
'minlength' => 'Escreva pelo menos 10 caracteres.',
'maxlength' => 'A mensagem pode ter até 2000 caracteres.',
],
],
];
Os nomes das regras (required, minlength, maxlength, email) são os do próprio plugin. Isso permite passar o array convertido em JSON sem tradução.
O validador do servidor
<?php
// validar.php
function validar(array $dados, array $config): array
{
$erros = [];
foreach ($config['regras'] as $campo => $regras) {
$valor = trim((string) ($dados[$campo] ?? ''));
$tamanho = mb_strlen($valor, 'UTF-8');
$msg = $config['mensagens'][$campo];
if (!empty($regras['required']) && $valor === '') {
$erros[$campo] = $msg['required'];
continue;
}
if ($valor === '') {
continue;
}
if (isset($regras['minlength']) && $tamanho < $regras['minlength']) {
$erros[$campo] = $msg['minlength'];
} elseif (isset($regras['maxlength']) && $tamanho > $regras['maxlength']) {
$erros[$campo] = $msg['maxlength'];
} elseif (!empty($regras['email']) && filter_var($valor, FILTER_VALIDATE_EMAIL) === false) {
$erros[$campo] = $msg['email'];
}
}
return $erros;
}
O mb_strlen conta caracteres, não bytes. Com strlen, "João" teria 5 e o limite do servidor discordaria do que o navegador contou. Os campos que o array não conhece são ignorados: o servidor só grava o que tem regra.
O endpoint que atende os dois casos
<?php
// contato.php
declare(strict_types=1);
require __DIR__ . '/validar.php';
$config = require __DIR__ . '/regras-contato.php';
$querJson = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json');
$erros = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$erros = validar($_POST, $config);
if ($erros && $querJson) {
http_response_code(422);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['erros' => $erros], JSON_UNESCAPED_UNICODE);
exit;
}
if (!$erros) {
// gravar a mensagem com consulta parametrizada
if ($querJson) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => true]);
exit;
}
header('Location: /contato-enviado.php', true, 303);
exit;
}
}
$regrasJson = json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
require __DIR__ . '/form-contato.phtml';
Sem JavaScript, o formulário é um POST comum: se houver erro, o template recebe $erros e mostra cada frase ao lado do campo. O JSON_HEX_TAG evita que um texto com </script> feche a tag onde o JSON será impresso.
O lado do navegador
No template, imprima a configuração dentro de um <script> como window.REGRAS_CONTATO = <?= $regrasJson ?>; e carregue o plugin. Depois:
const $form = $('#form-contato');
const validador = $form.validate({
rules: window.REGRAS_CONTATO.regras,
messages: window.REGRAS_CONTATO.mensagens,
submitHandler: function (form) {
$.ajax({
url: form.action,
method: 'POST',
data: $(form).serialize(),
dataType: 'json'
})
.done(function () {
window.location.href = '/contato-enviado.php';
})
.fail(function (xhr) {
if (xhr.status === 422 && xhr.responseJSON && xhr.responseJSON.erros) {
validador.showErrors(xhr.responseJSON.erros);
} else {
$('#aviso-contato').text('Não conseguimos enviar agora. Tente de novo em instantes.');
}
});
}
});
O showErrors do plugin recebe um objeto no formato { campo: mensagem }, que é exatamente o que o servidor devolve. O erro do servidor aparece no mesmo lugar e com a mesma cara do erro do navegador. Mantenha a mesma frase nos dois lados, como defende o guia sobre mensagem de erro igual na API.
O que só o servidor consegue conferir
Algumas regras não cabem no navegador, e não adianta tentar. Saber se o e-mail já está cadastrado, se a pessoa mandou 30 mensagens no último minuto ou se o token CSRF do formulário é válido exige dados que só o servidor tem. Essas verificações entram depois do validar(), no mesmo endpoint, e devolvem o erro no mesmo formato { campo: mensagem } quando dizem respeito a um campo.
O plugin tem a regra remote, que consulta o servidor enquanto a pessoa digita. Ela melhora a experiência, mas não substitui a checagem no envio: entre a consulta e o POST, outra pessoa pode ter usado o mesmo e-mail. Trate-a como conforto, exatamente como o resto da validação da tela.
Teste sem o navegador
O teste que prova a proteção não usa a página. Mande o dado ruim direto:
curl -i -X POST https://contato.exemplo.test/contato.php \
-H "Accept: application/json" \
-d "nome=A&email=nao-e-email&mensagem=oi"
A resposta esperada é HTTP/1.1 422 com três erros no JSON. Se vier 200, a validação mora só na tela. Repare também que o validador de e-mail do plugin e o filter_var não são idênticos em casos raros. Quando discordarem, quem decide é o servidor, e a mensagem 422 corrige a tela.
Erros comuns
- Validar só no plugin: qualquer POST direto grava lixo, e a tabela enche de registros que o sistema nunca deveria ter aceito.
- Campo oculto "validado=1": o servidor confia num valor que o próprio cliente escreve; o robô também escreve.
- Regras copiadas à mão nos dois lados: na primeira mudança elas divergem e a pessoa passa na tela para ser recusada no servidor.
- Contar bytes em vez de caracteres: nomes com acento estouram o limite no PHP e passam no JavaScript.
- Resposta de erro genérica no AJAX: a pessoa não sabe qual campo corrigir e envia de novo o mesmo dado.
- Formulário que depende de JavaScript para enviar: com o script bloqueado ou com falha de carregamento, ninguém consegue falar com a loja.
Checklist
- As regras e mensagens existem num único arquivo, usado pelo PHP e pela página.
- Um
curlcom dados inválidos recebe 422 e a lista de erros por campo. - Com JavaScript desligado, o formulário envia e mostra os erros do servidor.
- O erro devolvido pelo servidor aparece ao lado do campo correspondente.
- Tamanhos são contados em caracteres com
mb_strlen. - O servidor ignora campos que não têm regra.
- A gravação no banco usa consulta parametrizada.
Desligue o JavaScript no navegador e envie o seu formulário com o e-mail "a": o registro foi recusado ou já está na tabela?
Referência: http://www.linhadecodigo.com.br/artigo/3592/jquery-criando-uma-validacao-de-formularios-html.aspx