Validação de dados


1Introdução

O Temma oferece um sistema abrangente de validação de dados, tanto para entrada (dados recebidos pelo servidor) quanto para saída (dados enviados ao navegador).

Três níveis de uso estão disponíveis:

  1. Atributos PHP (abordagem declarativa): os atributos Check\* permitem validar dados diretamente nos controladores e ações, sem escrever código de validação. Essa é a abordagem mais simples e mais comum.
  2. Métodos do objeto Request (abordagem programática): para os casos em que os atributos não são suficientes, os métodos validate*() do objeto Request oferecem controle total a partir do código do controlador.
  3. O objeto DataFilter (baixo nível): o bloco de construção fundamental sobre o qual todo o sistema é construído. Pode ser usado em qualquer lugar do seu código para validar qualquer dado.

O fio condutor de todo o sistema é o contrato de validação: uma definição que descreve o formato de dados esperado.


2Contratos de validação

2.1Formato do contrato

Um contrato de validação pode ser escrito de duas formas:

Formato string (os parâmetros são separados por ponto e vírgula):

// um número inteiro entre 0 e 100
'int; min: 0; max: 100'

// uma string de 2 a 50 caracteres
'string; minLen: 2; maxLen: 50'

// um endereço de e-mail correspondente a um padrão
'email; mask: @mydomain\.com$'

Formato array (cada parâmetro é uma chave do array):

// um número inteiro entre 0 e 100
['type' => 'int', 'min' => 0, 'max' => 100]

// um array associativo com chaves tipadas
[
    'type' => 'assoc',
    'keys' => [
        'id'    => 'int',
        'name'  => 'string; minLen: 2',
        'email' => 'email',
    ]
]

Os dois formatos são intercambiáveis e podem ser usados em qualquer lugar onde um contrato seja esperado. Para mais detalhes, veja a documentação do DataFilter.


2.2Modo estrito e não estrito

Por padrão, a validação é não estrita: os dados são convertidos quando possível (uma string "42" é aceita para um contrato int), valores fora do intervalo são ajustados (um número grande demais é limitado ao máximo), e strings muito longas são truncadas.

No modo estrito, nenhuma conversão é realizada: os dados devem corresponder exatamente ao tipo esperado, e qualquer violação de limite causa um erro.

O modo pode ser controlado de várias formas:

  • Globalmente, por meio do parâmetro $strict dos atributos ou métodos de validação.
  • Por contrato, prefixando o tipo: = para forçar o modo estrito (ex.: '=int'), ~ para forçar o modo não estrito (ex.: '~string').

2.3Tipos disponíveis

O Temma oferece uma ampla gama de tipos de validação:

  • Escalares: null, bool, true, false, int, float, string
  • Texto e identificadores: email, url, slug, uuid, color, phone
  • Datas e horários: date, time, datetime
  • Rede: ip, ipv4, ipv6, mac, port
  • Códigos: isbn, ean, hash (e os aliases md5, sha1, sha256, sha512)
  • Geografia: geo
  • Estruturas: enum, list, assoc
  • Dados binários: json, binary, base64

Os tipos podem ser combinados com o operador pipe: 'null|int|string', e tornados anuláveis com o prefixo ?: '?bool'.

Para todos os detalhes de cada tipo e seus parâmetros, veja a documentação do DataFilter.


2.4Parâmetros universais

Vários parâmetros estão disponíveis para todos (ou a maioria) os tipos:

  • default: valor padrão caso os dados sejam inválidos.
  • min / max: valores mínimo e máximo (números, datas, coordenadas).
  • minLen / maxLen: comprimento mínimo e máximo (strings, listas). maxLen aceita as unidades K, M, G.
  • mask: expressão regular para validação (strings, e-mails, URLs).
  • values: valores aceitos (para enum).
  • contract: contrato de validação para os elementos de um list ou json.
  • keys: chaves esperadas em um array assoc.
  • mime: tipos MIME permitidos (binary, base64).
  • charset: codificação de caracteres.
  • format / inFormat / outFormat: formatos de data/hora.

2.5Chaves opcionais e curinga

Em contratos do tipo assoc (e em parâmetros GET/POST), as chaves podem ser marcadas como opcionais com o sufixo ?:

[
    'name'       => 'string',       // obrigatório
    'firstname?' => 'string',       // opcional
    'age'        => 'int; min: 0',  // obrigatório
]

O curinga ... permite aceitar dados adicionais não definidos no contrato. Ele pode, opcionalmente, impor um tipo:

// aceita todas as chaves adicionais como estão
['name' => 'string', '...']

// aceita chaves adicionais, mas elas devem ser inteiros
['name' => 'string', '...' => 'int']

Sem o curinga, no modo não estrito as chaves não definidas são removidas; no modo estrito, elas causam um erro.


3Contratos nomeados (configuração)

Para evitar a duplicação de contratos de validação, você pode defini-los uma única vez no arquivo de configuração etc/temma.php, na chave validationTypes:

<?php

return [
    'validationTypes' => [
        // contrato simples (enumeração)
        'sitecolor' => 'enum; values: orange, yellow, lime, green',
        // contrato complexo (array associativo)
        'user' => [
            'type' => 'assoc',
            'keys' => [
                'id?'   => 'int',
                'login' => 'string',
                'email' => 'email',
            ],
        ],
        // objeto de validação personalizado
        'category' => '\App\Validations\CategoryValidator',
    ],
];

Esses contratos nomeados podem então ser usados em qualquer lugar onde um contrato seja esperado (atributos Check\*, métodos validate*(), objeto DataFilter), simplesmente passando seu nome como uma string:

// em um atributo
#[TµCheckPost('user')]

// em um método de validação
$this->_request->validateInput('user');

// no DataFilter
\Temma\Utils\DataFilter::process($data, 'user');

4Validação de entrada (atributos Check)

4.1Os cinco atributos

Os atributos Check\* permitem validar os dados recebidos de forma declarativa, diretamente nos controladores e ações:

  • \Temma\Attributes\Check\Params: parâmetros da URL.
  • \Temma\Attributes\Check\Get: parâmetros GET.
  • \Temma\Attributes\Check\Post: parâmetros POST.
  • \Temma\Attributes\Check\Files: arquivos enviados.
  • \Temma\Attributes\Check\Payload: corpo da requisição (JSON, base64, binário).

Exemplo: validar que um formulário POST contém um e-mail e um nome:

use \Temma\Attributes\Check\Post as TµCheckPost;

class User extends \Temma\Web\Controller {
    // em caso de erro, redireciona para o referer HTTP;
    // os dados POST recebidos são copiados para a
    // variável flash padrão '__form'
    #[TµCheckPost([
        'email' => 'email',
        'name'  => 'string; minLen: 2',
    ])]
    public function create() {
        // os dados são válidos, podemos usá-los
    }
}

Exemplo: validar os parâmetros de URL de uma ação:

use \Temma\Attributes\Check\Params as TµCheckParams;

class Article extends \Temma\Web\Controller {
    // espera um número inteiro positivo e uma string (slug)
    #[TµCheckParams(['int; min: 1', 'slug'])]
    public function show(int $id, string $slug) {
        // ...
    }
}

Exemplo: validar um payload JSON:

use \Temma\Attributes\Check\Payload as TµCheckPayload;

class Api extends \Temma\Web\Controller {
    // espera um fluxo JSON contendo um array associativo;
    // em caso de erro, redireciona para '/api/error';
    // a variável flash '__apiErr' é definida como true
    #[TµCheckPayload(
        [
            'type'     => 'json',
            'contract' => [
                'type' => 'assoc',
                'keys' => [
                    'id'   => 'int',
                    'role' => 'enum; values: user, admin',
                ],
            ],
        ],
        redirect: '/api/error',
        flashVar: 'apiErr',
    )]
    public function update() {
        // ...
    }
}

Exemplo: recuperar os dados validados em uma variável de template:

use \Temma\Attributes\Check\Payload as TµCheckPayload;

class Api extends \Temma\Web\Controller {
    // espera um fluxo JSON; os dados validados são armazenados
    // na variável de template 'jsonData'
    #[TµCheckPayload(
        ['type' => 'json', 'contract' => ['id' => 'int', 'name' => 'string']],
        dataVar: 'jsonData',
    )]
    public function import() {
        // $this['jsonData'] contém os dados JSON decodificados e validados
    }
}

Para todos os detalhes e mais exemplos, veja a documentação dos atributos Check.


4.2Parâmetros comuns

Todos os atributos Check\* (exceto Output) aceitam parâmetros comuns para controlar o comportamento em caso de falha na validação:

  • $strict: (bool) ativa o modo de validação estrita (false por padrão).
  • $redirect: (string) URL de redirecionamento em caso de erro.
  • $redirectVar: (string) nome da variável de template que contém a URL de redirecionamento.
  • $redirectReferer: (bool) true por padrão. Se true e $redirect e $redirectVar estiverem vazios, o cabeçalho HTTP REFERER é usado como URL de redirecionamento.
  • $flashVar: (string) nome da variável flash que conterá os dados recebidos no redirecionamento ('form' por padrão, tornando os dados acessíveis em __form). Defina como null para desativar.
  • $dataVar: (?string) nome da variável de template que conterá os dados validados/filtrados pelo DataFilter. Permite recuperar diretamente os dados processados, evitando operações redundantes. null por padrão. Não disponível para Files.

4.3Prioridade de redirecionamento

Quando os dados são inválidos, a URL de redirecionamento é determinada na seguinte ordem:

  1. O parâmetro $redirect, se definido.
  2. O conteúdo da variável de template especificada por $redirectVar, se existir e não estiver vazia.
  3. O cabeçalho HTTP Referer, se $redirectReferer for true e o cabeçalho estiver presente.
  4. A chave redirect da configuração estendida x-security em etc/temma.php.

Se nenhuma URL for encontrada, um erro HTTP 403 (Forbidden) é retornado.


5Validação de saída (atributo Output)

O atributo \Temma\Attributes\Check\Output define um contrato de validação para os dados de saída (variáveis de template). Esse contrato pode ser usado pela view para validar os dados antes de enviá-los ao navegador.

use \Temma\Attributes\Check\Output as TµCheckOutput;

class User extends \Temma\Web\Controller {
    // as variáveis de template 'name' e 'email' devem estar presentes
    // e válidas; 'balance' é opcional
    #[TµCheckOutput([
        'name'     => 'string',
        'email'    => 'email',
        'balance?' => 'float',
    ])]
    public function show() {
        // ...
    }
}

Diferente dos demais atributos Check\*, Output não suporta parâmetros de redirecionamento ($redirect, $redirectVar, $flashVar).


6Validação programática (Request)

Para os casos em que os atributos não são suficientes (lógica condicional, contratos dinâmicos etc.), o objeto Request expõe quatro métodos de validação. Em caso de falha, eles lançam uma exceção \Temma\Exceptions\Application.

6.1validateParams

Valida os parâmetros recebidos na URL. O contrato é uma lista ordenada (um contrato por parâmetro). Um parâmetro opcional &$output permite recuperar os dados validados/filtrados por referência:

// o primeiro parâmetro deve ser um inteiro >= 1,
// o segundo uma string slug
$this->_request->validateParams(['int; min: 1', 'slug']);

// o primeiro deve ser um inteiro,
// o restante é aceito como está
$this->_request->validateParams(['int', '...']);

// usando um contrato nomeado
$this->_request->validateParams('deleteUserParameters');

// com recuperação dos dados filtrados
$output = null;
$this->_request->validateParams(['int; min: 1', 'slug'], output: $output);

6.2validateInput

Valida os parâmetros GET e/ou POST. O contrato é um array associativo (chave = nome do parâmetro). Um parâmetro opcional &$output permite recuperar os dados validados/filtrados por referência:

// valida os parâmetros 'name' e 'email' (GET ou POST)
$this->_request->validateInput([
    'name'  => 'string; minLen: 2',
    'email' => 'email',
]);

// valida apenas os parâmetros POST, em modo estrito;
// 'firstname' é opcional, 'age' é forçado ao modo não estrito
$this->_request->validateInput(
    [
        'name'       => 'string',
        'firstname?' => 'string',
        'age'        => '~int',
    ],
    'POST',
    true
);

// usando um contrato nomeado
$this->_request->validateInput('user');

// com recuperação dos dados filtrados
$output = null;
$this->_request->validateInput(
    ['name' => 'string', 'email' => 'email'],
    'POST',
    output: $output
);

6.3validatePayload

Valida o corpo da requisição (payload). Um parâmetro opcional &$output permite recuperar os dados validados/filtrados por referência:

// fluxo JSON contendo uma lista de inteiros
$this->_request->validatePayload([
    'type'     => 'json',
    'contract' => 'list; contract: int',
]);

// imagem codificada em base64 (GIF ou PNG)
$this->_request->validatePayload('base64; mime: image/gif, image/png');

// usando um contrato nomeado
$this->_request->validatePayload('avatar');

// com recuperação de metadados
$output = null;
$this->_request->validatePayload('binary; mime: image', output: $output);
// $output contém ['binary' => ..., 'mime' => ..., 'charset' => ...]

6.4validateFiles

Valida os arquivos enviados. O contrato é um array associativo (chave = nome do campo de arquivo):

// um arquivo JSON obrigatório e uma imagem opcional
$this->_request->validateFiles([
    'definition' => 'json',
    'avatar?'    => 'binary; mime: image',
]);

// um arquivo JSON e qualquer número de arquivos PDF
$this->_request->validateFiles([
    'count' => 'json; contract: int',
    '...'   => 'binary; mime: application/pdf',
]);

7Validação programática (Response)

O objeto Response permite gerenciar um contrato de validação para os dados de saída:

// define um contrato de validação de saída
$this->_response->setValidationContract([
    'name'  => 'string',
    'email' => 'email',
]);

// usa um contrato nomeado
$this->_response->setValidationContract('userData');

// recupera o contrato definido
$contract = $this->_response->getValidationContract();

// remove o contrato
$this->_response->setValidationContract(null);

O contrato definido pode ser usado pela view para validar as variáveis de template antes de enviá-las ao navegador.