API
Resources (Output DTOs)
O sistema de Resources implementa o padrão Resource/Transformer para serialização consistente de dados em APIs. Transforma dados brutos de entidades, base de dados e arrays numa representação JSON estruturada e previsível.
Arquitectura do Sistema
O fluxo de transformação passa pelos seguintes componentes:
| Padrão | Implementação |
|---|---|
| Resource Pattern | Classe base JsonResource com método toArray() |
| Factory Pattern | Métodos estáticos make() e collection() |
| Decorator Pattern | Traits adicionam comportamentos (validação, serialização) |
| Singleton Pattern | MissingValue::getInstance() para valores ausentes |
Classe Base: JsonResource
A classe JsonResource é o coração do sistema, fornecendo a estrutura base para transformação de dados.
use Og\Modules\Common\Resource\JsonResource;
final class UserOutputDTO extends JsonResource
{
public function toArray(): array
{
return [
'id' => $this->codempr,
'email' => $this->email,
'name' => $this->nome,
'short_name' => $this->nomeabreviado,
];
}
}
// Uso
$result = UserOutputDTO::make($userData);
// Resultado: ['id' => 1, 'email' => 'joao@ex.com', ...]
make($data)
Transforma um único objecto/array. Retorna array vazio se $data for null.
collection($data)
Transforma listas. Detecta paginação automaticamente e inclui metadata.
Campos Condicionais
O trait ConditionalAttributesTrait permite controlar quais campos aparecem na saída JSON.
whenHas() — Campo Existe e Tem Valor
public function toArray(): array
{
return [
// Sintaxe simples
'name' => $this->whenHas('name'),
// Com callback de transformação
'created_at' => $this->whenHas('created_at',
fn ($v) => Carbon::parse($v)->toIso8601String()
),
// Dot notation para dados aninhados
'city' => $this->whenHas('address.city'),
// DTO aninhado com lazy loading
'customer' => $this->whenHas('customer',
fn () => CustomerOutputDTO::make($this->customer)
),
// Com valor default
'status' => $this->whenHas('status', null, 'active'),
];
}
| Entrada | Resultado | Campo na Saída? |
|---|---|---|
| ['name' => 'João'] | 'João' | ✅ Sim |
| ['name' => ''] | MissingValue | ❌ Não |
| ['name' => null] | MissingValue | ❌ Não |
| [] (ausente) | MissingValue | ❌ Não |
when() — Condição Booleana Livre
return [
'id' => $this->id,
// Só aparece se documento estiver anulado
'cancelReason' => $this->when(
$this->resource['anulado'] === 'T',
$this->resource['cancel_reason']
),
// Só aparece para admins
'adminNotes' => $this->when(
auth()->user()?->isAdmin(),
fn () => $this->fetchAdminNotes()
),
// Com valor default
'badge' => $this->when($this->score >= 100, 'gold', 'standard'),
];
mergeWhen() — Múltiplos Campos Condicionais
Importante: O operador spread (...) é obrigatório antes de merge() e mergeWhen().
return [
'id' => $this->id,
// Mescla múltiplos campos se for admin
...$this->mergeWhen($this->isAdmin, [
'email' => $this->email,
'permissions' => $this->permissions,
'last_login' => $this->last_login,
]),
// Com closure para lazy loading
...$this->mergeWhen($this->includeFinancials, fn () => [
'total_spent' => $this->orders()->sum('total'),
'credit_limit' => $this->credit_limit,
]),
];
Validação de Tipos
O sistema inclui validação robusta de tipos para garantir integridade de dados na saída.
Tipos Primitivos
'id' => $this->isInteger('id'),
'name' => $this->isString('name'),
'active' => $this->isBoolean('active'),
'settings' => $this->isArray('settings'),
Tipos Avançados
'tag_ids' => $this->isArrayOf('integer', 'tag_ids'),
'email' => $this->isEmail('email'),
'website' => $this->isNullOrUrl('website'),
'quantity' => $this->isPositiveInteger('quantity'),
Colecções e Paginação
O método collection() detecta automaticamente dados paginados e inclui metadata.
Colecção Simples
$users = [
['codempr' => 1, 'nome' => 'João'],
['codempr' => 2, 'nome' => 'Maria'],
];
UserOutputDTO::collection($users);
// [
// ['id' => 1, 'name' => 'João'],
// ['id' => 2, 'name' => 'Maria'],
// ]
Com Paginação
$paginated = [
'items' => [...],
'per_page' => 10,
'total' => 50,
'current_page' => 1,
];
// Resultado inclui 'meta':
// {
// "items": [...],
// "meta": {
// "current_page": 1,
// "per_page": 10,
// "total": 50,
// ...
// }
// }
Boas Práticas
✅ Correcto
// Usar whenHas() para segurança
'name' => $this->whenHas('name'),
// DTO aninhado com lazy loading
'customer' => $this->whenHas('customer',
fn () => CustomerDTO::make($this->customer)
),
❌ Evitar
// Acesso directo pode falhar
'name' => $this->resource['name'],
// DTO sem lazy loading pode lançar erro
'customer' => CustomerDTO::make($this->customer),
Convenção de Nomenclatura
- Nome da classe:
{Domain}{Entity}OutputDTO(ex:CrmAppointmentOutputDTO) - Namespace:
Og\Modules\{Module}\{Domain}\DTOs - Campos de saída:
snake_case(ex:employee_id,created_at)