# Sistema de Base de Dados

**ORM Moderno e Schema Builder para OfficeGest — Documentação Completa**

---

## Índice

1. [Visão Geral](#visão-geral)
2. [Modelos de Dados (Data Models)](#modelos-de-dados-data-models)
3. [Sistema de Casting](#sistema-de-casting)
4. [Accessors e Mutators](#accessors-e-mutators)
5. [Timestamps Configuráveis](#timestamps-configuráveis)
6. [Mass Assignment Protection](#mass-assignment-protection)
7. [Visibilidade de Atributos](#visibilidade-de-atributos)
8. [Builder (Query Builder Moderno)](#builder-query-builder-moderno)
9. [CRUD Operations](#crud-operations)
10. [Interfaces Implementadas](#interfaces-implementadas)
11. [CLI: make:model](#cli-makemodel)
12. [Entidades Ricas (DDD)](#entidades-ricas-ddd)
13. [OGDB Forge (Schema Builder)](#ogdb-forge-schema-builder)
14. [Modernização: Legado vs. Moderno](#modernização-legado-vs-moderno)
15. [Referências](#referências)

---

## Visão Geral

O Sistema de Base de Dados do OfficeGest fornece uma camada moderna de ORM sobre o sistema legado existente, oferecendo:

- **Modelos tipados** com autocompletar e PHPDoc
- **Query Builder moderno** com proteções contra operações em massa
- **Sistema de Casting** para conversão automática de tipos
- **Accessors/Mutators** com PHP 8 Attributes
- **Schema Builder (OGDB Forge)** para criação e evolução de esquemas
- **Integração transparente** com código legado

### Arquitetura

```
Modules/Common/Database/
├── Model.php           # Classe base para modelos
├── Builder.php         # Query builder moderno
├── Traits/
│   ├── Castable.php        # Sistema de casting
│   ├── HasAttributes.php   # Manipulação de atributos
│   ├── HasAccessors.php    # PHP 8 Attributes para accessors
│   ├── TracksChanges.php   # Dirty tracking
│   ├── Persistable.php     # CRUD operations
│   ├── HasTimestamps.php   # Timestamps configuráveis
│   └── HashesPasswords.php # Hash automático de passwords
├── Attributes/
│   ├── Computed.php    # Accessor moderno
│   ├── Mutator.php     # Mutator moderno
│   └── Accessor.php    # Combined get/set
├── Casts/
│   ├── CastInterface.php   # Interface para casts
│   ├── JsonCast.php        # JSON encode/decode
│   ├── DateTimeCast.php    # Carbon conversion
│   ├── DecimalCast.php     # Números com precisão
│   └── CollectionCast.php  # Array → Collection
└── Interfaces/
    ├── QueueableEntity.php # Serialização em jobs
    └── UrlRoutable.php     # Route model binding
```

### Filosofia

O sistema visa:

| Princípio | Descrição |
|-----------|-----------|
| **Tipagem forte** | PHPDoc e typed properties para autocompletar |
| **Consistência** | Convenções padronizadas em todo o código |
| **Performance** | Modo híbrido: modelos para CRUD, arrays para relatórios |
| **Retrocompatibilidade** | Integração com tabelas e código legado |

---

## Modelos de Dados (Data Models)

### O Que São Modelos?

Um **Modelo** é uma classe PHP que representa uma tabela da base de dados. Em vez de trabalharmos diretamente com arrays associativos retornados do query builder, trabalhamos com **objetos tipados**.

### Estrutura Básica

```php
<?php

namespace Og\Modules\Tax\Models;

use Og\Modules\Common\Database\Model;

/**
 * Modelo de IVA (Taxas de Imposto).
 * 
 * @property-read string $codiva       Código do IVA
 * @property-read string $designacao   Descrição da taxa
 * @property-read float $iva           Percentagem de IVA
 * @property-read string $safttaxcode  Código SAF-T
 * @property-read Carbon $date_reg     Data de registo
 * @property-read Carbon $date_alter   Data de alteração
 */
class Vat extends Model
{
    /**
     * Nome da tabela (pode usar constantes).
     */
    protected string $table = TABLE_IVAS;
    
    /**
     * Chave primária (suporta chaves compostas via array).
     */
    protected string|array $primaryKey = 'CodIVA';
    
    /**
     * Casting automático de atributos.
     */
    protected array $casts = [
        'iva' => 'decimal:2',
        'date_reg' => 'datetime',
        'date_alter' => 'datetime',
    ];
}
```

### Propriedades Configuráveis

| Propriedade | Tipo | Default | Descrição |
|-------------|------|---------|-----------|
| `$table` | `string` | — | Nome da tabela |
| `$primaryKey` | `string\|array` | `'id'` | Chave primária (simples ou composta) |
| `$timestamps` | `bool` | `true` | Ativa/desativa timestamps automáticos |
| `$createdAtColumn` | `string` | `'created_at'` | Coluna de data de criação |
| `$updatedAtColumn` | `string` | `'updated_at'` | Coluna de data de alteração |
| `$timestampFormat` | `string` | `'Y-m-d H:i:s'` | Formato de data para SQL |
| `$normalizeKeys` | `?string` | `null` | Normalização: `'lower'` ou `'snake'` |
| `$casts` | `array` | `[]` | Mapeamento de casts |
| `$fillable` | `array` | `[]` | Campos permitidos em `fill()` |
| `$guarded` | `array` | `[]` | Campos bloqueados em `fill()` |
| `$hidden` | `array` | `[]` | Campos ocultos em `toArray()` |
| `$visible` | `array` | `[]` | Campos visíveis em `toArray()` (whitelist) |
| `$appends` | `array` | `[]` | Propriedades computadas a incluir |

### Uso Básico

```php
// Obter todos os IVAs
$vats = Vat::query()->get();

// Obter um IVA específico por primary key
$vat = Vat::query()->find('NOR');

// Acessar propriedades com tipagem
echo $vat->designacao;  // string
echo $vat->iva;         // float (já convertido via cast)
echo $vat->date_reg;    // Carbon (objeto de data)

// Filtrar com where
$normalVats = Vat::query()
    ->where('tipotaxa', 'NOR')
    ->where('iva', '>', 0)
    ->get();
```

### Queries Soltas vs. Modelos

**O Problema: Código Legado**

```php
// ❌ Abordagem Legada (queries soltas)
$tipodocs = $m['vendas']->listarConfNDoc([
    'compras' => 'F',
    'visiblemenu' => 'T',
]);

foreach ($tipodocs as $row) {
    // $row é um array associativo
    // Não sabemos que campos existem
    // Não há autocompletar
    // Erros de typo são silenciosos
    echo $row['codabreviado'];  // string? int? null?
    echo $row['coabreviado'];   // ❌ TYPO - retorna null silenciosamente!
}
```

**A Solução: Modelos**

```php
// ✅ Abordagem Moderna (com Modelos)
/**
 * @property-read string $codabreviado
 * @property-read string $langvar
 * @property-read bool $visiblemenu
 */
class TipoDocumento extends Model
{
    protected string $table = TABLE_NDOC;
    
    protected array $casts = [
        'visiblemenu' => 'boolean',
    ];
}

// Agora o IDE sabe tudo
$doc = TipoDocumento::query()->first();
$doc->codabreviado;  // ✓ Autocompletar funciona
$doc->coabreviado;   // ✗ IDE avisa que não existe
```

| Problema do Legado | Solução com Modelos |
|--------------------|---------------------|
| Sem tipagem | PHPDoc + typed properties |
| Sem autocompletar | IDE sugere campos |
| Erros silenciosos | Avisos em compile-time |
| Lógica espalhada | Métodos encapsulados no modelo |
| Difícil de testar | Unit tests por classe |

---

## Sistema de Casting

O sistema de casting converte automaticamente valores entre tipos PHP e tipos de base de dados.

### Casts Simples (String)

```php
protected array $casts = [
    'is_active' => 'bool',
    'price' => 'decimal:2',
    'settings' => 'json',
    'created_at' => 'datetime',
    'quantity' => 'int',
];
```

| Tipo | Descrição | Exemplo |
|------|-----------|---------|
| `int`, `integer` | Inteiro | `"123"` → `123` |
| `float`, `double` | Decimal | `"12.5"` → `12.5` |
| `bool`, `boolean` | Booleano | `"T"` → `true`, `"F"` → `false` |
| `string` | String | Sempre string |
| `array` | JSON → array | `'{"a":1}'` → `['a' => 1]` |
| `object` | JSON → stdClass | `'{"a":1}'` → `object` |
| `json` | JSON → array | Alias de `array` |
| `datetime`, `date` | Carbon instance | `"2024-01-15"` → `Carbon` |
| `timestamp` | Unix timestamp | `1705276800` → `Carbon` |
| `collection` | Array → Collection | `[1,2,3]` → `Collection` |
| `decimal:N` | Decimal com N casas | `"23.000"` → `"23.00"` |

### Casts de Classes

```php
use Og\Modules\Common\Database\Casts\JsonCast;
use Og\Modules\Common\Database\Casts\DateTimeCast;

protected array $casts = [
    'metadata' => JsonCast::class,              // Classe
    'settings' => new JsonCast(asObject: true), // Instância com config
];
```

#### Classes Disponíveis

| Classe | Descrição | Configurações |
|--------|-----------|---------------|
| `JsonCast` | JSON encode/decode | `asObject: bool` |
| `DateTimeCast` | Conversão para Carbon | `format: string` |
| `DecimalCast` | Números com precisão | `precision: int` |
| `CollectionCast` | Array para Collection | — |

### Criar Cast Customizado

```php
<?php

namespace Og\Modules\Billing\Casts;

use Og\Modules\Common\Database\Casts\CastInterface;

/**
 * Cast customizado para converter centavos em objeto Money.
 */
class MoneyCast implements CastInterface
{
    public function __construct(
        private string $currency = 'EUR'
    ) {}

    /**
     * Converter DB → PHP (get).
     */
    public function get(mixed $value, array $attributes = []): Money
    {
        return new Money(
            (int) $value, 
            $attributes['currency'] ?? $this->currency
        );
    }

    /**
     * Converter PHP → DB (set).
     */
    public function set(mixed $value, array $attributes = []): int
    {
        return $value instanceof Money 
            ? $value->cents 
            : (int) $value;
    }
}

// Uso no Model
class Order extends Model
{
    protected array $casts = [
        'total_cents' => MoneyCast::class,
        'discount_cents' => new MoneyCast('USD'),
    ];
}

// Agora podemos usar
$order->total_cents;  // Money object
$order->total_cents->format();  // "€ 150,00"
```

---

## Accessors e Mutators

Accessors (getters virtuais) e Mutators (setters com lógica) permitem transformar dados ao ler ou escrever.

### Estilo Moderno (PHP 8 Attributes) ✨

#### Accessor com `#[Computed]`

```php
use Og\Modules\Common\Database\Attributes\Computed;

class TipoDocumento extends Model
{
    /**
     * Campo calculado: nome completo do documento.
     * Método é automaticamente convertido para snake_case.
     */
    #[Computed]
    protected function nomeCompleto(): string
    {
        $nome = $this->langvar 
            ? lang($this->langvar) 
            : $this->codabreviado;
            
        return "{$nome} [{$this->codabreviado}]";
    }
}

// Uso - método é convertido para snake_case
echo $doc->nome_completo;  // "Fatura [FT]"
```

#### Mutator com `#[Mutator]`

```php
use Og\Modules\Common\Database\Attributes\Mutator;

class User extends Model
{
    /**
     * Transforma a password antes de guardar.
     */
    #[Mutator]
    protected function password(string $value): void
    {
        $this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
    }
}

$user->password = 'minhasenha';  // Automaticamente hashed!
```

#### Accessor + Mutator com `#[Accessor]`

```php
use Og\Modules\Common\Database\Attributes\Accessor;

class Order extends Model
{
    /**
     * Um método para get e set.
     */
    #[Accessor]
    protected function metadata(array $value = null): array|static
    {
        if ($value === null) {
            // Getter: retorna valor decodificado
            return json_decode($this->attributes['metadata'] ?? '{}', true);
        }
        
        // Setter: guarda como JSON
        $this->attributes['metadata'] = json_encode($value);
        return $this;
    }
}

// Uso
$order->metadata = ['key' => 'value'];  // Guarda como JSON
$data = $order->metadata;               // Retorna array
```

### Estilo Legacy (Laravel-style)

```php
class TipoDocumento extends Model
{
    // Accessor: getXxxAttribute()
    protected function getNomeCompletoAttribute(): string
    {
        return "{$this->designacao} [{$this->codabreviado}]";
    }
    
    // Mutator: setXxxAttribute()
    protected function setPasswordAttribute(string $value): void
    {
        $this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
    }
}

// Uso é o mesmo
echo $doc->nome_completo;       // Accessor
$user->password = 'senha123';   // Mutator
```

> [!TIP]
> **Passwords são hashed automaticamente!** Campos `password` e `senha` são hashed via `password_hash()` sem precisar de mutator. Só crie um mutator se precisar de lógica customizada.

---

## Timestamps Configuráveis

### Timestamps Padrão vs. Legados

```php
class Order extends Model
{
    // Padrão: created_at e updated_at
    protected bool $timestamps = true;
}

class Vat extends Model
{
    // Para tabelas legadas com date_reg/date_alter
    protected string $createdAtColumn = 'date_reg';
    protected string $updatedAtColumn = 'date_alter';
    
    // Formato (opcional, default: 'Y-m-d H:i:s')
    protected string $timestampFormat = 'Y-m-d H:i:s';
}
```

### Comportamento Automático

```php
// Os timestamps são atualizados automaticamente no save()
$vat = new Vat(['codiva' => 'NEW', 'iva' => 23]);
$vat->save();  // date_reg e date_alter são preenchidos automaticamente

$vat->iva = 25;
$vat->save();  // Apenas date_alter é atualizado
```

### Desativar Timestamps

```php
class LogEntry extends Model
{
    // Desativar permanentemente
    protected bool $timestamps = false;
}

// Ou temporariamente para uma operação:
$model->withoutTimestamps()->save();
```

---

## Mass Assignment Protection

Controlo fino sobre quais atributos podem ser preenchidos em massa para prevenir vulnerabilidades de segurança.

### Configuração

```php
class User extends Model
{
    // OPÇÃO 1: Whitelist - campos permitidos em fill()
    protected array $fillable = ['name', 'email', 'password'];
    
    // OPÇÃO 2: Blacklist - bloquear específicos (alternativa a $fillable)
    protected array $guarded = ['id', 'is_admin', 'role'];
}
```

### Métodos de Preenchimento

| Método | Respeita `$fillable` | Descrição |
|--------|---------------------|-----------|
| `fill($data)` | ✅ Sim | Ignora campos não-fillable silenciosamente |
| `fill($data, strict: true)` | ✅ Sim | Lança exceção para não-fillable |
| `fillOrFail($data)` | ✅ Sim | Alias para fill com strict |
| `forceFill($data)` | ❌ Não | Bypass total (uso interno apenas) |
| `safeUpdate($data)` | ✅ Sim | `fill()` + `save()` numa operação |

### Exemplos

```php
// Silencioso - ignora is_admin
$user->fill(['name' => 'João', 'is_admin' => true]);
$user->is_admin;  // → não alterado (permanece false)

// Modo estrito - lança exceção
try {
    $user->fillOrFail(['is_admin' => true]);
} catch (InvalidArgumentException $e) {
    // "Attribute 'is_admin' is not mass assignable"
}

// Update seguro (fill + save)
$user->safeUpdate([
    'name' => 'João', 
    'email' => 'joao@example.com'
]);

// Bypass para uso interno (cuidado!)
$user->forceFill(['is_admin' => true]);  // ⚠️ Ignora proteção
```

> [!CAUTION]
> Use `forceFill()` apenas em código de confiança (seeders, comandos internos). Nunca com dados de utilizador.

---

## Visibilidade de Atributos

Controle quais atributos aparecem na serialização e no acesso direto.

### Configuração

```php
class User extends Model
{
    // Atributos ocultos (retornam null em $model->field e excluídos de toArray())
    protected array $hidden = ['password', 'api_token', 'remember_token'];
    
    // Modo whitelist: só estes aparecem em toArray()
    protected array $visible = ['id', 'name', 'email', 'avatar'];
    
    // Atributos computados a incluir em toArray()
    // Se vazio, auto-descobre métodos com #[Computed]
    protected array $appends = ['full_name', 'profile_url'];
}
```

### Comportamento

```php
// Acesso bloqueado (retorna null)
$user->password;        // → null

// Acesso raw para uso interno
$user->getRawAttribute('password');  // → valor real

// Temporariamente visível
$user->makeVisible(['password'])->toArray();

// Temporariamente oculto
$user->makeHidden(['email'])->toArray();

// toArray() respeita hidden, visible, e inclui appends
$array = $user->toArray();
// ['id' => 1, 'name' => 'João', 'email' => '...', 'full_name' => 'João Silva']
```

### Normalização de Keys Legadas

Tabelas antigas podem ter colunas duplicadas como `SAFTTaxCode` e `safttaxcode`. O Model pode normalizar:

```php
class Vat extends Model
{
    // Opções: null (desativado), 'lower', 'snake'
    protected ?string $normalizeKeys = 'lower';
}

// SAFTTaxCode e safttaxcode → apenas safttaxcode
// Evita duplicação no toArray()
```

| Opção | Exemplo Input | Resultado |
|-------|---------------|-----------|
| `null` | `SAFTTaxCode` | `SAFTTaxCode` |
| `'lower'` | `SAFTTaxCode` | `safttaxcode` |
| `'snake'` | `SAFTTaxCode` | `s_a_f_t_tax_code` |

---

## Builder (Query Builder Moderno)

O Builder fornece uma interface fluente e tipada para construção de queries.

### Métodos de Consulta

```php
// WHERE básico
Vat::query()
    ->where('tipotaxa', 'NOR')
    ->where('iva', '>', 0)
    ->get();

// WHERE IN
Vat::query()
    ->whereIn('codiva', ['NOR', 'RED', 'ISE'])
    ->get();

// LIKE
Vat::query()
    ->like('designacao', 'normal')
    ->get();

// ORDER BY e LIMIT
Vat::query()
    ->orderBy('iva', 'desc')
    ->limit(10)
    ->get();
```

### Métodos de Conveniência

```php
// Encontrar por ID
$vat = Vat::query()->find('NOR');
$vat = Vat::query()->findOrFail('NOR');  // Lança exceção se não encontrar

// Obter primeiro resultado
$vat = Vat::query()->where('iva', 23)->first();
$vat = Vat::query()->where('iva', 23)->firstOrFail();

// Obter valores de uma coluna
$ids = Vat::query()->pluck('codiva');          // ['NOR', 'RED', 'ISE']
$names = Vat::query()->pluck('designacao', 'codiva');  // ['NOR' => 'Normal', ...]

// Obter um único valor
$rate = Vat::query()->where('codiva', 'NOR')->value('iva');  // 23.0

// Verificar existência
if (Vat::query()->where('codiva', 'NEW')->exists()) {
    // ...
}

// Contar resultados
$count = Vat::query()->where('iva', '>', 0)->count();
```

### Métodos de Criação

```php
// Encontrar ou criar nova instância (NÃO salva)
$vat = Vat::query()->firstOrNew(
    ['codiva' => 'NEW'],                        // Critério de busca
    ['iva' => 23, 'designacao' => 'Novo']       // Valores para nova instância
);  // ⚠️ Não salva automaticamente!

// Encontrar ou criar E SALVAR
$vat = Vat::query()->firstOrCreate(
    ['codiva' => 'NEW'],
    ['iva' => 23, 'designacao' => 'Novo']
);  // ✅ Salva automaticamente se criou!

// Atualizar ou criar
$vat = Vat::query()->updateOrCreate(
    ['codiva' => 'NEW'],           // Critério de busca
    ['iva' => 25]                  // Valores para atualizar/criar
);
```

### Proteção Contra Operações em Massa

> [!CAUTION]
> O Builder moderno previne UPDATE/DELETE acidentais sem WHERE.

```php
// ❌ ERRO: RuntimeException - tentativa de DELETE sem WHERE
Vat::query()->safeDelete();

// ✅ CORRETO: DELETE com WHERE
Vat::query()->where('codiva', 'OLD')->safeDelete();

// ⚠️ Bypass explícito (usar com extrema cautela!)
Vat::query()->withoutSafetyGuards()->safeDelete();

// Verificar se há constraints
if (Vat::query()->where('iva', 23)->hasWhereConstraints()) {
    // Seguro para operar
}
```

### Cache de Queries

```php
// Cache por 5 minutos (300 segundos)
$vats = Vat::query()
    ->where('tipotaxa', 'NOR')
    ->cached('vats:normal', 300)
    ->get();

// Cache com chave dinâmica
$filters = ['tipo' => 'NOR', 'ativo' => true];
$vats = Vat::query()
    ->where('tipotaxa', $filters['tipo'])
    ->cached('vats:' . md5(serialize($filters)), 600)
    ->get();
```

### Modo Híbrido para Performance

```php
class TipoDocumento extends Model
{
    // Para relatórios: retornar arrays por performance
    public static function forReport(array $filters): array
    {
        return static::query()
            ->asArray()  // ❗ Sem hydratação de objetos
            ->where('compras', $filters['compras'] ?? 'F')
            ->where('visiblemenu', 'T')
            ->cached('tiposdoc:' . md5(serialize($filters)), 300)
            ->get();
    }
    
    // Para CRUD: retornar modelo
    public static function findByCode(string $code): ?static
    {
        return static::query()
            ->where('codabreviado', $code)
            ->first();
    }
}
```

| Cenário | Abordagem |
|---------|-----------|
| CRUD de entidades | Modelo com hydratação |
| Relatórios pesados (muitos rows) | `->asArray()` |
| Agregações (SUM, COUNT) | Query direta |
| Queries com muitos JOINs | Query direta ou View |

---

## CRUD Operations

### Criar

```php
// Opção 1: new + fill + save
$vat = new Vat();
$vat->fill(['codiva' => 'NEW', 'iva' => 15]);
$vat->save();

// Opção 2: new com dados
$vat = new Vat(['codiva' => 'NEW', 'iva' => 15]);
$vat->save();

// Via query builder
$vat = Vat::query()->firstOrCreate(
    ['codiva' => 'NEW'],
    ['iva' => 15, 'designacao' => 'Novo']
);
```

### Ler

```php
// Por primary key
$vat = Vat::query()->find('NOR');
$vat = Vat::query()->findOrFail('NOR');

// Com condições
$vat = Vat::query()->where('iva', 23)->first();
$vats = Vat::query()->where('iva', '>', 0)->get();
```

### Atualizar

```php
// Modificar e salvar (só campos alterados = dirty tracking)
$vat->iva = 16;
$vat->save();  // UPDATE só do campo 'iva'

// Safe update
$vat->safeUpdate(['iva' => 17, 'designacao' => 'Novo nome']);

// Via query builder
Vat::query()->updateOrCreate(
    ['codiva' => 'NOR'],
    ['iva' => 24]
);
```

### Eliminar

```php
// Por instância
$vat->delete();

// Via query (com proteção)
Vat::query()->where('codiva', 'OLD')->safeDelete();
```

### Dirty Tracking

O modelo rastreia apenas campos modificados:

```php
$vat = Vat::query()->find('NOR');

$vat->iva = 25;
$vat->isDirty();           // true
$vat->isDirty('iva');      // true
$vat->isDirty('designacao'); // false

$vat->getDirty();          // ['iva' => 25]
$vat->getOriginal('iva');  // 23 (valor original)

$vat->save();              // UPDATE apenas 'iva'
$vat->isDirty();           // false
```

---

## Interfaces Implementadas

### QueueableEntity

Permite serializar modelos em jobs de queue de forma eficiente. Em vez de serializar todo o objeto, apenas o ID é guardado.

```php
// Dispatch de job com modelo
$order = Order::query()->find(123);
Queue::dispatch(new ProcessOrderJob($order));

// No job, o $order é automaticamente carregado do banco
class ProcessOrderJob implements JobInterface
{
    public function __construct(
        private Order $order  // Apenas o ID é serializado
    ) {}
    
    public function handle(): void
    {
        // $this->order está atualizado do banco
        $this->order->process();
    }
}
```

**Métodos disponíveis:**

| Método | Descrição |
|--------|-----------|
| `getQueueableId()` | Retorna a primary key |
| `getQueueableClass()` | Retorna `static::class` |
| `getQueueableConnection()` | Conexão do banco (opcional) |
| `resolveFromQueue($id)` | Carrega modelo do banco pelo ID |

### UrlRoutable

Permite route model binding - converter parâmetros de URL diretamente em modelos.

```php
// routes.php
Route::get('/orders/{order}', [OrderController::class, 'show']);

// Controller - $order já é o modelo carregado!
class OrderController
{
    public function show(Order $order): Response
    {
        return view('orders.show', compact('order'));
    }
}
```

**Customizar a coluna de binding:**

```php
class Order extends Model
{
    // Usar 'uuid' em vez de 'id' nas URLs
    public function getRouteKeyName(): string
    {
        return 'uuid';
    }
}

// URL: /orders/abc-123-def → busca por uuid='abc-123-def'
```

**Métodos disponíveis:**

| Método | Descrição |
|--------|-----------|
| `getRouteKey()` | Retorna o valor da route key |
| `getRouteKeyName()` | Nome da coluna (default: primary key) |
| `resolveRouteBinding($value, $field)` | Carrega modelo pelo valor da URL |

### ArrayAccess

Modelos implementam `ArrayAccess` para acesso estilo array:

```php
$user = User::query()->find(1);

// Acesso por array
echo $user['name'];      // equivalente a $user->name
$user['email'] = 'new@example.com';

// Verificação
isset($user['name']);    // true
unset($user['temp']);    // remove do array de atributos
```

---

## CLI: make:model

O comando `make:model` cria um novo Model com toda a estrutura moderna do ORM.

### Uso Básico

```bash
# Criar model básico num módulo
php og make:model User --module=Auth

# Criar model num addon
php og make:model Invoice --addon=CustomBilling

# Criar model para tabela legada
php og make:model Vat --module=Tax --legacy --normalize --pk=CodIVA --table=ivas
```

### Opções Disponíveis

| Opção | Descrição | Exemplo |
|-------|-----------|---------|
| `--module=` | Módulo destino | `--module=Billing` |
| `--addon=` | Addon destino | `--addon=CustomReport` |
| `--table=` | Nome da tabela | `--table=ivas` |
| `--pk=` | Primary key (default: id) | `--pk=CodIVA` |
| `--legacy` | Timestamps legados (`date_reg`/`date_alter`) | |
| `--normalize` | Normalização de keys (lowercase) | |
| `--force` | Sobrescrever existente | |

### Exemplo de Output

```bash
$ php og make:model Customer --module=Crm --legacy

Model created successfully: Customer
Path: Modules/Crm/Models/Customer.php
  → Legacy timestamps enabled (date_reg/date_alter)
```

**Ficheiro gerado:**

```php
<?php

namespace Og\Modules\Crm\Models;

use Og\Modules\Common\Database\Model;

/**
 * @property-read int $id
 */
class Customer extends Model
{
    protected string $table = 'customers';
    
    protected string|array $primaryKey = 'id';
    
    protected string $createdAtColumn = 'date_reg';
    protected string $updatedAtColumn = 'date_alter';
    
    protected array $casts = [
        'date_reg' => 'datetime',
        'date_alter' => 'datetime',
    ];
    
    protected array $fillable = [];
}
```

---

## Entidades Ricas (DDD)

### Modelo Anémico vs. Entidade Rica

**Modelo Anémico (Apenas Dados):**

```php
// ❌ Modelo anémico: só getters, sem lógica
class Order extends Model
{
    protected string $table = 'orders';
}

// Lógica espalhada pelo código
$order = Order::query()->find(1);
$total = $order->subtotal + $order->tax;  // Cálculo fora do modelo
if ($order->status === 'pending') { ... } // Condição fora do modelo
```

**Entidade Rica (Dados + Comportamento):**

```php
// ✅ Entidade rica: dados + lógica de negócio
class Order extends Model
{
    protected string $table = 'orders';
    
    // ========== Accessors ==========
    
    #[Computed]
    protected function total(): float
    {
        return $this->subtotal + $this->tax - $this->discount;
    }
    
    // ========== Estado ==========
    
    public function isPending(): bool
    {
        return $this->status === 'pending';
    }
    
    public function isPaid(): bool
    {
        return $this->status === 'paid';
    }
    
    public function canBeCancelled(): bool
    {
        return in_array($this->status, ['pending', 'confirmed']);
    }
    
    // ========== Comportamentos ==========
    
    public function confirm(): void
    {
        if (!$this->isPending()) {
            throw new \DomainException('Só pedidos pendentes podem ser confirmados');
        }
        
        $this->status = 'confirmed';
        $this->confirmed_at = now();
        $this->save();
    }
    
    public function cancel(string $reason): void
    {
        if (!$this->canBeCancelled()) {
            throw new \DomainException('Este pedido não pode ser cancelado');
        }
        
        $this->status = 'cancelled';
        $this->cancellation_reason = $reason;
        $this->cancelled_at = now();
        $this->save();
    }
    
    public function applyDiscount(float $percentage): void
    {
        if ($percentage < 0 || $percentage > 100) {
            throw new \InvalidArgumentException('Desconto inválido');
        }
        
        $this->discount = $this->subtotal * ($percentage / 100);
    }
}
```

### Benefícios

1. **Tell, Don't Ask**: Pedimos ao objeto para fazer, em vez de perguntar estado
2. **Invariantes protegidas**: Regras de negócio no lugar certo
3. **Código mais legível**: `$order->confirm()` vs `$order->status = 'confirmed'`
4. **Testável**: Podemos testar `Order::confirm()` isoladamente

### Evolução Gradual para DDD

```
┌─────────────────────────────────────────────────────────────────┐
│                    EVOLUÇÃO GRADUAL                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Etapa 1: Arrays        →  Etapa 2: Modelos    →  Etapa 3: DDD  │
│  (Atual legado)            (Atual moderno)         (Futuro)      │
│                                                                  │
│  $row['status']         →  $order->status      →  $order->status│
│                            $order->isPaid()       (Value Object) │
│                                                                  │
│  $row['total']          →  $order->total       →  $order->total │
│                            (accessor)             (Money VO)     │
│                                                                  │
│  Lógica na view         →  Lógica no modelo    →  Domain Events │
│                                                   Aggregates     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### Value Objects (Exemplo Futuro)

```php
// Value Object para dinheiro
final class Money
{
    public function __construct(
        public readonly float $amount,
        public readonly string $currency = 'EUR'
    ) {}
    
    public function add(Money $other): Money
    {
        return new Money($this->amount + $other->amount, $this->currency);
    }
    
    public function format(): string
    {
        return number_format($this->amount, 2) . ' ' . $this->currency;
    }
}

// Uso no modelo
class Order extends Model
{
    protected function getTotalAttribute(): Money
    {
        return new Money($this->attributes['total']);
    }
}

$order->total->format();  // "150.00 EUR"
```

---

## OGDB Forge (Schema Builder)

O OGDB Forge fornece uma API de alto nível para operações de definição e evolução de esquema de banco de dados (DDL).

### Visão Geral

A OGDB Forge permite:

- Criar e remover bases de dados e tabelas
- Definir colunas com tipos e restrições
- Definir chaves primárias e índices
- Definir chaves estrangeiras
- Aplicar padrões de campos (timestamps, soft deletes, user tracking)

### Criar Tabela

```php
$this->dbforge
    ->add_field([
        'id' => ['type' => 'INT', 'constraint' => 11, 'auto_increment' => true],
        'user_id' => ['type' => 'INT', 'constraint' => 11, 'null' => false],
        'email' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
        'created_at' => ['type' => 'DATETIME', 'null' => true, 'default' => RawSql::currentTimestamp()],
    ])
    ->primary('id')
    ->unique('email')
    ->index(['user_id', 'created_at'])
    ->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE')
    ->create_table('user_emails', true);  // true = IF NOT EXISTS
```

### Tipos de Campos

| Atributo | Tipo | Descrição |
|----------|------|-----------|
| `type` | `string` | VARCHAR, INT, BIGINT, DATETIME, TEXT, etc. |
| `constraint` | `int` | Tamanho/precisão (255, 11, etc.) |
| `null` | `bool` | Permite NULL |
| `default` | `mixed` | Valor padrão (string, numérico, null, RawSql) |
| `unsigned` | `bool` | Para tipos numéricos |
| `auto_increment` | `bool` | Para chaves numéricas incrementais |
| `unique` | `bool` | Índice único |
| `after` | `string` | Posicionar coluna após outra |

### Chaves Primárias e Índices

```php
// Chave primária simples
$this->dbforge->primary('id');

// Chave primária composta
$this->dbforge->primary(['order_id', 'product_id']);

// Índice regular
$this->dbforge->index('user_id');
$this->dbforge->index(['user_id', 'created_at'], 'idx_user_created');

// Índice único
$this->dbforge->unique('email');
$this->dbforge->unique(['email'], 'uk_users_email');
```

### Chaves Estrangeiras

```php
// Durante a criação da tabela
$this->dbforge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE');

// Após criar tabela (adicionar coluna + FK)
$this->dbforge->addColumnWithForeignKey(
    'posts',
    ['category_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true]],
    'category_id',
    'categories',
    'id',
    'SET NULL',    // ON DELETE
    'CASCADE'      // ON UPDATE
);
```

### Adicionar Colunas

```php
$this->dbforge->addColumns(
    'orders',
    [
        'status' => ['type' => 'VARCHAR', 'constraint' => 30, 'default' => 'pending', 'after' => 'total'],
        'external_id' => ['type' => 'VARCHAR', 'constraint' => 64]
    ],
    null,            // $_after
    true,            // verifyIfExists (idempotente)
    [
        'status' => 'index',
        'external_id' => 'unique'
    ]
);
```

### Modificar e Remover

```php
// Modificar coluna
$this->dbforge->modify_column('users', [
    'email' => ['name' => 'email', 'type' => 'VARCHAR', 'constraint' => 320, 'null' => true]
]);

// Remover coluna
$this->dbforge->drop_column('users', 'legacy_flag');

// Remover tabela
$this->dbforge->drop_table('temp_data', true);  // true = IF EXISTS

// Renomear tabela
$this->dbforge->rename_table('old_name', 'new_name');
```

### Padrões de Campos (Atalhos)

```php
// Timestamps: date_reg e date_alter
$this->dbforge->timestamps();

// Soft deletes: deleted_at + índice
$this->dbforge->softDeletes();

// User tracking: oper_reg e oper_alter com FKs e índices
$this->dbforge->userTracking();

// Ou via addPattern
$this->dbforge->addPattern('timestamps');
$this->dbforge->addPattern('soft_deletes');
$this->dbforge->addPattern('user_tracking');
```

### Exemplo Completo de Migração

```php
<?php

namespace Database\Migrations;

use Og\Modules\Database\Migration;
use Og\Modules\Common\Database\RawSql;

class CreateProductsTable extends Migration
{
    public function up(): void
    {
        $this->dbforge
            ->add_field([
                'id' => ['type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, 'auto_increment' => true],
                'sku' => ['type' => 'VARCHAR', 'constraint' => 64, 'null' => false],
                'name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
                'description' => ['type' => 'TEXT', 'null' => true],
                'price_cents' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'null' => false],
                'stock' => ['type' => 'INT', 'constraint' => 11, 'default' => 0],
                'category_id' => ['type' => 'INT', 'constraint' => 11, 'null' => true],
                'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1],
            ])
            ->primary('id')
            ->unique('sku')
            ->index('category_id')
            ->index('is_active')
            ->addForeignKey('category_id', 'categories', 'id', 'SET NULL', 'CASCADE')
            ->timestamps()          // date_reg, date_alter
            ->softDeletes()         // deleted_at
            ->create_table('products', true);
    }
    
    public function down(): void
    {
        $this->dbforge->drop_table('products', true);
    }
}
```

### Checklist de Migração Segura

**Antes de aplicar:**
- [ ] Rodou localmente em base de testes?
- [ ] `addColumns(..., verifyIfExists: true)` quando apropriado?
- [ ] Índices/uniqueness não conflitam com dados existentes?
- [ ] FK aponta para tabela/coluna correta e engine suporta FKs?

**Depois de aplicar:**
- [ ] Índices foram criados? (`SHOW INDEX` / `DESC tabela`)
- [ ] Defaults/nullable conforme esperado?
- [ ] Aplicou em staging antes de produção?

### Troubleshooting

| Erro | Solução |
|------|---------|
| Erro de sintaxe/driver | Verifique se tipos/atributos existem no SGBD |
| Não consigo recriar coluna existente | Use `verifyIfExists: true` |
| FK falha | Confirme tipos e collation compatíveis |
| Índice duplicado | Use nomes determinísticos (`uk_`/`idx_`) |

---

## Modernização: Legado vs. Moderno

Esta secção demonstra as vantagens da arquitetura moderna através de um exemplo prático.

### Sumário Executivo

| Métrica | Legado | Moderno | Ganho |
|---------|--------|---------|-------|
| **Linhas de código** | ~170 | ~80 | -53% |
| **Testabilidade** | ❌ Difícil | ✅ Unit tests | 100% |
| **Reutilização** | ❌ Copy/paste | ✅ Components | Alta |
| **Type safety** | ❌ Nenhuma | ✅ Forte | 100% |
| **API documentation** | ❌ Manual | ✅ OpenAPI | Automática |
| **Manutenção** | ❌ Complexa | ✅ Simples | Significativa |

### O Problema: Código Legado

```php
// ❌ LEGADO: ~170 linhas, sem tipagem, difícil de testar

public function list_report_handling($request)
{
    global $db;  // 🔴 Dependência global
    $search = [];

    // 🔴 Parsing manual de cada parâmetro
    if (Funcs::not_empty_verify($request['date_ini'])) {
        $search['where']['fa.date_reg >='] = Funcs::dateFormat($_REQUEST['date_ini']) . ' 00:00:00';
    }
    // ... +50 linhas de condicionais similares ...

    // 🔴 Query monolítica com 40+ colunas
    $list = $db->select([...])
        ->from('ams_flights_attendance fa')
        ->join(...)
        ->get()->result();

    // 🔴 Formatação manual em loop
    foreach ($list as $k => $v) {
        $list[$k]['total_f'] = Funcs::number_format($v['total']);
    }

    return $list;  // 🔴 Array sem tipagem
}
```

**Problemas identificados:**

| Categoria | Problemas |
|-----------|-----------|
| **Segurança** | Acesso direto a `$_REQUEST`, sem validação de tipos |
| **Manutenção** | 170+ linhas, queries misturadas com lógica |
| **Testabilidade** | Dependências globais, impossível mockar |
| **Reutilização** | Copy/paste entre relatórios |

### A Solução: Arquitetura Moderna

**Estrutura de diretórios:**

```
Modules/Ams/Handling/
├── Models/
│   └── Handling.php              # Entidade de domínio
├── Actions/
│   └── HandlingList.php          # Caso de uso isolado
├── DTOs/
│   ├── HandlingFilterDTO.php     # Input validado
│   └── HandlingOutputDTO.php     # Output formatado
├── Policies/
│   └── HandlingPolicy.php        # Autorização
├── Controllers/
│   └── Api/
│       └── HandlingController.php
└── Routes/
    └── Api.php                   # Rotas RESTful
```

**1. Model:**

```php
class Handling extends Model
{
    protected string $table = 'ams_flights_attendance';
    
    protected array $casts = [
        'entry_date' => 'datetime',
        'departure_date' => 'datetime',
    ];
    
    #[Computed]
    protected function duration(): string
    {
        return $this->entry_date?->diffForHumans($this->departure_date, true) ?? '-';
    }
    
    public function canBeEdited(): bool
    {
        return in_array($this->status?->type, ['pending', 'in_progress']);
    }
}
```

**2. Filter DTO:**

```php
final readonly class HandlingFilterDTO
{
    public function __construct(
        public ?array $statusIds = null,
        public ?array $airportIds = null,
        public ?\DateTimeInterface $dateFrom = null,
        public ?\DateTimeInterface $dateTo = null,
        public ?int $limit = 100,
    ) {}
    
    public static function fromRequest(Request $request): self
    {
        return new self(
            statusIds: $request->array('status'),
            airportIds: $request->array('airports'),
            dateFrom: $request->date('date_from'),
            dateTo: $request->date('date_to'),
            limit: $request->int('limit', 100),
        );
    }
}
```

**3. Action:**

```php
readonly class HandlingList
{
    public function execute(HandlingFilterDTO $filters, array $relations = []): array
    {
        $query = Handling::query();
        
        $this->applyFilters($query, $filters);
        
        return $query
            ->with($relations)
            ->orderBy($filters->orderBy, $filters->orderDir)
            ->limit($filters->limit)
            ->get()
            ->toArray();
    }
}
```

**4. Controller:**

```php
class HandlingController extends Controller
{
    /**
     * @OA\Get(path="/api/v2/ams/handling", summary="List flight attendances")
     */
    public function index(Request $request, HandlingList $action): Response
    {
        $this->authorize('viewAny');
        
        $filters = HandlingFilterDTO::fromRequest($request);
        $handlings = $action->execute($filters, $this->defaultRelations);
        
        return response()->ogJson([
            'data' => HandlingOutputDTO::collection($handlings),
        ]);
    }
}
```

### Comparativo de Request/Response

**Legado:**
```http
GET /ajaxserver?action=ams&option=list_report_handling&status=1,2&date_ini=01/12/2024
Cookie: PHPSESSID=abc123
```

```json
[{"id": 1, "customer_f": "[CLI001] Empresa", "total_f": "1.234,56"}]
```

**Moderno:**
```http
GET /api/v2/ams/handling?status[]=1&status[]=2&date_from=2024-12-01
Authorization: Bearer eyJ0eXAiOiJKV1Q...
```

```json
{
  "data": [{
    "id": 1,
    "entry_date": "2024-12-01T08:30:00+00:00",
    "duration": "6 hours",
    "customer": {"id": "CLI001", "name": "Empresa ABC"},
    "totals": {"grand_total": "1 234,56 €"}
  }],
  "meta": {"total": 1}
}
```

### Benefícios Estratégicos

```mermaid
graph LR
    A[Novo Relatório] --> B{Arquitetura}
    B -->|Legada| C["Copy/Paste + Adaptação"]
    B -->|Moderna| D["Reutilizar Components"]
    C --> E[2-3 dias]
    D --> F[2-4 horas]
```

| Tipo de Erro | Legado | Moderno |
|--------------|--------|---------|
| Typo em campo | Runtime (silencioso) | IDE/Compile-time |
| SQL Injection | Possível | Prevenido |
| Tipo incorreto | Runtime | Type error |
| Campo esquecido | Null silencioso | Required property |

> [!IMPORTANT]
> **ROI Estimado**: Para cada hora investida na migração, economiza-se 3-5 horas em manutenção futura e debugging.

---

## Referências

### Classes Base
- [Model.php](file:///Modules/Common/Database/Model.php) — Classe base para modelos
- [Builder.php](file:///Modules/Common/Database/Builder.php) — Query builder moderno

### Traits
- [Castable.php](file:///Modules/Common/Database/Traits/Castable.php) — Sistema de casting
- [HasAttributes.php](file:///Modules/Common/Database/Traits/HasAttributes.php) — Manipulação de atributos
- [HasAccessors.php](file:///Modules/Common/Database/Traits/HasAccessors.php) — PHP 8 Attributes para accessors
- [TracksChanges.php](file:///Modules/Common/Database/Traits/TracksChanges.php) — Dirty tracking
- [Persistable.php](file:///Modules/Common/Database/Traits/Persistable.php) — CRUD operations
- [HasTimestamps.php](file:///Modules/Common/Database/Traits/HasTimestamps.php) — Timestamps configuráveis
- [HashesPasswords.php](file:///Modules/Common/Database/Traits/HashesPasswords.php) — Hash automático de passwords

### Attributes (PHP 8)
- [Computed.php](file:///Modules/Common/Database/Attributes/Computed.php) — Accessors
- [Mutator.php](file:///Modules/Common/Database/Attributes/Mutator.php) — Mutators
- [Accessor.php](file:///Modules/Common/Database/Attributes/Accessor.php) — Combined get/set

### Casts
- [CastInterface.php](file:///Modules/Common/Database/Casts/CastInterface.php) — Interface para casts
- [JsonCast.php](file:///Modules/Common/Database/Casts/JsonCast.php) — JSON encode/decode
- [DateTimeCast.php](file:///Modules/Common/Database/Casts/DateTimeCast.php) — Conversão para Carbon
- [DecimalCast.php](file:///Modules/Common/Database/Casts/DecimalCast.php) — Números com precisão
- [CollectionCast.php](file:///Modules/Common/Database/Casts/CollectionCast.php) — Array para Collection

### Interfaces
- [QueueableEntity.php](file:///Modules/Common/Database/Interfaces/QueueableEntity.php) — Queue serialization
- [UrlRoutable.php](file:///Modules/Common/Database/Interfaces/UrlRoutable.php) — Route model binding

### Exemplo Real
- [Vat.php](file:///Modules/Tax/Models/Vat.php) — Modelo de IVA
