Event-Driven Architecture
Decouple side effects from business logic using domain events and auto-discovered listeners.
System Overview
AlKareem ERP uses an event-driven architecture to handle side effects like GL posting, stock updates, and notifications. Events are fired from Models after approval, and Listeners react to perform the necessary operations without coupling them to the core business logic.
Domain Events
Simple data containers fired when business operations complete.
Listeners
React to events and perform side effects (GL, Stock, Notifications).
Auto-Discovery
Laravel auto-discovers listeners via union type hints.
Event Flow
sequenceDiagram
autonumber
participant S as Service
participant M as Model
participant E as Event
participant L1 as GL Listener
participant L2 as Stock Listener
participant L3 as Notification Listener
S->>M: save() + fireCreateEvent()
M->>E: new TransferCreated($this)
par Parallel Execution
E->>L1: handle(TransferCreated)
L1->>L1: Create GL Transactions
E->>L2: handle(TransferCreated)
L2->>L2: Update Stock Levels
E->>L3: handle(TransferCreated)
L3->>L3: Send Notifications
end
Key Principle
Events are only fired after approval. If a document requires approval, the
fireCreateEvent() method is called by the ApprovalEngine when
all approval steps complete, not by the Service directly.
Event Structure
Events are simple data containers that hold the relevant entity. They're organized by module
in the app/Events/{Module}/ directory.
Created Event
// app/Events/Inventory/TransferCreated.php
namespace App\Events\Inventory;
class TransferCreated
{
public function __construct(
public Transfer $data
) {}
}
Updated Event
// app/Events/Inventory/TransferUpdated.php
namespace App\Events\Inventory;
class TransferUpdated
{
public function __construct(
public Transfer $data
) {}
}
Deleted Event (Special Structure)
// app/Events/Inventory/TransferDeleted.php
// Note: For delete events, pass IDs since the entity will be deleted
namespace App\Events\Inventory;
class TransferDeleted
{
public function __construct(
public int $transferId,
public array $itemIds // Include related IDs for cleanup
) {}
}
Directory Structure
app/Events/
├── Accounts/
│ ├── VoucherCreated.php
│ ├── VoucherUpdated.php
│ └── VoucherDeleted.php
├── Inventory/
│ ├── TransferCreated.php
│ ├── TransferUpdated.php
│ └── TransferDeleted.php
├── Purchase/
│ └── ...
└── Lease/
└── ...
Model Event Firing
// In Model
public function fireCreateEvent(): void
{
$this->loadMissing('items');
event(new TransferCreated($this));
}
Listeners
Listeners react to events and perform side effects. They're organized by module in
app/Listeners/{Module}/ and use union types for auto-discovery.
Multi-Event Listener Example
// app/Listeners/Inventory/ManageStockTransactionFromTransfer.php
namespace App\Listeners\Inventory;
use App\Events\Inventory\{TransferCreated, TransferUpdated, TransferDeleted};
use App\Services\Inventory\StockTransactionService;
class ManageStockTransactionFromTransfer
{
public function __construct(
protected StockTransactionService $stockTransactionService
) {}
/**
* Union type enables auto-discovery for all three events
*/
public function handle(
TransferCreated|TransferUpdated|TransferDeleted $event
): void {
match (true) {
$event instanceof TransferCreated => $this->handleCreated($event),
$event instanceof TransferUpdated => $this->handleUpdated($event),
$event instanceof TransferDeleted => $this->handleDeleted($event),
};
}
protected function handleCreated(TransferCreated $event): void
{
if ($event->data->status === 'RECEIVED') {
$this->createTransactions($event->data);
}
}
protected function handleUpdated(TransferUpdated $event): void
{
// Reverse old transactions, create new ones
$this->stockTransactionService->reverseForSource($event->data->id);
$this->createTransactions($event->data);
}
protected function handleDeleted(TransferDeleted $event): void
{
$this->stockTransactionService->deleteForSource(
$event->transferId,
'Transfer'
);
}
protected function createTransactions(Transfer $transfer): void
{
foreach ($transfer->items as $item) {
// TRANSFER_OUT from source warehouse
$this->stockTransactionService->create([
'type' => 'TRANSFER_OUT',
'warehouse_id' => $transfer->from_warehouse_id,
'item_id' => $item->item_id,
'quantity' => -$item->quantity,
]);
// TRANSFER_IN to destination warehouse
$this->stockTransactionService->create([
'type' => 'TRANSFER_IN',
'warehouse_id' => $transfer->to_warehouse_id,
'item_id' => $item->item_id,
'quantity' => $item->quantity,
]);
}
}
}
Common Listener Patterns
Pattern 1: GL Transaction Creation
// Creates debit/credit entries in the General Ledger
class CreateTransactionFromVoucher
{
public function handle(VoucherCreated $event): void
{
foreach ($event->data->entries as $entry) {
$this->transactionService->create([
'account_id' => $entry->account_id,
'debit' => $entry->debit_amount,
'credit' => $entry->credit_amount,
'voucher_id' => $event->data->id,
]);
}
}
}
Pattern 2: Warehouse Stock Updates
// Updates warehouse_stock_items and recalculates WAC
class UpdateWarehouseStock
{
public function handle(
StockTransactionCreated|StockTransactionDeleted $event
): void {
$this->warehouseStockService->recalculate(
$event->data->warehouse_id,
$event->data->item_id
);
}
}
Pattern 3: Cascade to Child Events
// A listener that fires additional events
class CreateReceivingFromPurchaseInvoice
{
public function handle(PurchaseInvoiceCreated $event): void
{
// Create receiving voucher
$receiving = $this->receivingService->createFromInvoice(
$event->data
);
// Fire its own event (triggers stock listeners)
$receiving->fireCreateEvent();
}
}
Laravel Auto-Discovery
Laravel automatically discovers listeners based on the type hints in their handle()
method. No manual registration in EventServiceProvider is needed.
How It Works
- Laravel scans
app/Listeners/ - Reads type hints on
handle()method - Automatically maps event → listener
- Union types register for multiple events
Union Type Syntax
// Single event
public function handle(TransferCreated $e)
// Multiple events (union type)
public function handle(
TransferCreated|TransferUpdated $e
)
Clear Cache After Adding Listeners
If a new listener isn't being called, clear the event cache:
php artisan event:clear && php artisan event:cache
Module Event Summary
| Module | Key Events | Side Effects |
|---|---|---|
| Accounts | VoucherCreated, ExpenseCreated | GL Transactions |
| Inventory | TransferCreated, DeliveryVoucherCreated | Stock Transactions, WAC Calculation |
| Purchase | PurchaseInvoiceCreated, SupplierPaymentCreated | Receiving Vouchers, AP Updates |
| CashSale | CashSaleInvoiceCreated | Delivery, Revenue Posting |
| Lease | LeaseProcessingCreated, CollectionCreated | Installment Schedules, AR Updates |