OG Framework
OG Framework Documentação
Voltar para Documentação

System

Database System

ORM moderno e Schema Builder para OfficeGest — Modelos tipados, Query Builder expressivo, Casting automático e Migrações.

Visão Geral

O Sistema de Base de Dados do OfficeGest fornece uma camada moderna de ORM que encapsula o query builder legado existente (CodeIgniter 2).

📌 Arquitetura

O Model e o Builder não substituem o query builder legado — eles o encapsulam. Internamente, todas as queries continuam a usar o OGDB_query_builder do CodeIgniter 2. A integração com o legado é isolada num adaptador interno (LegacyQueryAdapter), mantendo a API do Builder moderna e consistente. O que adicionamos é uma camada de conveniência com tipagem forte, PHPDoc, PHP 8 Attributes, casting automático e proteções contra operações acidentais.

Modelos Tipados

PHPDoc e autocompletar para todas as propriedades.

Query Builder

Interface fluente com proteções contra operações em massa.

Casting Automático

Conversão automática entre tipos PHP e tipos de banco.

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
│   ├── TracksChanges.php   # Dirty tracking
│   ├── Persistable.php     # CRUD operations
│   └── HasTimestamps.php   # Timestamps configuráveis
├── Casts/
│   ├── JsonCast.php        # JSON encode/decode
│   ├── DateTimeCast.php    # Carbon conversion
│   └── DecimalCast.php     # Números com precisão
└── Interfaces/
    ├── QueueableEntity.php # Serialização em jobs
    └── UrlRoutable.php     # Route model binding

Modelos de Dados

Um Modelo é uma classe PHP que representa uma tabela da base de dados. Em vez de arrays associativos, trabalhamos com objetos tipados.

Estrutura Básica de um Modelo

<?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 Carbon $date_reg     Data de registo
 */
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',
    ];
}
Propriedade Tipo Default Descrição
$tablestringNome da tabela
$primaryKeystring|array'id'Chave primária (simples ou composta)
$timestampsbooltrueAtiva timestamps automáticos
$castsarray[]Mapeamento de casts
$fillablearray[]Campos permitidos em fill()
$hiddenarray[]Campos ocultos em toArray()

Uso Básico

// Obter todos os IVAs
$vats = Vat::all();

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

	// Acessar propriedades com tipagem
	echo $vat->designacao;  // string
	echo $vat->iva;         // string (decimal:2, ex.: "23.00")
	echo $vat->date_reg;    // Carbon (objeto de data)

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

✨ Sintaxe Flexível

O Model suporta várias formas de interação. A forma estática é a recomendada para uso comum:

// ✅ RECOMENDADO: Métodos estáticos (mais limpo e direto)
$vats = Vat::where('iva', '>', 0)->get();
$vat = Vat::find('NOR');
$vat = Vat::findOrFail('NOR');
$count = Vat::where('tipotaxa', 'NOR')->count();

	// 🔧 Via query() - acesso ao Builder moderno (API camelCase)
	$results = Vat::query()
	    ->where('iva', '>', 0)
	    ->asArray()
	    ->get();
	
	// Escape hatch: acesso ao query builder legado (CodeIgniter 2)
	// Use apenas quando precisares de métodos ainda não mapeados no wrapper moderno.
	$legacy = Vat::query()->unwrap();
	$legacy->where_in('codiva', ['NOR', 'RED'])->get();

// Via instância (para casos específicos)
$model = new Vat();
$vats = $model->newQuery()->where('iva', '>', 0)->get();

💡 Quando usar query()?

Use Model::query() para obter um Builder moderno (API camelCase) e encadear queries. Se precisares de um método específico do CodeIgniter 2 que ainda não existe no wrapper moderno, usa ->unwrap() para acessar o OGDB_query_builder diretamente.

Sistema de Casting

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

Casts Simples (String)

protected array $casts = [
    'is_active' => 'bool',       // "T" → true
    'price' => 'decimal:2',      // "23.000" → "23.00"
    'settings' => 'json',        // JSON string → array
    'created_at' => 'datetime',  // String → Carbon
    'quantity' => 'int',         // "123" → 123
];
Tipo Descrição Exemplo
int, integerInteiro"123" → 123
float, doubleDecimal"12.5" → 12.5
bool, booleanBooleano"T" → true
array, jsonJSON → array'{"a":1}' → ['a' => 1]
datetime, dateCarbon instance"2024-01-15" → Carbon
timestampUnix timestamp1705276800 → Carbon
collectionArray → Collection[1,2,3] → Collection
decimal:NDecimal com N casas"23.000" → "23.00"

Cast Customizado

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_cents' => MoneyCast::class,
];

Accessors e Mutators

Accessors (getters virtuais) e Mutators (setters com lógica) transformam dados ao ler ou escrever. Suporta dois estilos: PHP 8 Attributes e Laravel-style.

✨ Estilo Moderno (PHP 8 Attributes)

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)

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

// Uso é o mesmo
echo $doc->nome_completo;  // "Fatura [FT]"

Mutator (#[Mutator])

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!

💡 Dica

Campos password e senha são hashed automaticamente via password_hash() sem precisar de mutator!

Builder (Wrapper do Query Builder)

O Builder fornece uma interface fluente e tipada que encapsula o OGDB_query_builder legado do CodeIgniter 2. Internamente, todos os métodos delegam para o query builder original, mas com camadas adicionais de segurança e conveniência.

ℹ️ API camelCase

No wrapper moderno, priorizamos métodos em camelCase (ex.: whereIn). Chamadas diretas a métodos snake_case do legado (ex.: where_in, order_by) devem ser feitas via ->unwrap().

Métodos de Consulta

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

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

	// LIMIT
	Vat::query()->limit(10)->get();

Métodos de Conveniência

// Encontrar por ID
$vat = Vat::find('NOR');
$vat = Vat::findOrFail('NOR');

// Obter valores de uma coluna
$ids = Vat::pluck('codiva');  // ['NOR', 'RED', 'ISE']

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

Métodos de Criação

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

// Encontrar ou criar E SALVAR
$vat = Vat::firstOrCreate(
    ['codiva' => 'NEW'],
    ['iva' => 23, 'designacao' => 'Novo']
);

// Atualizar ou criar
$vat = Vat::updateOrCreate(
    ['codiva' => 'NEW'],
    ['iva' => 25]
);

⚠️ Proteção Contra Operações em Massa

O Builder previne UPDATE/DELETE acidentais sem WHERE por padrão. Para operações em massa, o bypass precisa ser explícito.

// ❌ ERRO: RuntimeException - sem WHERE
	Vat::query()->delete();
	
	// ✅ CORRETO: DELETE com WHERE
	Vat::where('codiva', 'OLD')->delete();
	
	// ⚠️ Bypass explícito (usar com extrema cautela)
	Vat::query()->withoutSafetyGuards()->delete();
	
	// ✅ Segurança extra: whereIn([]) representa conjunto vazio e vira no-op
	$affected = Vat::query()->whereIn('codiva', [])->update(['iva' => 0]); // 0

Cache de Queries e Modo Híbrido

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

	// Modo híbrido para relatórios (sem hidratar Models)
	// Usa asArray() para receber arrays
	$data = Vat::query()->asArray()->get();

CRUD Operations

CREATE

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

READ

$vat = Vat::find('NOR');
$vats = Vat::where('iva', '>', 0)->get();

UPDATE

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

// Ou via update
$vat->update(['iva' => 17]);

DELETE

$vat->delete();

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

Dirty Tracking (Mudanças em Memória)

O Model mantém um snapshot dos valores originais vindos do banco. Isso permite detectar alterações em memória e garantir que o save() atualize apenas os campos realmente modificados.

Estados

$vat = Vat::find('NOR');

// Modelo vindo do banco
$vat->exists(); // true

// Snapshot do que veio do banco
$vat->getOriginal(); // array

// Mudanças desde o último syncOriginal()
$vat->getDirty(); // array
$vat->isDirty();  // bool
$vat->isClean();  // bool

Mudanças e Reversão

$vat = Vat::find('NOR');

$vat->iva = '9.99';

// Lista apenas os campos alterados
$vat->getDirty(); // ['iva' => '9.99']

// Mudanças com original + current
$vat->getChanges(); // ['iva' => ['original' => '23.00', 'current' => '9.99']]

// Desfaz alterações em memória
$vat->discardChanges();

💡 Por que isso importa?

Em produção, isso reduz writes desnecessários, evita atualizar colunas sem intenção e simplifica auditoria (só muda o que foi alterado).

Mass Assignment Protection

class User extends Model
{
    // Whitelist - campos permitidos em fill()
    protected array $fillable = ['name', 'email', 'password'];
    
    // Ou Blacklist - bloquear específicos
    protected array $guarded = ['id', 'is_admin', 'role'];
}
Método Respeita $fillable Descrição
fill($data)✅ SimIgnora não-fillable silenciosamente
fillOrFail($data)✅ SimLança exceção para não-fillable
forceFill($data)❌ NãoBypass total (uso interno)
update($data)✅ Simfill() + save()

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

Criar Tabela

$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
Atributo Tipo Descrição
typestringVARCHAR, INT, BIGINT, DATETIME, TEXT
constraintintTamanho/precisão (255, 11, etc.)
nullboolPermite NULL
defaultmixedValor padrão ou RawSql
unsignedboolPara tipos numéricos
auto_incrementboolIncremento automático

Padrões de Campos (Atalhos)

// Timestamps: date_reg e date_alter com defaults
$this->dbforge->timestamps();

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

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

Adicionar Colunas

$this->dbforge->addColumns(
    'orders',
    [
        'status' => [
            'type' => 'VARCHAR', 
            'constraint' => 30, 
            'default' => 'pending'
        ],
    ],
    null,
    true,  // verifyIfExists
    ['status' => 'index']
);

Modificar/Remover

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

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

Modernização: Legado vs. Moderno

Métrica Legado Moderno Ganho
Linhas de código~170~80-53%
Testabilidade❌ Difícil✅ Unit tests100%
Type safety❌ Nenhuma✅ Forte100%
Reutilização❌ Copy/paste✅ ComponentsAlta
API Docs❌ Manual✅ OpenAPIAuto

❌ Código Legado

// 170+ linhas, sem tipagem
public function list_report($request)
{
    global $db;  // Dependência global
    
    // Parsing manual
    if (Funcs::not_empty_verify($request['date'])) {
        $search['where']['date >='] = ...;
    }
    // +50 linhas de condicionais...
    
    // Formatação no loop
    foreach ($list as $k => $v) {
        $list[$k]['total_f'] = Funcs::number_format($v['total']);
    }
    
    return $list;  // Array sem tipo
}

✅ Código Moderno

readonly class HandlingList
	{
	    public function execute(
	        HandlingFilterDTO $filters
	    ): array {
	        $query = Handling::query();
	
	        if ($filters->statusIds) {
	            $query->whereIn('status_id', $filters->statusIds);
	        }
	
	        if ($filters->dateFrom) {
	            $query->where('date_reg', '>=', $filters->dateFrom);
	        }
	
	        return $query
	            ->limit($filters->limit)
	            ->asArray()
	            ->get();
	    }
	}

💡 ROI Estimado

Para cada hora investida na migração, economiza-se 3-5 horas em manutenção futura e debugging.

CLI: make:model

# Criar model básico
php og make:model User --module=Auth

# Criar model para tabela legada
php og make:model Vat --module=Tax --legacy --normalize --pk=CodIVA --table=ivas
Opção Descrição
--module=Módulo destino
--addon=Addon destino
--table=Nome da tabela
--pk=Primary key
--legacyTimestamps legados (date_reg/date_alter)
--normalizeNormalização de keys

Timestamps Configuráveis

Controle flexível sobre colunas de timestamp para tabelas modernas e legadas.

Tabelas Modernas

class Order extends Model
{
    // Padrão: created_at e updated_at
    protected bool $timestamps = true;
    
    // Formato (opcional)
    protected string $timestampFormat = 'Y-m-d H:i:s';
}

Tabelas Legadas

class Vat extends Model
{
    // Para tabelas com date_reg/date_alter
    protected string $createdAtColumn = 'date_reg';
    protected string $updatedAtColumn = 'date_alter';
}

Desativar Timestamps

// Permanentemente no modelo
class LogEntry extends Model
{
    protected bool $timestamps = false;
}

// Temporariamente para uma operação
$model->withoutTimestamps()->save();

Visibilidade de Atributos

Controle quais atributos aparecem na serialização (toArray(), toJson()).

class User extends Model
{
    // Ocultar sempre (excluídos de toArray, retornam null no acesso)
    protected array $hidden = ['password', 'api_token', 'remember_token'];
    
    // Modo whitelist: SÓ estes aparecem em toArray()
    protected array $visible = ['id', 'name', 'email', 'avatar'];
    
    // Computed properties a incluir automaticamente
    protected array $appends = ['full_name', 'profile_url'];
}

Alterações Temporárias

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

// Ocultar temporariamente
$user->makeHidden(['email'])->toArray();

Acesso Interno

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

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

Normalização de Keys Legadas

Tabelas antigas podem ter colunas duplicadas com case diferente (ex: SAFTTaxCode e safttaxcode). O Model normaliza automaticamente.

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 Input Resultado
nullSAFTTaxCodeSAFTTaxCode
'lower'SAFTTaxCodesafttaxcode
'snake'SAFTTaxCodes_a_f_t_tax_code

Interfaces Implementadas

QueueableEntity

Serialização eficiente em jobs de queue. Apenas o ID é guardado.

// Dispatch com modelo
Queue::dispatch(new ProcessOrderJob($order));

// No job, carrega automaticamente do banco
class ProcessOrderJob {
    public function __construct(
        private Order $order  // Só ID serializado
    ) {}
}

UrlRoutable

Route Model Binding — converte parâmetros de URL em modelos.

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

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

ArrayAccess

Acesso estilo array para compatibilidade com código legado.

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

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

// Verificação
isset($user['name']);  // true
unset($user['temp']);  // remove atributo

Entidades Ricas (DDD)

Evolução de modelos anémicos (só dados) para entidades ricas com comportamento de negócio encapsulado.

❌ Modelo Anémico

// Só getters, sem lógica
class Order extends Model {}

// Lógica espalhada pelo código
$total = $order->subtotal + $order->tax;
if ($order->status === 'pending') { ... }

✅ Entidade Rica

class Order extends Model
{
    #[Computed]
    protected function total(): float {
        return $this->subtotal + $this->tax;
    }
    
    public function isPending(): bool {
        return $this->status === 'pending';
    }
}

Exemplo Completo: Entidade Rica

class Order extends Model
{
    #[Computed]
    protected function total(): float {
        return $this->subtotal + $this->tax - $this->discount;
    }
    
    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']);
    }
    
    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->save();
    }
}

📈 Evolução Gradual

Etapa 1: Arrays (legado) → Etapa 2: Modelos tipados (atual) → Etapa 3: Entidades ricas + Value Objects (futuro)