Skip to Content
Course content

228: Symfony Event Dispatcher

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this pattern a dozen times in legacy codebases: the "God Service." It usually starts innocently. You have an OrderService with a placeOrder() method. At first, it just saves the order to the database. Then, you need to send a confirmation email, so you inject the MailerService. Then the warehouse needs a notification, so you add the ShippingService. Suddenly, you're adding a loyalty points update, a Slack alert for high-value orders, and an analytics ping.

Before you know it, your placeOrder() method is 200 lines long and requires seven different dependencies in the constructor. It's a nightmare to test because to test the core logic of "saving an order," you have to mock half the application. This is the naive way—tightly coupling every side effect to the primary action.

The cost of hard-coded dependencies

// The "Naive" Way
public function placeOrder(Order $order) 
{
    $this->repository->save($order);
    
    // Every new requirement adds a line here
    $this->mailer->sendConfirmation($order);
    $this->shipping->notifyWarehouse($order);
    $this->loyaltyPoints->addPoints($order->getUser(), $order->getTotal());
    $this->slack->sendAlert("New order #{$order->getId()} placed!");
}

The problem here isn't just that the method is long; it's that the OrderService now knows way too much. It knows that the warehouse exists, it knows how loyalty points work, and it knows about Slack. If the loyalty points system crashes or requires a breaking change, your order placement logic—the most critical part of your business—is now at risk. You've essentially glued your core business logic to a bunch of optional side effects.

Decoupling with the Event Dispatcher

This is where the Symfony Event Dispatcher comes in. Instead of the OrderService commanding other services to act, it simply announces that something happened. I like to think of it as a radio broadcast. The OrderService shouts, "An order was just placed!" into the void. It doesn't care who is listening, and it certainly doesn't care what they do with that information.

To do this, you create a specific Event class (which is just a data carrier) and use the EventDispatcherInterface to dispatch it. Here is how that looks in practice:

// 1. The Event Class
class OrderPlacedEvent extends Event 
{
    public const NAME = 'order.placed';
    public function __construct(public Order $order) {}
}

// 2. The Refactored Service
public function placeOrder(Order $order) 
{
    $this->repository->save($order);
    
    // We just dispatch the event and forget about it
    $event = new OrderPlacedEvent($order);
    $this->dispatcher->dispatch($event, OrderPlacedEvent::NAME);
}

Now, you create "Listeners" or "Subscribers." These are small, isolated classes that wait for order.placed. One handles the email, one handles the shipping, and one handles the Slack alert. If you want to stop sending Slack alerts, you don't touch the OrderService`; you just remove the listener.

The trade-off: Traceability vs. Flexibility

I'll be honest with you: this approach isn't a free lunch. The biggest downside is that you lose "static traceability." In the naive example, you can Ctrl+Click on sendConfirmation() and go straight to the code. With events, clicking on dispatch() only takes you to the Symfony core. You have to look at your service configuration or search the codebase for who is listening to order.placed to figure out what actually happens.

However, for any professional application, this is a trade-off I'll take every single time. The flexibility is immense. You can add a "Welcome Discount" email for first-time buyers by simply adding a new listener that checks the user's order count. You haven't touched a single line of the core order processing code, meaning you haven't introduced any risk of breaking the checkout flow. You've successfully separated the primary intent (placing an order) from the secondary effects (notifications, logs, points).




📋 Practical Task

Implement a User Registration Notification System

You are tasked with decoupling a user registration process. Currently, the UserRegistrationService handles password hashing, saving the user, sending a welcome email, and creating a default profile folder on the server. This is too bloated.

Your goal: Refactor this into an event-driven system using the Symfony Event Dispatcher.

  • Create a UserRegisteredEvent class that carries the User object.
  • Modify the UserRegistrationService to remove the Mailer and FileSystem dependencies and instead dispatch the UserRegisteredEvent.
  • Create two separate listeners:
    • WelcomeEmailListener: Handles the logic for sending the email.
    • ProfileFolderListener: Handles the logic for creating the directory.
  • Ensure that if the ProfileFolderListener is removed or fails, the user is still successfully registered and the email is still sent.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.