I recently started a fresh project with my people at WTS. A SaaS themed solution for content creators to engage with their fans. (Oh by the way, we've put out new music: Lofn 3, a collection of love stories. Go check it out).
As with all things new, A SaaS application usually starts out with simple functionality, without intertwined logic and complex calculations. User Logs in, User creates A, C happens. User updates B, D is calculated. User logs out. etc. But as the user base grows, or even as active development continues, new business requirements come up that entirely threatens the structure of existing logic. If the codebase is not rightly structured, it is likely to drift into a plethora of issues. Longer functions, longer constructor parameters, nested ifs within nested ifs within nested loops.
That's how legacy code bases are born. New developers are afraid to touch the code, it takes days to understand what is going on, a lot of logic all happening in one place.
Having complex domain logic cannot be avoided when building SaaS applications, however the right approach must be taken to keep the codebase understandable, maintainable and structured. A good way to begin would be to adopt the OOP principles, having classes, interfaces and objects which are SOLID will provide a basic structure for the application of business requirement.
What about events? How does it come into play here?
The cost of increasing changes
Let's take the TrivYeah app as an instance. The application is a multi tenant system, that is, each registered organization on the app will have its own database, pointing to its own hostname.
Here we see that a couple of things are going on in the createTenant method, we are creating:
An Organization
A hostname for the organization
A database for the organization
At the surface, everything seems good. Not much is going on, just basic stuff. Now after a couple of days, we decide that we want to add some default configuration settings for newly created organizations. We can easily go back to our method and add a few lines of code.
Well, two more days later, business requirement changes and now we need to create an admin user in the tenant database as soon as the database is created. That's right we'll just go back to that method and add a few more lines.
We can both see where this is going, what happens when business requirements keep changing/growing? What if we need to send out an immediate welcome email, or create a default account ledger for the organization?
It most often would lead to an inflated code base, filled with different logic, that can become more complex and hard for new developers who would be working on the project to quickly understand.
Decoupling logic using events
Events depict that an activity is taking place. We can make use of events to build modularized applications. The more modular our application is, the more maintainable it will be.
Think of events as a kind of interface that allows interested parties to plug into an activity. Let's go back to our example method above.
We can rewrite our createTenant function to make use of events to achieve its goal.
Here, the only thing we do in our method is to actually create the tenant (which is the organization). By triggering the TenantCreated event, we are telling other interested parties that the Tenant has been created, then we return the newly created tenant.
Now that we have triggered our event, we can now create several handlers/listeners that will act on this event.
We can see how we have made use of events to decouple our code. When the TenantCreated event is triggered, Laravel passes the event object to all the registered listeners of that event. First the CreateHostName listener is called, followed by the SetupDatabase listener, followed by the CreateAdmin listener. Each listener gets called in the order you define them.
If we have a new requirement which needs to be done when creating our tenant, we do not need to touch the createTenantMethod. All we need to do is write a listener to handle the event.
Events beyond modularization
Apart from helping with decoupling code, we can make use of events to trigger application level changes like, dynamically registering route files, appending middlewares, or even setting a new route action.
Here we have a service provider, that identifies a tenant by the environment. We can let other aspects of our application know when a tenant has been identified by triggering an event.
We are dispatching two types of event, depending on the outcome of resolving the tenant. If a tenant is resolved, we trigger the TenantIdentified event, if not, we trigger the NothingIdentified event.
This gives us the ability to plug into any of the scenarios. In our case, we want to load a route file based on the tenant.
A few things are going on here. First, we get the tenant route path based on the organization, if the tenant has a route configured, we save it in memory, else we use the default tenant route path. We then get the router instance from Laravel's container, flush the existing route collection, and set the new path for the tenant on the router. Done. Dazzzall.
One more reference
In Italo Baeza's recently released package, Laraguard, we see another example of how events are used to manipulate functionality.
This package silently enables two factor authentication using 6 digits codes, without Internet or external providers.
A listener is hooked into the Illuminate\Auth\Events\Validated event, which gets fired after a login attempt is validated. When this happens, the package plays out its own validation logic.
Conclusion
We've been able to explore ways how events can help us build more structured apps. It may sound like it's more work. The tempting feeling of :
"why write more classes? when you can just do everything in one place?"
It shouldn't be about what you can do for the code, or the necessity of agile sprints. If your code could speak, what would it say about you?
For more references about events in Laravel, The official documentation is splendid: