Laravel's event system is phenomenal when it comes to dealing with the complex data in our web apps as it is a cornerstone for building decoupled and absolutely complex apps. This guide tells extremely detailed points on implementation and utilization of event listening especially in 2024, providing a fresh perspective with the most expansive content and detailed code examples event listeners in Laravel 11.
*(A) Understanding The Core Behind Events and Listeners
*
SO, LET'S BREAK THEM DOWN, the events in Laravel do representing specific occurrences inside an app. Listeners are the classes that would respond to all such app events. This pattern keeps promoting a separation of concerns and has been allowing for more modular and testable code.
*(B) Creating an Event
*
Let us begin by creating an even a complex event for that we will be using the Artisan command to explain better we highly suggest you'd do that too
php artisan make:event OrderPlaced
This command will be generating a new event class in the app/Events directory. Let us examine a more detailed event class
`namespace App\Events;
use App\Models\Order;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderPlaced implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
public $user;
/**
* Create a new event instance.
*
* @param \App\Models\Order $order
* @param \App\Models\User $user
* @return void
*/
public function __construct(Order $order, User $user)
{
$this->order = $order;
$this->user = $user;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('orders.'.$this->user->id);
}
/**
* The event's broadcast name.
*
* @return string
*/
public function broadcastAs()
{
return 'order.placed';
}
}`
In this expanded example, we have been including both the Order and User models. The SerializesModels trait has been ensuring that our Eloquent models are correctly serialized and deserialized when the event is passed to queued listeners. We have also implemented the ShouldBroadcast interface and defined the broadcastOn and broadcastAs methods, allowing this event to be broadcasted to websockets for real-time updates.
*Creating Multiple Listeners
*
For a single event, we might want multiple listeners. Let us create two listeners for our OrderPlaced event to expand the example furthermore. I just want you guys to make sure you get the gist of everything. So, for that please see the code example below
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
php artisan make:listener UpdateInventory --event=OrderPlaced
SO now you'd understand that this command line would get us a couple of new listener classes in the app/Listeners directory of ours. Now the thing is, here below, we would examine the SendOrderConfirmation listener and see how to it how it progresses further
`namespace App\Listeners;
use App\Events\OrderPlaced;
use App\Mail\OrderConfirmation;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class SendOrderConfirmation implements ShouldQueue
{
use InteractsWithQueue;
/**
* The number of times the job may be attempted.
*
* @var int
*/
public $tries = 3;
/**
* Handle the event.
*
* @param \App\Events\OrderPlaced $event
* @return void
*/
public function handle(OrderPlaced $event)
{
$order = $event->order;
$user = $event->user;
try {
Mail::to($user->email)->send(new OrderConfirmation($order));
Log::info('Order confirmation email sent', ['order_id' => $order->id, 'user_id' => $user->id]);
} catch (\Exception $e) {
Log::error('Failed to send order confirmation email', ['order_id' => $order->id, 'user_id' => $user->id, 'error' => $e->getMessage()]);
$this->fail($e);
}
}
/**
* Handle a job failure.
*
* @param \App\Events\OrderPlaced $event
* @param \Throwable $exception
* @return void
*/
public function failed(OrderPlaced $event, $exception)
{
Log::error('Order confirmation listener failed', ['order_id' => $event->order->id, 'user_id' => $event->user->id, 'error' => $exception->getMessage()]);
}
}`
This listener has been implementing the ShouldQueue interface, indicating that it should be queued. We have added error handling, logging, and defined a failed method to handle failures. The $tries property would be set to allow for multiple attempts in case of failure.
Now, let us look at the UpdateInventory listener
`namespace App\Listeners;
use App\Events\OrderPlaced;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class UpdateInventory implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*
* @param \App\Events\OrderPlaced $event
* @return void
*/
public function handle(OrderPlaced $event)
{
$order = $event->order;
DB::transaction(function () use ($order) {
foreach ($order->items as $item) {
$product = $item->product;
if ($product->stock < $item->quantity) {
throw new \Exception("Insufficient stock for product: {$product->id}");
}
$product->decrement('stock', $item->quantity);
Log::info("Inventory updated", ['product_id' => $product->id, 'quantity' => $item->quantity]);
}
});
}
/**
* Handle a job failure.
*
* @param \App\Events\OrderPlaced $event
* @param \Throwable $exception
* @return void
*/
public function failed(OrderPlaced $event, $exception)
{
Log::error('Failed to update inventory', ['order_id' => $event->order->id, 'error' => $exception->getMessage()]);
}
}`
Now, you'd understand that, this listener is there for a reasons like to upgrade the the inventory based on the order items etc. We have wrapped the inventory update in a database transaction to ensure data consistency. We have also added error checking to prevent negative stock and included logging for successful updates and failures.
*Registering Events & Listeners
*
We will be registering these events and listeners in the EventServiceProvider
`use App\Events\OrderPlaced;
use App\Listeners\SendOrderConfirmation;
use App\Listeners\UpdateInventory;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
parent::boot();
//
}
}`
Dispatching Events:
We can dispatch the event from a controller or service class
`use App\Events\OrderPlaced;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
/**
* Place a new order.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function placeOrder(Request $request)
{
$user = auth()->user();
DB::transaction(function () use ($request, $user) {
$order = Order::create($request->all());
$order->user()->associate($user);
$order->save();
event(new OrderPlaced($order, $user));
});
return response()->json(['message' => 'Order placed successfully', 'order_id' => $order->id]);
}
}`
In this example, we have wrapped the order creation and event dispatching in a database transaction to ensure that both occur successfully or not at all.