System
Event System
Sistema de eventos do OfficeGest — Desacople componentes, reaja a ações e construa aplicações extensíveis com o padrão Observer.
Introdução
O sistema de eventos do OfficeGest implementa o padrão Observer, permitindo que componentes da aplicação comuniquem entre si de forma desacoplada. Quando algo importante acontece (um documento é registado, uma fatura é emitida), um evento é disparado e múltiplos listeners podem reagir a ele independentemente.
Desacoplamento
O código que dispara o evento não sabe quem o escuta.
Extensibilidade
Adicione novos listeners sem modificar código existente.
Testabilidade
Teste cada listener isoladamente com mocks.
Auditoria
Fácil registar todas as ações importantes.
Conceitos Fundamentais
O que é um Evento?
Um evento é um objeto que representa "algo que aconteceu" na aplicação. É um container de dados que transporta informação sobre a ocorrência. Eventos são imutáveis — apenas guardam dados, não contêm lógica.
<?php
final readonly class OrderCreated
{
public function __construct(
public int $orderId,
public float $amount,
public int $userId,
) {}
}
O que é um Listener?
Um listener é uma classe que "escuta" um evento específico e executa uma ação quando ele ocorre. Cada listener deve ter uma única responsabilidade.
<?php
class SendOrderConfirmationListener
{
public function handle(OrderCreated $event): void
{
$this->mailer->send(
to: $event->userId,
subject: 'Pedido Confirmado',
data: ['orderId' => $event->orderId]
);
}
}
O que é um Subscriber?
Um subscriber é uma classe que pode escutar múltiplos eventos. É ideal quando tens lógica relacionada que responde a vários eventos do mesmo domínio.
<?php
final readonly class OrderEventsSubscriber
{
public function subscribe(Dispatcher $events): void
{
$events->listen(OrderCreated::class, $this->onOrderCreated(...));
$events->listen(OrderShipped::class, $this->onOrderShipped(...));
$events->listen(OrderCancelled::class, $this->onOrderCancelled(...));
}
}
| Cenário | Usar |
|---|---|
| Escutar um evento | Listener |
| Escutar múltiplos eventos relacionados | Subscriber |
| Lógica de auditoria | Subscriber |
| Notificação específica | Listener |
| Integração complexa | Subscriber |
Quando Usar Eventos
✅ Usar eventos quando:
- • Múltiplas ações devem acontecer após uma operação
- • Auditoria/Logging de ações importantes
- • Notificações (email, SMS, push)
- • Integrações com sistemas externos
- • Ações secundárias que não devem bloquear a principal
❌ Não usar eventos quando:
- • A ação é simples e única
- • O resultado do listener afeta a resposta principal
- • Performance crítica (eventos adicionam overhead mínimo)
❌ Sem eventos (código acoplado)
class RegisterInvoiceAction
{
public function handle($data)
{
$this->registerWithAuthority($data);
// Código acoplado - tudo junto
$this->logger->log($data); // Log
$this->emailService->notify($data); // Email
$this->stats->increment($data); // Estatísticas
$this->webhook->send($data); // Webhook
}
}
✅ Com eventos (código desacoplado)
class RegisterInvoiceAction
{
public function handle($data)
{
$response = $this->registerWithAuthority($data);
// Dispara evento - listeners tratam o resto
event(new InvoiceRegistered($data, $response));
}
}
Estrutura do Sistema
Modules/
├── Common/
│ └── Events/
│ ├── EventServiceProvider.php # Regista o dispatcher e registry
│ ├── EventSubscriberRegistry.php # Registry central de subscribers
│ └── Console/
│ ├── MakeEventCommand.php # php og make:event
│ ├── MakeListenerCommand.php # php og make:listener
│ ├── MakeSubscriberCommand.php # php og make:subscriber
│ └── stubs/
│ ├── MakeEvent.stub
│ ├── MakeListener.stub
│ └── MakeSubscriber.stub
└── SeuModulo/
├── Events/
│ └── SeuEvento.php
├── Listeners/
│ └── SeuSubscriber.php
└── Providers/
└── SeuModuloServiceProvider.php
Criando Eventos
Use o comando Artisan para gerar eventos rapidamente com a estrutura correta.
php og make:event NomeDoEvento --module=NomeDoModulo
| Opção | Descrição |
|---|---|
| --module= | Módulo onde criar (ex: Billing, SaftAo/ElectronicInvoicing) |
| --addon= | Addon onde criar (ex: MeuAddon) |
| --properties= | Propriedades do evento (ex: "orderId:int,total:float") |
| --force | Sobrescrever ficheiro existente |
Evento simples
php og make:event OrderCreated --module=Billing
Evento com propriedades tipadas
php og make:event PaymentReceived --module=Billing --properties="orderId:int,amount:float,userId:int"
Evento em sub-componente
php og make:event InvoiceRegistered --module=SaftAo/ElectronicInvoicing
Estrutura Gerada
<?php
declare(strict_types=1);
namespace Og\Modules\Billing\Events;
final readonly class OrderCreated
{
public function __construct(
public readonly int $orderId,
public readonly float $amount,
public readonly int $userId,
) {
}
}
💡 Dica
Eventos são "data containers" — não têm lógica, apenas transportam informação. São declarados como final readonly para garantir imutabilidade.
Criando Listeners
Um Listener escuta um único evento e executa uma ação quando ele ocorre.
php og make:listener NomeDoListener --module=NomeDoModulo
| Opção | Descrição |
|---|---|
| --module= | Módulo onde criar |
| --addon= | Addon onde criar |
| --event= | Classe do evento a escutar (FQCN) |
| --force | Sobrescrever ficheiro existente |
Exemplo Completo
php og make:listener SendOrderConfirmation --module=Billing --event="Og\\Modules\\Billing\\Events\\OrderCreated"
<?php
declare(strict_types=1);
namespace Og\Modules\Billing\Listeners;
use Og\Modules\Billing\Events\OrderCreated;
class SendOrderConfirmationListener
{
public function handle(OrderCreated $event): void
{
// Enviar email de confirmação
$this->mailer->send(
to: $event->userId,
subject: 'Pedido Confirmado',
data: ['orderId' => $event->orderId]
);
}
}
Criando Subscribers
Um Subscriber escuta múltiplos eventos numa única classe. É ideal quando tens lógica relacionada que responde a vários eventos.
php og make:subscriber NomeDoSubscriber --module=NomeDoModulo
| Opção | Descrição |
|---|---|
| --module= | Módulo onde criar |
| --addon= | Addon onde criar |
| --events= | Lista de eventos separados por vírgula |
| --service= | Classe de serviço a injetar |
| --force | Sobrescrever ficheiro existente |
Comando com Múltiplos Eventos
php og make:subscriber OrderEventsSubscriber --module=Billing --events="OrderCreated,OrderShipped,OrderCancelled"
Estrutura do Subscriber
<?php
declare(strict_types=1);
namespace Og\Modules\Billing\Listeners;
use Illuminate\Events\Dispatcher;
use Og\Modules\Billing\Events\OrderCreated;
use Og\Modules\Billing\Events\OrderShipped;
use Og\Modules\Billing\Events\OrderCancelled;
final readonly class OrderEventsSubscriber
{
public function __construct(private OrderAuditService $auditService)
{
}
public function subscribe(Dispatcher $events): void
{
$events->listen(OrderCreated::class, $this->onOrderCreated(...));
$events->listen(OrderShipped::class, $this->onOrderShipped(...));
$events->listen(OrderCancelled::class, $this->onOrderCancelled(...));
}
public function onOrderCreated(OrderCreated $event): void
{
$this->auditService->log('created', $event->orderId);
}
public function onOrderShipped(OrderShipped $event): void
{
$this->auditService->log('shipped', $event->orderId);
}
public function onOrderCancelled(OrderCancelled $event): void
{
$this->auditService->log('cancelled', $event->orderId);
}
}
Registando Subscribers
Para que um subscriber seja activo, precisa ser registado no ServiceProvider do módulo.
✅ Usar o EventSubscriberRegistry (Recomendado)
<?php
namespace Og\Modules\Billing\Providers;
use Og\Modules\Common\Events\EventSubscriberRegistry;
use Og\Modules\Common\Providers\ServiceProvider;
use Og\Modules\Billing\Listeners\OrderEventsSubscriber;
class BillingServiceProvider extends ServiceProvider
{
public function register(): void
{
// Registar o subscriber (apenas adiciona à lista - não resolve ainda)
$this->app->make(EventSubscriberRegistry::class)
->add(OrderEventsSubscriber::class);
}
}
Registo directo no boot() (Alternativa)
public function boot(): void
{
$dispatcher = $this->app->make('events');
$subscriber = $this->app->make(OrderEventsSubscriber::class);
$subscriber->subscribe($dispatcher);
}
Disparando Eventos
Para disparar um evento, use a função global event() ou a Facade Event.
Uso Básico
use Og\Modules\Billing\Events\OrderCreated;
// Dentro de uma Action, Controller, Service...
event(new OrderCreated(
orderId: $order->id,
amount: $order->total,
userId: $order->user_id,
));
Via Facade
use Og\Modules\Common\Facades\Event;
// Disparar evento
Event::dispatch(new OrderCreated($orderId, $amount, $userId));
// Escutar evento dinamicamente
Event::listen(OrderCreated::class, function ($event) {
// Handle
});
// Verificar se há listeners
Event::hasListeners(OrderCreated::class);
Disparo Condicional
// Disparar apenas se condição for verdadeira
if ($shouldNotify) {
event(new OrderCreated($orderId, $amount, $userId));
}
Exemplo Real: SaftAo Electronic Invoicing
Este exemplo mostra como o sistema é usado na prática para registar submissões à autoridade fiscal angolana.
Evento de Interação
final readonly class SaftAoElectronicInvoicingRegisterInvoiceInteraction
{
public function __construct(
public readonly ?string $referenceId,
public readonly ?int $userId,
public readonly int|string $documentNumber,
public readonly string $documentOperationType,
public readonly string $submissionUuid,
public readonly string $endpoint,
public readonly string $sourceClass,
public readonly string $status,
public readonly string $endpointMethod,
public readonly string $authorityCountry,
public readonly ?array $payload,
public readonly ?array $response,
) {}
}
Subscriber que Grava Histórico
final readonly class SaftAoElectronicInvoicingTaxAuthoritySubmissionSubscriber
{
public function __construct(private TaxAuthorityDocumentSubmissionRecorder $recorder)
{
}
public function subscribe(Dispatcher $events): void
{
$events->listen(
SaftAoElectronicInvoicingRegisterInvoiceInteraction::class,
$this->onInteraction(...),
);
$events->listen(
SaftAoElectronicInvoicingRegisterInvoiceSucceeded::class,
$this->onSucceeded(...),
);
}
public function onInteraction($event): void
{
$this->recorder->recordInteraction([
'authority_reference_id' => $event->referenceId,
'user_id' => $event->userId,
'document_number' => (string) $event->documentNumber,
'status' => $event->status,
// ...
], $event->payload, $event->response);
}
}
Action que Dispara os Eventos
public function handle(string $documentType, int $documentNumber): array
{
$response = $this->postJson($endpoint, $body);
// Dispara evento de interação (sempre)
event(new SaftAoElectronicInvoicingRegisterInvoiceInteraction(
$requestId, Auth::id(), $documentNumber, /* ... */
));
if ($wasSuccessful) {
// Dispara evento de sucesso
event(new SaftAoElectronicInvoicingRegisterInvoiceSucceeded(
(string) $requestId, Auth::id(), $documentNumber, /* ... */
));
}
return $result;
}
Testando Eventos
O sistema oferece uma Facade Event com métodos para fake e asserções em testes.
✅ Usando Event::fake()
<?php
namespace Tests\Feature;
use Og\Modules\Common\Facades\Event;
use Og\Modules\Billing\Events\OrderCreated;
use Tests\TestCase;
class CreateOrderTest extends TestCase
{
protected function tearDown(): void
{
Event::stopFaking(); // Limpa o fake após cada teste
parent::tearDown();
}
public function test_order_created_event_is_dispatched(): void
{
// Ativa o fake para eventos
Event::fake([OrderCreated::class]);
// Executa a action
$action = app(CreateOrderAction::class);
$action->handle(['orderId' => 123, 'amount' => 100]);
// Verifica que o evento foi disparado
Event::assertDispatched(OrderCreated::class);
}
public function test_event_has_correct_data(): void
{
Event::fake([OrderCreated::class]);
$action = app(CreateOrderAction::class);
$action->handle(['orderId' => 456, 'amount' => 199.99]);
// Verifica com condições específicas
Event::assertDispatched(OrderCreated::class, function ($event) {
return $event->orderId === 456;
});
}
}
| Método | Descrição |
|---|---|
| Event::fake() | Inicia fake de todos os eventos |
| Event::fake([Event1::class]) | Fake apenas eventos específicos |
| Event::assertDispatched(Class) | Verifica que evento foi disparado |
| Event::assertDispatched(Class, fn) | Verifica com callback |
| Event::assertNotDispatched(Class) | Verifica que evento NÃO foi disparado |
| Event::assertDispatchedTimes(Class, n) | Verifica número de vezes |
| Event::stopFaking() | Para o fake e volta ao normal |
Testar Listeners Isoladamente (Recomendado)
public function test_listener_sends_email(): void
{
// Mocka o serviço de email
$mailerMock = $this->createMock(Mailer::class);
$mailerMock->expects($this->once())
->method('send')
->with($this->callback(fn($params) => $params['to'] === 42));
// Cria o listener com o mock
$listener = new SendOrderConfirmationListener($mailerMock);
// Cria o evento e executa o listener directamente
$event = new OrderCreated(orderId: 123, amount: 99.99, userId: 42);
$listener->handle($event);
}
Debugging
⚠️ Problema: Evento Disparado mas Listener Não Executa
1. Verificar se o subscriber está registado:
// No ServiceProvider
public function register(): void
{
dd($this->app->make(EventSubscriberRegistry::class)->all());
// Deve mostrar a classe do subscriber
}
2. Verificar se o listener está subscrito:
// Temporariamente, no boot() do ServiceProvider
public function boot(): void
{
$dispatcher = $this->app->make('events');
dd($dispatcher->getListeners(OrderCreated::class));
}
Checklist de Debugging
- ☐ O subscriber está registado no
ServiceProvider::register()? - ☐ O
ServiceProviderestá listado noContainer.php? - ☐ O método
subscribe()do subscriber está correto? - ☐ O evento está a ser criado com os dados correctos?
- ☐ Há erros no log (
storage/logs/)? - ☐ As dependências do subscriber estão bindadas?
Boas Práticas
| Tipo | Convenção | Exemplo |
|---|---|---|
| Evento | {Entidade}{Acção} (passado) | OrderCreated, InvoiceRegistered |
| Listener | {Acção}{Entidade}Listener | SendOrderEmailListener |
| Subscriber | {Contexto}EventsSubscriber | OrderEventsSubscriber |
✅ Eventos específicos
// Fácil de escutar selectivamente
OrderCreated
OrderShipped
OrderCancelled
❌ Evento genérico
// Dificulta filtragem
OrderUpdated
// O que mudou? Status? Dados?
✅ Um Listener = Uma Responsabilidade
SendOrderEmailListener // Envia email
UpdateInventoryListener // Atualiza stock
NotifyWarehouseListener // Notifica armazém
❌ Listener que faz tudo
// Email + Stock + Notificação + ...
HandleOrderCreatedListener
Resumo de Comandos
| Comando | Descrição |
|---|---|
| php og make:event Nome --module=Mod | Criar evento |
| php og make:listener Nome --module=Mod | Criar listener |
| php og make:subscriber Nome --module=Mod | Criar subscriber |