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

Apêndice

Padrões e Casos de Uso

Este apêndice demonstra como os componentes do framework trabalham em conjunto para resolver problemas reais de um ERP. Cada exemplo vem de código em produção.

Porque Mostrar Padrões?

A documentação de componentes individuais explica o "o quê" — o que cada classe faz e que métodos tem. Esta secção mostra o "como" — como combinar os componentes para resolver problemas reais.

Cada padrão inclui também anti-patterns (o que NÃO fazer), para ajudar a evitar erros comuns.

Processamento Assíncrono de Documentos

Quando o utilizador pede para gerar um ficheiro SAF-T (que pode demorar 5-10 minutos para clientes grandes), não podemos fazê-lo esperar. A solução é processar em background e notificar quando estiver pronto.

✅ Solução: Queue + Notificações

O controller enfileira o trabalho e devolve imediatamente. Quando o Job termina, envia uma notificação ao utilizador.

// No Controller
public function exportSaft(SaftExportRequest $request)
{
    $data = $request->validated();

    // Enfileira o trabalho pesado
    SaftExportJob::dispatch($data, auth()->id())
        ->onQueue('saft-export');

    // Devolve imediatamente
    return response()->json([
        'message' => 'O ficheiro está a ser gerado. Serás notificado quando estiver pronto.'
    ]);
}

// No Job
class SaftExportJob implements ShouldQueue
{
    public function handle(): void
    {
        // Processamento pesado aqui...
        $filePath = $this->generateSaftFile($this->data);

        // Notificar quando terminar
        Notification::send([
            'codempr' => $this->userId,
            'subject' => 'Ficheiro SAF-T Pronto',
            'text' => "O teu ficheiro está disponível para download.",
            'color' => 'success',
            'action_url' => route('saft.download', $filePath)
        ]);
    }
}

❌ Anti-Pattern: Processamento Síncrono

Este código chama o browser do utilizador a esperar, resulta em timeouts, e impossibilita mostrar progresso.

// ❌ NÃO FAÇAS ISTO
public function exportSaft(Request $request)
{
    // Utilizador fica a olhar para um ecrã branco durante 10 minutos
    $saftData = $this->generateSaft($request->all());

    // Pode nem chegar aqui (timeout)
    return response()->download($saftData['file']);
}

DTOs com Validação Contextual

DTOs (Data Transfer Objects) são classes imutáveis que representam dados estruturados. No OfficeGest, usamo-los para garantir que os dados estão válidos antes de chegarem à lógica de negócio — falham rápido no constructor.

Exemplo: DTO de Exportação

Este DTO valida os dados no constructor e adapta a lógica baseada no tipo de exportação. Se algo estiver errado, lança excepção imediatamente.

final class ExportInputDTO
{
    public readonly int $dateMonthFrom;
    public readonly int $dateMonthTo;
    public readonly string $type;
    public readonly bool $includeDetails;

    public function __construct(array $data)
    {
        // Validação falha rápido
        $this->validate($data);

        $this->type = $data['tipo'];
        $this->includeDetails = $data['detalhes'] ?? false;

        // Lógica contextual: campos diferentes por tipo
        $dateField = match($this->type) {
            'accounting' => 'data_mes_ini_ctb',
            'invoicing' => 'data_mes_ini',
            default => throw new InvalidArgumentException("Tipo inválido: {$this->type}")
        };

        $this->dateMonthFrom = (int) $data[$dateField];
        $this->dateMonthTo = (int) $data[str_replace('_ini', '_fim', $dateField)];
    }

    private function validate(array $data): void
    {
        $required = ['tipo'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                throw new ValidationException("Campo obrigatório: {$field}");
            }
        }
    }
}

Integração com Código Legacy

O OfficeGest tem código de 15+ anos. Em vez de reescrever tudo, criamos "bridges" — adaptadores que permitem que código novo use funcionalidades legacy de forma limpa e testável.

✅ Solução: Adapter via ServiceProvider

Registamos o código legacy no container com uma interface moderna. O código novo depende da interface, não do legacy.

// ServiceProvider regista o adapter
class LegacyBridgeServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Encapsula o $u global num adapter moderno
        $this->app->singleton(UserInterface::class, function() {
            global $u;
            return new LegacyUserAdapter($u);
        });
    }
}

// O adapter implementa uma interface limpa
class LegacyUserAdapter implements UserInterface
{
    public function __construct(private $legacyUser) {}

    public function hasPermission(string $permission): bool
    {
        // Mapeia nomes modernos para o formato legacy
        $legacyName = $this->mapPermission($permission);
        return $this->legacyUser->permissao($legacyName);
    }
}

// Código novo usa a interface (testável, limpo)
class StockService
{
    public function __construct(private UserInterface $user) {}

    public function update(Stock $stock): void
    {
        if (!$this->user->hasPermission('stocks:update')) {
            throw new AuthorizationException();
        }
        // ...
    }
}

❌ Anti-Pattern: Dependências Globais

// ❌ NÃO FAÇAS ISTO
class StockService
{
    public function update(Stock $stock): void
    {
        global $u; // Impossível testar, dependência escondida

        if (!$u->permissao('ver_stocks')) {
            // ...
        }
    }
}

Autorização Granular com Policies

Nem todas as verificações de permissão são simples "pode ou não pode". Muitas vezes dependem do recurso específico — por exemplo, um funcionário pode ver stocks do seu sector, mas não de outros.

Policy com Lógica Contextual

class StockPolicy
{
    public function view(User $user, Stock $stock): bool
    {
        // Managers vêem tudo
        if ($user->hasPermission('stocks.manage')) {
            return true;
        }

        // Funcionários só vêem stocks do seu sector
        if ($user->hasPermission('stocks.view')) {
            return $user->sectors->contains($stock->sector_id);
        }

        return false;
    }

    public function update(User $user, Stock $stock): bool
    {
        // Só o responsável do sector pode editar
        return $stock->sector->manager_id === $user->getId()
            || $user->hasPermission('stocks.manage');
    }
}

// Uso num controller
public function show(int $id)
{
    $stock = Stock::find($id);

    if (!$this->policy->view(auth()->user(), $stock)) {
        abort(403, 'Não tens permissão para ver este stock');
    }

    return view('stocks.show', compact('stock'));
}