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