Decouple your Laravel code using Attribute Events
Last week, I worked with my team to build a Warehouse Management API in Laravel.
Our client had the following requirements:
The stock of a product can be modified by several processes:
- After placing an order, the stock of the ordered product must be reduced.
- The stock can be adjusted manually by a warehouse employee.
After a stock change, these processes must be initiated:
- The new stock must be pushed to our Amazon seller account.
- If the stock becomes zero, a notification email must be sent to the purchasing department.
Getting to work
We started writing the code for handling orders. So in OrderController@store, we added the requested functionality:
On its own, this code is fine. However, it is not very scalable. Because now, at every place in the code where the stock needs to be updated, we also have to add this code to push the stock to Amazon and send the out-of-stock email. The code for these “side effects” is tightly coupled to the data change.
An obvious solution is to extract the code for changing the stock to a method on the Product model:
However, this solution has some issues:
- Processing email and calling external systems are not the responsibility of the model.
- We’ll always have to use the updateStock method to update the stock, which can quickly be forgotten.
Time to decouple
In situations like this, when data can be modified by multiple processes and other processes must be initiated after a modification, events are a good way to keep the code segregated.
Let’s refactor our code: we remove the updateStock method from the Product model and instead add the following attribute events:
This way, whenever the stock of a product changes, the StockUpdated event will be dispatched. And as soon as the stock reaches 0, the ProductSoldOut event will also be dispatched.
We can react to the events using listeners:
Absolutely great! Notice how readable this code is; we have given meaningful names to the state transitions. And by doing so, we’ve solved a lot of our problems:
- The model doesn’t have to deal with side effects such as sending emails.
- If we need to adjust stock in a new place, we don’t have to worry about performing necessary side effects or calling special methods. The listeners will always trigger when the data is updated.
- If a new requirement arises, only an additional listener needs to be added.
- The code is nicely decoupled. We can put the listeners anywhere, completely separate from the calling code. Especially useful when using a folder-by-feature structure.
Finally, we can remove all the side effects from our initial controller action and just update the stock: