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

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.

Ação Fatura Registada Pedido Criado Evento Disparado InvoiceRegistered event(new Event(...)) Listeners Reagem 📝 Gravar log de auditoria 📧 Enviar email ao cliente 📊 Atualizar dashboard 🔗 Notificar via webhook

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 eventoListener
Escutar múltiplos eventos relacionadosSubscriber
Lógica de auditoriaSubscriber
Notificação específicaListener
Integração complexaSubscriber

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")
--forceSobrescrever 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)
--forceSobrescrever 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
--forceSobrescrever 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 ServiceProvider está listado no Container.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}ListenerSendOrderEmailListener
Subscriber{Contexto}EventsSubscriberOrderEventsSubscriber

✅ 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=ModCriar evento
php og make:listener Nome --module=ModCriar listener
php og make:subscriber Nome --module=ModCriar subscriber