@extends('layouts.docs') @section('title', 'Sistema de Validação - OG Framework') @section('body')
{{-- Floating shapes for styling --}}
{{-- Left Sidebar: Navigation --}} @include('docs.partials.sidebar') {{-- Main Content --}}
{{-- Page Header --}}
Voltar para Documentação

Forms & Data

Sistema de Validação

Motor robusto e extensível para validação de dados. Baseado nos padrões Strategy, Rule Pattern e Chain of Responsibility, oferece uma API fluente para validação de requests HTTP, forms e dados de API.

{{-- Architecture SVG Diagram --}}

Arquitectura

{{-- SVG: Validation Flow --}}
{{-- Input Sources --}} Origem dos Dados HTTP Request Form Data API Payload {{-- Arrow --}} {{-- Validator Core --}} Motor de Validação {{-- Validator --}} Validator 702 lines {{-- Rule Sets --}} Rule Sets • CommonRule • DateRule • FormatRule • FileRule • Custom Rules {{-- BaseRequest --}} BaseRequest {{-- Arrow --}} {{-- Output --}} Resultado validated() errors() {{-- Arrowhead --}}
{{-- Helper Functions --}}

Helpers Globais

validate()

Valida dados contra regras

$passes = validate($data, [
    'email' => 'required|email',
    'name' => 'required|min_length[2]'
]);

validationErrors()

Obtém erros de validação

if (!$passes) {
    $errors = validationErrors();
    // ['email' => 'O email é inválido']
}

validated()

Obtém dados validados

$data = validated();
// ['email' => 'test@ex.com', 
//  'name' => 'João']
{{-- BaseRequest --}}

BaseRequest

Classe abstracta para validação automática de HTTP requests:

class AppointmentsCreateRequest extends BaseRequest
{
    public function authorize(): bool
    {
        return true; // Ou lógica de permissão
    }

    public function rules(): array
    {
        return [
            'subject'    => ['required', 'string'],
            'start_date' => ['required', 'date_format[Y-m-d]'],
            'end_date'   => ['required', 'after_or_equal[start_date]'],
            
            // Condicionais
            'seller_id'  => ['if_exist', 'integer', 'exists[comerciais,codrepres]'],
            
            // Arrays
            'contacts'   => ['if_exist', 'array'],
            'contacts.*' => ['string'],
        ];
    }
}

// Uso no Controller
public function store()
{
    $request = new AppointmentsCreateRequest();
    return Appointment::create($request->validated());
}
{{-- Rule Factory --}}

Rule Factory

A classe Rule fornece criação fluente de rules complexas:

Unicidade com Ignore

use Og\Modules\Common\Validation\Rules\Rule;

$rules = [
    'email' => [
        'required',
        'email',
        Rule::unique('users', 'email')
            ->ignore($userId, 'id')
    ]
];

Existência e Enums

use Og\Modules\Common\Validation\Rules\Rule;

$rules = [
    'category_id' => [
        'required',
        'exists[categories,id,{active:1}]'
    ],
    'status' => Rule::enum(OrderStatus::class),
    'type'   => Rule::in(['A', 'B', 'C']),
];
{{-- Rule Catalog --}}

Catálogo de Rules

{{-- CommonRule --}}

CommonRule — Regras Básicas

Rule Sintaxe Descrição
requiredrequiredCampo obrigatório
min_lengthmin_length[5]Mínimo de caracteres
max_lengthmax_length[255]Máximo de caracteres
in_listin_list[A,B,C]Valor na lista
is_uniqueis_unique[table.col]Único na tabela
existsexists[table,col]Registo existe
greater_thangreater_than[0]Maior que valor
required_withrequired_with[f1,f2]Obrigatório se campos preenchidos
{{-- DateRule --}}

DateRule — Regras Temporais

Rule Sintaxe Descrição
beforebefore[field]Antes de outra data
afterafter[field]Depois de outra data
after_or_equalafter_or_equal[field]Depois ou igual
date_formatdate_format[Y-m-d]Formato específico
is_futureis_futureData no futuro
is_pastis_pastData no passado
{{-- FormatRule --}}

FormatRule — Regras de Formato

Rule Sintaxe Descrição
emailemailEmail válido
urlurlURL válida
regex_matchregex_match[/pattern/]Match de regex
valid_jsonvalid_jsonJSON válido
alpha_dashalpha_dashLetras, números, _, -
integerintegerDeve ser inteiro
{{-- FileRule --}}

FileRule — Regras de Ficheiros

$rules = [
    'avatar' => [
        'uploaded[avatar]',
        'max_size[avatar,1024]',        // 1MB
        'is_image[avatar]',
        'mime_in[avatar,image/jpeg,image/png,image/webp]'
    ],
    'document' => [
        'uploaded[document]',
        'max_size[document,5120]',      // 5MB
        'ext_in[document,pdf,doc,docx]'
    ],
];
{{-- Modifiers --}}

Modificadores Especiais

if_exist

Valida apenas se campo existir nos dados

'phone' => ['if_exist', 'string', 'max_length[20]'],
// Campo ausente = válido
// Campo presente = aplica regras

permit_empty

Permite valores vazios, mas valida se preenchido

'bio' => ['permit_empty', 'string', 'max_length[500]'],
// Campo vazio = válido
// Campo com valor = valida regras

sometimes

Aplica regras apenas se campo existir no dataset

'email' => ['sometimes', 'required', 'email'],
// Campo ausente = não valida
// Campo presente = email obrigatório

checkbox

Define um valor default quando o campo está ausente/vazio

// Default 0 se vazio
'newsletter' => ['checkbox[0]'],

// Obrigatório como 1
'terms' => ['required', 'checkbox[1]']
// Transforma em: 0 ou 1
{{-- Conditionable Trait --}}

Validação Condicional

Use Rule::when() para escolher rules em tempo de execução:

Rule::when() — Condicional

$isUpdate = $request->has('id');

$rules = [
    'email' => Rule::when(
        $isUpdate,
        // SE update:
        ['sometimes', 'email', Rule::unique('users', 'email')->ignore($userId, 'id')],
        // SENÃO (create):
        ['required', 'email', Rule::unique('users', 'email')]
    ),
];

Com Callback

$rules = [
    'phone' => Rule::when(
        fn () => $data['contact_method'] === 'phone',
        ['required', 'regex_match[/^\\+\\d+$/]'],
        ['nullable']
    ),
    'category_id' => Rule::when(
        $data['use_default_category'] ?? false,
        ['nullable'],
        ['required', 'exists[categories,id,{active:1}]']
    ),
];
{{-- Custom Messages --}}

After (pós-validação)

Use after() no BaseRequest para executar validações que dependem de contexto externo (ex.: permissões, estado em base de dados, integrações). O callback recebe o ValidatorInterface e pode adicionar erros com setError() / addError().

use Og\Modules\Common\Http\BaseRequest;
use Og\Modules\Common\Validation\Contracts\ValidatorInterface;
use Og\Modules\Common\Validation\Rules\Rule;

class AmsConciergeNoteRequest extends BaseRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'subject' => [
                Rule::requiredIf(fn () => $this->request->isPost()),
                'string',
                'max_length[128]',
            ],
        ];
    }

    public function after(): \Closure
    {
        return function (ValidatorInterface $validator): void {
            if (!$this->request->route('concierge')) {
                $validator->setError('concierge', 'Concierge inválido.');
            }
        };
    }
}

Mensagens Customizadas

Em BaseRequest

class UserRequest extends BaseRequest
{
    public function rules(): array
    {
        return [
            'email' => ['required', 'email'],
            'password' => ['required', 'min_length[8]'],
        ];
    }

    protected function messages(): array
    {
        return [
            'email' => [
                'required' => 'O email é obrigatório.',
                'email' => 'Forneça um email válido.',
            ],
            'password' => [
                'min_length' => 'Mínimo 8 caracteres.',
            ],
        ];
    }
}

Por Campo Inline

$rules = [
    'email' => [
        'label' => 'Endereço de Email',
        'rules' => ['required', 'email'],
        'errors' => [
            'required' => 'Preencha o email.',
            'email' => 'Formato inválido.',
        ]
    ]
];

// Placeholders disponíveis:
// {field} - Nome do campo
// {param} - Parâmetro da rule
// {value} - Valor actual
{{-- Dot Notation & Wildcards --}}

Dot Notation & Wildcards

O sistema suporta validação de estruturas aninhadas e placeholders dinâmicos:

$rules = [
    // Acesso aninhado
    'user.name' => ['required', 'string'],
    'user.profile.bio' => ['string', 'max_length[500]'],
    
    // Wildcard para arrays
    'items.*.name' => ['required', 'string'],
    'items.*.price' => ['required', 'numeric', 'greater_than[0]'],
    
    // Placeholders dinâmicos - substituídos pelos valores actuais
    'min_value' => ['required', 'numeric'],
    'max_value' => ['required', 'numeric', 'greater_than[{min_value}]'],
    'value' => ['required', 'numeric', 'greater_than[{min_value}]', 'less_than[{max_value}]'],
    
    // O validator substitui {min_value} por 10 e {max_value} por 100
];
{{-- Validator API --}}

Validator API

Métodos do validator (instância retornada por app(ValidatorInterface::class)):

Configuração & Execução

$v = app(ValidatorInterface::class);

// Definir rules
$v->setRule('email', 'Email', 'required|email');
$v->setRules(['email' => 'required', 'name' => 'string']);

// Carregar dados
$v->withRequest($data);
$v->defineData($data);

// Executar
$passes = $v->validate();
$passes = $v->validate($data); // inline

// Validação rápida de valor
$ok = $v->check($email, ['required', 'email']);

Resultados & Erros

// Erros
$erros = $v->errors();
$has = $v->hasError('email');
$msg = $v->error('email');

// Dados validados
$all = $v->validated();
$one = $v->validated('email');
$some = $v->validated(['email', 'name']);

// Filtros
$only = $v->only(['email'])->validated();
$except = $v->except(['password'])->validated();

// Adicionar erro manual
$v->addError('field', 'Mensagem');

// Callback after (pós-validação)
$v->after(function (ValidatorInterface $validator): void {
    $validator->setError('field', 'Mensagem');
});
{{-- Custom Rules --}}

Custom Rules

Crie regras personalizadas implementando as interfaces disponíveis:

RuleInterface — Básico

class ValidNIF implements RuleInterface
{
    public function passes($attr, $value): bool
    {
        $nif = preg_replace('/\s+/', '', $value);
        if (!preg_match('/^[0-9]{9}$/', $nif)) {
            return false;
        }
        // Validar dígito de controlo...
        return (int)$nif[8] === $checkDigit;
    }

    public function message(): string
    {
        return 'O campo {field} deve ter um NIF válido.';
    }
}

// Uso
'nif' => ['required', new ValidNIF()]

DataAwareRule — Com Dados

Quando a rule precisa enxergar outros campos do payload, implemente DataAwareRule. O validator injeta o dataset completo antes de chamar passes().

class UniqueInvoice implements RuleInterface, DataAwareRule
{
    private array $data = [];
    
    public function defineData(array $data): void
    {
        $this->data = $data;
    }

    public function passes($attr, $value): bool
    {
        // Acesso a outros campos
        $companyId = $this->data['company_id'];
        $year = $this->data['year'];
        
        return !Invoice::where('company_id', $companyId)
            ->where('year', $year)
            ->where('number', $value)
            ->exists();
    }
}

ValidatorAwareRule — Acesso ao Validator

Útil quando você precisa adicionar erro em outro campo, ou consultar estado do validator durante a validação. O validator injeta a instância via defineValidator().

use Og\Modules\Common\Validation\Contracts\ValidatorAwareRule;
use Og\Modules\Common\Validation\Contracts\ValidatorInterface;

class RequiresInvoiceIdWhenTypeIsInvoice implements RuleInterface, DataAwareRule, ValidatorAwareRule
{
    private array $data = [];
    private ?ValidatorInterface $validator = null;

    public function defineData(array $data): void
    {
        $this->data = $data;
    }

    public function defineValidator(ValidatorInterface $validator): void
    {
        $this->validator = $validator;
    }

    public function passes($attr, $value): bool
    {
        $type = $this->data['type'] ?? null;
        $invoiceId = $this->data['invoice_id'] ?? null;

        if ($type === 'invoice' && empty($invoiceId)) {
            $this->validator?->setError('invoice_id', 'Informe a fatura para este tipo.');
        }

        return true;
    }

    public function message(): string
    {
        return '';
    }
}

CompilableRules (WIP) — ForEach Style

A interface CompilableRules existe para viabilizar regras “tipo forEach() do Laravel”: permitir que um objeto gere, em runtime, um conjunto de rules a partir do $attribute e do $value (inclusive para campos resolvidos por wildcard como items.0.price, items.1.price).

Status: contrato presente, mas a integração automática no validator pode ainda não estar finalizada. Enquanto isso, use wildcards (items.*.field) + Rule::when() e/ou rules custom com DataAwareRule.

use Og\Modules\Common\Validation\Contracts\CompilableRules;

class AmountRules implements CompilableRules
{
    public function compile(string $attribute, mixed $value, array $data = null): array
    {
        $data ??= [];

        $rules = ['numeric', 'greater_than[0]'];
        if (($data['currency'] ?? 'EUR') === 'EUR') {
            $rules[] = 'max_value[999999]';
        }

        return $rules;
    }
}

Interfaces Disponíveis

RuleInterface — Implementa passes() e message()
DataAwareRule — Recebe o payload via defineData()
ValidatorAwareRule — Recebe o validator via defineValidator()
CompilableRules — Gera um array de rules via compile()
{{-- File Rules Detail --}}

Validação de Ficheiros

Rules especializadas para upload de ficheiros:

Rule Sintaxe Descrição
uploadeduploaded[field]Ficheiro foi uploaded com sucesso
max_sizemax_size[field,2048]Tamanho máximo em KB
mime_inmime_in[field,image/png,image/jpeg]Tipos MIME permitidos
ext_inext_in[field,pdf,doc,docx]Extensões permitidas
max_dimsmax_dims[field,1024,768]Dimensões máximas (imagens)
is_imageis_image[field]Valida se é imagem
$rules = [
    'avatar' => [
        'uploaded[avatar]',
        'max_size[avatar,1024]',              // 1MB
        'is_image[avatar]',
        'mime_in[avatar,image/jpeg,image/png,image/webp]',
        'max_dims[avatar,800,800]'            // Max 800x800px
    ],
    'document' => [
        'uploaded[document]',
        'max_size[document,5120]',            // 5MB
        'ext_in[document,pdf,doc,docx,xls,xlsx]'
    ],
    'attachments.*' => [                       // Array de ficheiros
        'uploaded[attachments.*]',
        'max_size[attachments.*,2048]'
    ]
];
{{-- Navigation --}}
{{-- Right Sidebar: Table of Contents --}} @include('docs.partials.toc', ['sections' => [ 'arquitectura' => 'Arquitectura', 'helpers' => 'Helpers Globais', 'base-request' => 'BaseRequest', 'rule-factory' => 'Rule Factory', 'catalogo-rules' => 'Catálogo de Rules', 'modificadores' => 'Modificadores', 'conditionable' => 'Validação Condicional', 'after-callbacks' => 'After (pós-validação)', 'mensagens' => 'Mensagens Customizadas', 'wildcards' => 'Dot Notation', 'validator-api' => 'Validator API', 'custom-rules' => 'Custom Rules', 'file-rules' => 'Ficheiros', ]])
@endsection