framework icon indicating copy to clipboard operation
framework copied to clipboard

refactor: events refactor (Listen, Dispatch)

Open ahmed3mar opened this issue 7 months ago • 1 comments

📑 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 Event and Listener interfaces unchanged
  • ✅ All existing tests pass
  • ✅ No breaking changes

✅ Checks

  • [x] Added test cases for my code

ahmed3mar avatar Jul 19 '25 09:07 ahmed3mar