Programação Por Equipe DevBras

Capa do artigo: jQuery Validate e PHP com as mesmas regras: uma fonte só, duas verificações

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

  1. Escreva as regras e mensagens num arquivo PHP, no formato que o jQuery Validate entende.
  2. Implemente no servidor um validador que lê esse mesmo arquivo.
  3. Entregue as regras para a página como JSON.
  4. No navegador, configure o plugin com essas regras e envie por AJAX.
  5. Quando o servidor recusar, devolva 422 com os erros por campo e mostre cada um ao lado do campo certo.
  6. 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 curl com 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?

Avalie este artigo

Referência: http://www.linhadecodigo.com.br/artigo/3592/jquery-criando-uma-validacao-de-formularios-html.aspx