# Modelos de Dados (Data Models)

Este documento explica o conceito de **Modelos** no OfficeGest, as suas vantagens e desvantagens em comparação com queries SQL soltas, e como podem evoluir para entidades de domínio ricas.

---

## 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**.

### Exemplo: Modelo `Vat` (IVA)

```php
<?php

namespace Og\Modules\Tax\Models;

use Og\Modules\Common\Database\Model;

/**
 * @property-read string $codiva
 * @property-read string $designacao
 * @property-read float $iva
 * @property-read string $safttaxcode
 */
class Vat extends Model
{
    protected string $table = TABLE_IVAS;
    protected string|array $primaryKey = 'CodIVA';

    protected array $casts = [
        'iva' => 'decimal:2',
        'date_reg' => 'datetime',
        'date_alter' => 'datetime',
    ];
}
```

### Uso Básico

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

// Obter um IVA específico
$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 com Queries Soltas

Considere um trecho típico do relatório `_views/relatorios/vendas/lucrodocumento.php`:

```php
// Abordagem Legada (queries soltas)
$tipodocs = $m['vendas']->listarConfNDoc([
    'compras' => 'F',
    'visiblemenu' => 'T',
    'tipoterc' => 'C',
    'recibo' => 'F',
    'actualizacc' => '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['langvar'];       // existe sempre?
}
```

### Problemas desta Abordagem

| Problema | Descrição |
|----------|-----------|
| **Sem tipagem** | `$row['campo']` não tem type hints |
| **Sem autocompletar** | IDE não sabe que campos existem |
| **Erros silenciosos** | Typos como `$row['coabreviado']` retornam `null` |
| **Lógica espalhada** | Formatação, validação, cálculos repetidos em várias views |
| **Difícil de testar** | Sem classe, sem unit tests |
| **Sem encapsulamento** | Regras de negócio misturadas com apresentação |

---

## Vantagens dos Modelos

### 1. **Tipagem Forte e Autocompletar**

Com PHPDoc ou typed properties, a IDE sugere os campos:

```php
/**
 * @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
```

### 2. **Casting Automático**

Valores são convertidos automaticamente:

```php
protected array $casts = [
    'iva' => 'decimal:2',      // "23.000" → "23.00"
    'date_reg' => 'datetime',   // "2024-01-15" → Carbon
    'activo' => 'boolean',      // "T" → true
    'metadata' => 'array',      // JSON string → array PHP
];
```

### 3. **Accessors (Getters Virtuais)**

Campos calculados que parecem propriedades. Suportamos **dois estilos**:

#### Estilo Moderno (PHP 8 Attributes) ✨

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

class TipoDocumento extends Model
{
    #[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]"
```

#### Estilo Legacy (Laravel-style)

```php
class TipoDocumento extends Model
{
    // Accessor: getXxxAttribute()
    protected function getNomeCompletoAttribute(): string
    {
        return "{$this->designacao} [{$this->codabreviado}]";
    }
}

// Uso
echo $doc->nome_completo;  // "Fatura [FT]"
```

### 4. **Mutators (Setters com Lógica)**

Transformar dados antes de guardar. Também suporta **dois estilos**:

#### Estilo Moderno (PHP 8 Attributes) ✨

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

class User extends Model
{
    #[Mutator]
    protected function password(string $value): void
    {
        $this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
    }
}

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

#### Estilo Combinado (Accessor + Mutator)

```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 User extends Model
{
    protected function setPasswordAttribute(string $value): void
    {
        $this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
    }
}

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

> [!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.

### 5. **Timestamps Configuráveis**

Por padrão, os modelos usam `created_at` e `updated_at`. Para tabelas legadas com `date_reg`/`date_alter`:

```php
class Vat extends Model
{
    // Configurar colunas de timestamp para tabelas legadas
    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';
}

// 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
{
    protected bool $timestamps = false;  // Não atualizar timestamps
}

// Ou temporariamente:
$model->withoutTimestamps()->save();
```

### 6. **Encapsulamento de Regras de Negócio**

Métodos que encapsulam lógica do domínio:

```php
class Vat extends Model
{
    public function isExempt(): bool
    {
        return $this->iva === 0.0;
    }
    
    public function calculateTax(float $baseValue): float
    {
        return round($baseValue * ($this->iva / 100), 2);
    }
    
    public function getDisplayName(): string
    {
        return $this->isExempt() 
            ? "{$this->designacao} (Isento)"
            : "{$this->designacao} ({$this->iva}%)";
    }
}

// Uso
$vat = Vat::query()->find('NOR');
$tax = $vat->calculateTax(100.00);  // 23.00
echo $vat->getDisplayName();        // "Normal (23%)"
```

### 7. **CRUD Simplificado**

```php
// Criar
$vat = new Vat();
$vat->fill(['codiva' => 'NEW', 'iva' => 15]);
$vat->save();

// Atualizar (só campos alterados)
$vat->iva = 16;
$vat->save();  // UPDATE só do campo 'iva'

// Eliminar
$vat->delete();
```

### 8. **Métodos de Conveniência do Builder**

```php
// Obter valores de uma única 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

// Encontrar ou criar novo
$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
);
```

### 9. **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
}
```

### 10. **Controlo de Visibilidade (`$hidden`, `$visible`, `$appends`)**

O Model permite controlar quais atributos aparecem na serialização e no acesso direto:

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

// 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();
```

### 11. **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 | Resultado |
|-------|---------|-----------|
| `null` | `SAFTTaxCode` | `SAFTTaxCode` |
| `'lower'` | `SAFTTaxCode` | `safttaxcode` |
| `'snake'` | `SAFTTaxCode` | `s_a_f_t_tax_code` |

### 12. **Mass Assignment Protection**

Controlo fino sobre quais atributos podem ser preenchidos em massa:

```php
class User extends Model
{
    // Campos permitidos em fill()
    protected array $fillable = ['name', 'email', 'password'];
    
    // Ou bloquear específicos (alternativa a $fillable)
    protected array $guarded = ['id', 'is_admin'];
}
```

#### Métodos de Preenchimento

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

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

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

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

---

## Desvantagens e Quando NÃO Usar

| Situação | Recomendação |
|----------|--------------|
| **Queries muito complexas** | Manter query builder direta |
| **Relatórios com muitos JOINs** | Usar `->asArray()` ou raw SQL |
| **Performance crítica** | Evitar hydratação de milhares de objetos |
| **Queries ad-hoc/únicas** | Não vale criar modelo só para uma query |

### Exemplo: Quando Usar Query Direta

```php
// Relatório complexo com agregações - melhor manter query direta
$stats = $db->select('
    SUM(TotalSIVA) as total,
    COUNT(*) as count,
    AVG(MargemLucro) as avg_margin
')
->from('view_dump_documents')
->where('Data >=', $startDate)
->where('Data <=', $endDate)
->get()
->row();
```

### Padrão Híbrido (Recomendado)

```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();
    }
}
```

---

## De Modelo Anémico para 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 ==========
    
    protected function getTotalAttribute(): 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 da Entidade Rica

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 para DDD (Domain-Driven Design)

Os modelos atuais podem evoluir gradualmente para um design mais rico:

```
┌─────────────────────────────────────────────────────────────────┐
│                    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     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### Exemplo de Value Object (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"
```

---

## Comparação: Relatório com Queries vs. com Modelos

### Antes (Query Solta)

```php
// _views/relatorios/vendas/lucrodocumento.php
$tipodocs = $m['vendas']->listarConfNDoc([
    'compras' => 'F', 
    'visiblemenu' => 'T'
]);

foreach ($tipodocs as $row) {
    $nome = empty($row['langvar']) 
        ? $row['codabreviado'] 
        : $l->r($row['langvar']);
    echo "<option value=\"{$row['codabreviado']}\">{$nome}</option>";
}
```

### Depois (Com Modelo)

```php
// Modules/Sales/Models/DocumentType.php
class DocumentType extends Model
{
    protected string $table = TABLE_NDOC;
    
    public function getDisplayName(): string
    {
        return $this->langvar 
            ? lang($this->langvar) 
            : $this->codabreviado;
    }
    
    public static function forSalesReport(): Collection
    {
        return static::query()
            ->where('compras', 'F')
            ->where('visiblemenu', 'T')
            ->cached('doctypes:sales', 600)
            ->get();
    }
}

// Na view (mais limpa)
$tipodocs = DocumentType::forSalesReport();

foreach ($tipodocs as $doc) {
    echo "<option value=\"{$doc->codabreviado}\">{$doc->getDisplayName()}</option>";
}
```

---

## Resumo: Quando Usar Cada Abordagem

| Cenário | Abordagem Recomendada |
|---------|----------------------|
| CRUD de entidades | **Modelo** |
| Leitura simples com filtros | **Modelo** |
| Relatórios pesados (muitos rows) | **Query + asArray()** |
| Agregações (SUM, COUNT, AVG) | **Query direta** |
| Queries com muitos JOINs | **Query direta ou View** |
| Lógica de negócio reutilizável | **Modelo com métodos** |
| Dados temporários/calculados | **Query direta** |

---

## Próximos Passos

1. **Criar modelos para entidades principais** (Clientes, Artigos, Documentos)
2. **Adicionar PHPDoc** para autocompletar nas propriedades
3. **Encapsular lógica comum** em métodos do modelo
4. **Usar `->cached()`** para queries frequentes
5. **Manter híbrido**: modelos para CRUD, queries para relatórios pesados

---

## Interfaces Implementadas

### QueueableEntity

Permite serializar modelos em jobs de queue de forma eficiente. Em vez de serializar todo o objeto, apenas o ID é guardado. O modelo é re-hidratado quando o job executa.

```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:**
- `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'));
    }
}
```

**Métodos disponíveis:**
- `getRouteKey()` — retorna o valor da route key
- `getRouteKeyName()` — retorna o nome da coluna (default: primary key)
- `resolveRouteBinding($value, $field)` — carrega modelo pelo valor da URL

**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'
```

---

## CLI Commands

### `make:model`

Cria um novo Model com toda a estrutura moderna do ORM.

```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

| 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)
```

---

## Sistema de Casts

O ORM suporta casting automático de atributos.

### Casts Simples (string)

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

| Tipo | Descrição |
|------|-----------|
| `int`, `integer` | Inteiro |
| `float`, `double` | Decimal |
| `bool`, `boolean` | Booleano |
| `string` | String |
| `array` | JSON → array |
| `object` | JSON → stdClass |
| `json` | JSON → array |
| `datetime`, `date` | Carbon instance |
| `timestamp` | Unix timestamp |
| `collection` | Array → Collection |
| `decimal:N` | Decimal com N casas |

### Casts Customizados (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
];
```

#### Classes Disponíveis

| Classe | Descrição |
|--------|-----------|
| [JsonCast](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Casts/JsonCast.php) | JSON encode/decode |
| [DateTimeCast](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Casts/DateTimeCast.php) | Conversão para Carbon |
| [DecimalCast](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Casts/DecimalCast.php) | Números com precisão |
| [CollectionCast](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Casts/CollectionCast.php) | Array para Collection |

#### Criar Cast Customizado

```php
<?php

namespace Og\Modules\MyModule\Casts;

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

class MoneyCast implements CastInterface
{
    public function get(mixed $value, array $attributes = []): Money
    {
        return new Money((int) $value, $attributes['currency'] ?? 'EUR');
    }

    public function set(mixed $value, array $attributes = []): int
    {
        return $value instanceof Money ? $value->cents : (int) $value;
    }
}

// Uso no Model
protected array $casts = [
    'total' => MoneyCast::class,
];
```

---

## Referências

### Classes Base
- [Model.php](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Model.php) - Classe base
- [Builder.php](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Builder.php) - Query builder moderno

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

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

### Interfaces
- [QueueableEntity.php](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Interfaces/QueueableEntity.php) - Queue serialization
- [UrlRoutable.php](file:///home/paulo/www/guisoft/ogdevel/Modules/Common/Database/Interfaces/UrlRoutable.php) - Route model binding

### Exemplo Real
- [Vat.php](file:///home/paulo/www/guisoft/ogdevel/Modules/Tax/Models/Vat.php) - Modelo de IVA
