refactor: events refactor (Listen, Dispatch)
📑 Description
Closes https://github.com/goravel/goravel/issues/730
This PR introduces a modern, flexible event handling system with Listen() and Dispatch() methods, replacing the old Register()/Job() pattern. The new system supports wildcard patterns, dynamic listener registration, and seamless queue integration.
Key Features
1. Flexible Event Registration with Listen()
The new Listen() method supports multiple event formats and listener types:
// String events
app.Listen("user.created", func(event string, user User) error {
return nil
})
// Multiple events at once
app.Listen([]string{"user.created", "user.updated"}, listener)
// Event interface types
app.Listen(&UserCreated{}, &UserCreatedListener{})
// Wildcard patterns
app.Listen("user.*", func(event string) error {
// Handles user.created, user.updated, user.deleted, etc.
return nil
})
Comparison with old approach:
// ❌ Old way - Rigid, ceremony-heavy
app.Register(map[event.Event][]event.Listener{
&UserCreated{}: {&UserCreatedListener{}},
&UserUpdated{}: {&UserCreatedListener{}}, // Same listener, must repeat
})
// ✅ New way - Flexible, concise
app.Listen([]event.Event{&UserCreated{}, &UserUpdated{}}, &UserCreatedListener{})
// Or even better with wildcards:
app.Listen("user.*", &UserCreatedListener{})
2. Direct Event Dispatching with Dispatch()
Fire events immediately and get listener responses:
// Simple dispatch
responses := app.Dispatch("user.created")
// With payload
responses := app.Dispatch("order.placed", []event.Arg{
{Value: order, Type: "Order"},
{Value: customer, Type: "Customer"},
})
// Responses collected from all listeners
for _, response := range responses {
log.Info("Listener returned:", response)
}
Comparison with old approach:
// ❌ Old way - Indirect, requires Task creation
task := app.Job(event, args)
err := task.Dispatch() // No responses available
// ✅ New way - Direct with responses
responses := app.Dispatch(event, args) // Get all listener responses
3. Wildcard Pattern Support
Match multiple events with a single listener registration:
// Listen to all user events
app.Listen("user.*", func(eventName string, data any) error {
log.Info("User event fired:", eventName)
return nil
})
// Now all these events trigger the listener:
app.Dispatch("user.created")
app.Dispatch("user.updated")
app.Dispatch("user.deleted")
app.Dispatch("user.logged_in")
Real-world example:
// Audit logging for all admin actions
app.Listen("admin.*", &AuditLogger{})
// Security monitoring for auth events
app.Listen("auth.*", &SecurityMonitor{})
// Metrics collection for all events
app.Listen("*", &MetricsCollector{})
4. Automatic Queue Integration
Queue-aware listeners are automatically dispatched to the queue system:
type UserCreatedListener struct{}
func (l *UserCreatedListener) Handle(event any, args ...any) error {
// Process event
return nil
}
func (l *UserCreatedListener) ShouldQueue() bool {
return true // Automatically queued
}
func (l *UserCreatedListener) Signature() string {
return "UserCreatedListener"
}
func (l *UserCreatedListener) Queue(args ...any) event.Queue {
return event.Queue{
Connection: "redis",
Queue: "events",
Enable: true,
}
}
// Just listen - queueing happens automatically!
app.Listen("user.created", &UserCreatedListener{})
5. Dynamic Listener Arguments
Listeners automatically receive matched arguments:
// No arguments
app.Listen("app.started", func() error {
log.Info("App started")
return nil
})
// Event name only
app.Listen("user.created", func(eventName string) error {
log.Info("Event:", eventName)
return nil
})
// Event + typed payload
app.Listen("user.created", func(eventName string, user User) error {
log.Info("User created:", user.Email)
return nil
})
// Variadic for all arguments
app.Listen("order.placed", func(eventName string, args ...any) error {
// Handle variable number of arguments
return nil
})
Migration Guide
The old Register() and Job() methods are now deprecated but still supported:
// ❌ Old Pattern (Deprecated)
app.Register(map[event.Event][]event.Listener{
&UserCreated{}: {&EmailListener{}, &NotificationListener{}},
&OrderPlaced{}: {&InvoiceListener{}},
})
task := app.Job(&UserCreated{}, args)
task.Dispatch()
// ✅ New Pattern (Recommended)
app.Listen(&UserCreated{}, &EmailListener{}, &NotificationListener{})
app.Listen(&OrderPlaced{}, &InvoiceListener{})
responses := app.Dispatch(&UserCreated{}, args)
Architecture Improvements
- Thread-safe: All operations use proper locking (RWMutex for registration, RLock for dispatch)
- Cached wildcard matching: Performance optimization for frequently dispatched events
- Flexible listener types: Functions, closures, structs - all supported
- Type-safe: Automatic argument type matching with reflection
- Memory efficient: Listener deduplication prevents memory leaks
- High performance: Interface assertions instead of reflection in hot paths
Backward Compatibility
- ✅ Old
Register()method still works - ✅ Old
Job()method still works - ✅ Existing
EventandListenerinterfaces unchanged - ✅ All existing tests pass
- ✅ No breaking changes
✅ Checks
- [x] Added test cases for my code