@extends('layouts.docs') @section('title', 'Padrões e Casos de Uso - OG Framework') @section('body')
Apêndice
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.
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.
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 (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}");
}
}
}
}
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')) {
// ...
}
}
}
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'));
}