تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Event-Driven Refactor Overview0:00

All right, behind the scenes, I made some changes so that the application can be event-driven. I built all of the langos, and now we have to make them work together. Let's start with the changes on the PurchaseItems class. As you can see, all it does now is create an order, start the order, and then dispatch an event. It doesn't have any logic related to payments. That also means that the PurchaseItems class is much simpler. We just have to assert that we have the proper values in the order that was recently created. Within the OrderStarter class, we have a very simple object. It accepts an order DTO, a user DTO, and a pending payment object.

Testing Event Listeners0:34

Within the OrderStarter class, we have a very simple object. It accepts an order DTO, a user DTO, and a pending payment object. And if we go into OrderStarterTask, we also have a very simple task. Laravel allows us to assert that certain listeners are listening to an event. In that case, we want to make sure that both SendOrderConfirmationEmail as well as PayOrder are listening to this event. We can do that by first calling the fake method on the event facade, and then calling the AssertListening method on the fake event facade. The first argument is the expected event, and the second argument is the expected listener. So if we run this, it is going to fail, and that's because the listeners are not listening.

Reviewing Payment Flow Listeners1:10

The first argument is the expected event, and the second argument is the expected listener. So if we run this, it is going to fail, and that's because the listeners are not listening to the event. Before we connect these pieces, let's take a look at the other listeners. First, let's go into SendOrderConfirmationEmail. As you can see, pretty simple. We just send an email to a given User. Now let's take a look at PayOrder. This one's a little bit more complex. As you can see, we're trying to create a payment for the order that's being processed.

This one's a little bit more complex. As you can see, we're trying to create a payment for the Order that's being processed. And if it fails, we publish a PaymentFailed event, and then we throw the exception. However, if it succeeds, we publish a PaymentSucceeded event. If we go into PaymentSucceededTask, we can see that we expect two listeners for this event. The first one is the ChrisProductStock. Let's take a look at it. Pretty simple. We call the ProductStockManager on each of the orderLines, and this is only going

Pretty simple. We call the ProductStockManager on each of the order lines, and this is only going to execute once a payment succeeds. And then we also have the CompleteOrderListener. This one's also pretty simple. It just calls the complete method on the given order. And this method is also very, very simple. We just change the status to Completed. Now let's check the PaymentFailedTask. We also expect two listeners.

Now let's check the PaymentFailedTask. We also expect two listeners. The first one is to notify the user of a payment failure, and as we can see, pretty simple. We send an email to a User that is the PaymentForOrderFailedMailable. If we go into the task for this class, you can see it's pretty simple too. We just assert that an email was sent. The OrderListener is MarkOrderAsFailed, and we just simply mark an order as failed. So we first check if it is completed. If it is completed, we cannot mark this as failed. It wouldn't make sense.

If it is completed, we cannot mark this as failed. It wouldn't make sense. However, if it isn't, we can tag this order as PaymentFailed. Now notice that this is only an example. You could have both of these in the same listener, especially because they belong to the same module. This is to showcase how granular you can go with events. Alright, since we have tasks for both of these events, we're guaranteed that they're being listened to. And if we run our tasks, you can see that we have three failures.

Wiring Events in Providers3:24

listened to. And if we run our tasks, you can see that we have three failures. Let's start with the OrderStartedTask. Let's run this. It's obviously failing. So the first thing we have to do is to listen to the event. We already have the logic on the listener built out. So it's just a matter of telling Laravel that whenever the OrderStarted event is batched, it should invoke those two listeners. As we can see, they're both in the Order module.

it should invoke those two listeners. As we can see, they're both in the Order module. So let's go into the Order module event service provider and add them to the OrderStartedListeners. When an order started, we want to send an order confirmation email, and we want to try and pay for the order. Let's go back into our task and run this. Okay, this is passing. Now let's go into the PaymentSucceededTask. Let's run this. Obviously failing.

Let's run this. Obviously failing. And let's see which listeners do we need. Well, we need CompleteOrder, which lives in the Order module, and we also need DecreaseProductStock, which lives in the Product module. So let's start with the Order module. Let's go into the OrderEventServiceProvider, and let's add a new event. Let's listen to PaymentSucceeded. We need to import this class. Now let's add our listener.

We need to import this class. Now let's add our listener. Let's go back here and rerun our task. Okay, we're still missing DecreaseProductStock. So now let's go into the Product event service provider, listen to PaymentSucceeded, and call DecreaseProductStock. Let's rerun our task. Okay, now it's green. Now it's time for PaymentFailed. Let's go into PaymentFailedTask, run the task.

Now it's time for PaymentFailed. Let's go into PaymentFailedTask, run the task. PaymentFailing, and let's see which modules are we touching. Well, they both belong to the Order module, although the event is being sent from within the Payment module. So let's go into the Order module EventServiceProvider and listen to PaymentFailed. So we want to mark an order as failed, and we want to notify the user of a payment failure. Let's rerun the task, and it's passing. Now let's run all of our tasks, and they're all passing. Okay, let's go back to PurchaseItems and try to follow what's happening.

Now let's run all of our tasks, and they're all passing. Okay, let's go back to PurchaseItems and try to follow what's happening. On PurchaseItems, we're simply creating an order, and then we are dispatching an event. So this is what this action is responsible for, simply creating the order. Now we're listening to this event, and one of the listeners is PayOrder. So let's go and check it out. As we've seen, also pretty simple, it has one goal, to create a payment for a given order. If a payment fails, it will dispatch a PaymentFailed event, and if it succeeds, it will dispatch a PaymentSucceeded event.

If a payment fails, it will dispatch a PaymentFailed event, and if it succeeds, it will dispatch a PaymentSucceeded event. For the PaymentSucceeded event, we have two listeners. The first one is CompleteOrder, and as we've seen, it's pretty simple. And the other one is to decrease the product stock, as we've seen, also very simple. As you can see, each component becomes very, very simple, focused on one single thing. We have a component focused on creating an order, and then dispatching a message. We have another component who's responsible for picking up the message, or listening to it, and trying to charge the user's credit card. Depending on the outcome of that action, it would dispatch either a PaymentFailed event,

Event-Driven Tradeoffs6:34

it, and trying to charge the user's credit card. Depending on the outcome of that action, it would dispatch either a PaymentFailed event, or a PaymentSucceeded event. And then you have other components picking that up. Now this is not as good as it sounds. I want to also present some problems. First of all, if you're publishing these events to a broker, let's say, Apache Kafka, this is going to be handled asynchronously. So we're going to have eventual consistency, for one. If you are using the Laravel event bus, those events will be processed synchronously by

So we're going to have eventual consistency, for one. If you are using the Laravel event bus, those events will be processed synchronously by default. That is, for example, if an exception is thrown, it will halt the execution of other events, or other listeners. In general, we want to make sure that our events are handled asynchronously, and we're going to see how to handle that in just a few minutes. Now the second problem I want to present is, let's say that the system receives an order, and it creates the record on the database. What happens if the record is created, but the event publishing fails?

and it creates the record on the database. What happens if the record is created, but the event publishing fails? That is, we cannot publish the event. Well, what would happen is that that order would be stuck in its pending state. It would never progress, a payment would never be attempted, and it would never either fail, or be succeeded. That's another classical problem when dealing with events. Now depending on how you're handling your events, for example, if you're pushing them to a queue, that's more unlikely to happen. However, there are solutions for this problem, and one of them is the outbox pattern, which

to a queue, that's more unlikely to happen. However, there are solutions for this problem, and one of them is the outbox pattern, which is not something that we're going to cover on this series. But it's something that you should be aware of. Now regarding event listeners being handled synchronously, thankfully Laravel has it covered for us. If we implement the shared queue interface on our listeners, Laravel will take care of handling those asynchronously through its queue. So if, for example, you have four listeners and one of them fails, it will not impact the other listeners.

Queueing Listeners and Tests8:25

So if, for example, you have four listeners and one of them fails, it will not impact the other listeners. They will be handled in isolation. And if it fails for some reason, you can fix it and rerun the job on the queue. I suggest that if you're using events, you process them asynchronously. Let's see how that works. Let's take a look at decreased product stock. We can simply implement shared queue and Laravel will queue this automatically for us. The same thing with complete order. Let's implement shared queue and Laravel will do the job for us.

The same thing with complete order. Let's implement shared queue and Laravel will do the job for us. Pay order, same thing. Let's implement shared queue. And that's it. Let's see which handlers we have here. So send order confirmation email. Let's also implement shared queue and pay order. Okay, this is not supposed to be on the order module. This is supposed to be on the payment module.

Okay, this is not supposed to be on the order module. This is supposed to be on the payment module. So let's get rid of this. Let's rerun our tests to make sure one of them fails. Okay, they're failing. Let's go back into our payment module and listen to this event right here. Let's run our tasks. Okay, they're back to green. And let's continue to queue our listeners. Complete order.

And let's continue to queue our listeners. Complete order. Okay, that's already queued. Mark order has failed. Let's queue this. Notify User of a payment failure. Let's also queue this. We do not have to test that our listeners are being queued because Laravel handles that for us. We can, however, write a simple test to make sure that they implement the shared queue.

for us. We can, however, write a simple test to make sure that they implement the shared queue interface that would save us if, for example, someone deletes that by mistake. Let's go into our PayOrderTest all the way up. Well, not all the way up. And let's add a new test. Let's call it itIsQueued. And we can call the assertInstanceOf method. The first argument is what we expect an object to be an instance of. In this case, it is SharedQueue.

The first argument is what we expect an object to be an instance of. In this case, it is sharedQueue. And the second argument is an instance of the object. So let's go apt instantiated using Laravel's container and then instantiate payOrder. Let's run our test. And it's passing. All right. As we've seen in this lesson, we do have lots of benefits with an event-driven approach. But we also have downsides. It's more complex.

But we also have downsides. It's more complex. So we have to pay attention for things like, are we making state changes and dispatching events in an atomic operation in a transaction? We need to ensure that if we cannot dispatch the event for some reason, we should not make the state change. Otherwise, we're going to have inconsistent state in our application. Another problem or rather a characteristic of this approach that's often downplayed is it becomes harder to understand how a feature works. Previously, we could go into the PurchaseItems class and see everything that it did.

it becomes harder to understand how a feature works. Previously, we could go into the PurchaseItems class and see everything that it did. Now we have to go through a few different classes to understand everything that's related to that feature, since it is now decoupled from the event. So use this with caution. All right. That's pretty much it for this lesson, and I'll see you in the next video. Bye bye.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟