@extends('layouts.docs') @section('title', 'Event System - OG Framework') @section('body')
System
Sistema de eventos do OfficeGest — Desacople componentes, reaja a ações e construa aplicações extensíveis com o padrão Observer.
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.
O código que dispara o evento não sabe quem o escuta.
Adicione novos listeners sem modificar código existente.
Teste cada listener isoladamente com mocks.
Fácil registar todas as ações importantes.
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,
) {}
}
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]
);
}
}
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 |
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
}
}
class RegisterInvoiceAction
{
public function handle($data)
{
$response = $this->registerWithAuthority($data);
// Dispara evento - listeners tratam o resto
event(new InvoiceRegistered($data, $response));
}
}
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
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 |
php og make:event OrderCreated --module=Billing
php og make:event PaymentReceived --module=Billing --properties="orderId:int,amount:float,userId:int"
php og make:event InvoiceRegistered --module=SaftAo/ElectronicInvoicing
<?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.
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 |
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]
);
}
}
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 |
php og make:subscriber OrderEventsSubscriber --module=Billing --events="OrderCreated,OrderShipped,OrderCancelled"
<?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);
}
}
Para que um subscriber seja activo, precisa ser registado no ServiceProvider do módulo.
<?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);
}
}
public function boot(): void
{
$dispatcher = $this->app->make('events');
$subscriber = $this->app->make(OrderEventsSubscriber::class);
$subscriber->subscribe($dispatcher);
}
Para disparar um evento, use a função global event() ou a Facade Event.
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,
));
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);
// Disparar apenas se condição for verdadeira
if ($shouldNotify) {
event(new OrderCreated($orderId, $amount, $userId));
}
Este exemplo mostra como o sistema é usado na prática para registar submissões à autoridade fiscal angolana.
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,
) {}
}
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);
}
}
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;
}
O sistema oferece uma Facade Event com métodos para fake e asserções em testes.
<?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 |
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);
}
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));
}
ServiceProvider::register()?ServiceProvider está listado no Container.php?subscribe() do subscriber está correto?storage/logs/)?| Tipo | Convenção | Exemplo |
|---|---|---|
| Evento | {Entidade}{Acção} (passado) | OrderCreated, InvoiceRegistered |
| Listener | {Acção}{Entidade}Listener | SendOrderEmailListener |
| Subscriber | {Contexto}EventsSubscriber | OrderEventsSubscriber |
// Fácil de escutar selectivamente
OrderCreated
OrderShipped
OrderCancelled
// Dificulta filtragem
OrderUpdated
// O que mudou? Status? Dados?
SendOrderEmailListener // Envia email
UpdateInventoryListener // Atualiza stock
NotifyWarehouseListener // Notifica armazém
// Email + Stock + Notificação + ...
HandleOrderCreatedListener
| 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 |