This Small Corner

Eric Koyanagi's tech blog. At least it's free!

Writing Lean Controllers in Laravel

By Eric Koyanagi
Posted on

What is “Good MVC” in Laravel?

Many developers know the old aspiration: “skinny controllers, fat models”. Let’s put aside the implications around body-shaming for now (although it’s interesting how many computer science terms have derogatory roots) and focus on this in more detail. 

First, why is this the standard? What’s wrong with big controllers? 

For starters, testability. Controllers are not easy to test because of their purpose: they basically accept input and control the flow of communication between the model and view. This isn’t a unit-level operation.  

Second, throwing code in controllers makes it difficult to reuse. The controller is meant to be high level, not the place where actual business logic lives. You want to be able to read a controller and know what it’s doing easily…without needing to know exactly how it does that. 

That said, it isn’t as simple as ripping code out of controllers to throw them into models…not in the real world. It’s one thing to demand “thin controllers”, but another thing to explain how

Here are some basic tips for how to structure your code so that controllers don’t get out of…well, control. 


How do I Avoid Massive Models, then?

The basic strategy for cleaning up your MVC application is to move more code into models and strip any business logic from controllers. This doesn’t always work, though. Or rather, it isn’t always so straightforward. 

First, no one really wants 1000+ line model god classes. Great that your controllers are short, but that doesn’t automatically make your codebase understandable. If models are becoming convoluted, that’s a problem. Our intuition tells us it’s a problem, and we should listen to it! 

Further, many articles tell us that controllers “should be thin”, but don’t often offer examples of any real complexity. You often need more than one model to do some task. What, then?

The naive approach is to shrug and figure that you have no choice but to couple this relationship in the model layer (or worse, throw it into the controller). Devs often build logic into controllers simply because they aren’t sure where else to put it. 

This is where service classes come into play. These are like the strategy or mediator design pattern -- we have some set of operations we want to do that requires various dependencies. We can lift this operation into its own class to maximize code reuse and abstract implementation details. 

For example, API operations often require various different pieces of data -- putting this in a model tightly couples any dependencies and limits your ability to write DRY code. In its own service class, this logic is more self-contained, dependencies are more flexible, and it is far easier to reuse. 


Decoupling with Observers and Events

Laravel has other ways to decouple code in the form of events and observers. These concepts can keep your controllers and models lean by lifting some logic into specialized classes. We already did a deep dive into the events system, but let’s review. 

Events work by registering themselves in the service container (specifically the EventServiceProvider), which we know happens as the application bootstraps. Similarly, model observers are initialized in basically the same way and time, as you can see in the snippet (of the Laravel 10 source code) below:

   foreach ($events as $event => $listeners) {
        foreach (array_unique($listeners, SORT_REGULAR) as $listener) {
            Event::listen($event, $listener);
        }
    }

    foreach ($this->subscribe as $subscriber) {
        Event::subscribe($subscriber);
    }

    foreach ($this->observers as $model => $observers) {
        $model::observe($observers);
    }

So we see that it does simple foreach loops to initialize every event and observer as the app bootstraps. This is something to keep in mind, because this must happen with each request, but for most applications the performance implication is minor. 

As we note in the linked article, we can also use events with queues to do work “in the background” (with another PHP process), so there are performance benefits, too. If the application grows, we can have an entirely separate worker app manage the queue operations on its own hardware without that much work. 

Events and observers are a powerful tool for keeping our MVC application decoupled and flexible. 

Personally, I’m not a big fan of model observers in big applications, especially applications with many developers. It’s too easy to abuse the ORM in this way because operations can become obfuscated in observers. A naive dev might not realize that doing a given operation on a model that should be “safe” actually triggers all sorts of logic that kills the server if used incorrectly.

In code review, it can be missed because even experienced devs can forget that doing an operation on a given model triggers observers. This is great for some situations where you absolutely need this sort of functionality, but risky if stuffing too much logic, here.

I like events because they are more explicit. You can see in the code where they’re being fired and easily find any relevant listeners. There’s still a danger that a naive dev will fire an event without realizing how much work that event is trying to do…but at least it’s explicit! 


Decoupling with Queues or Microservices

As I just mentioned, queues are a powerful resource in Laravel that allow you to offload operations to workers. This can be yet another way you decouple your application logic. A front end or API might push operations into an SQS (or other) queue. A separate worker application might then consume each entry and “do work” against it. 

With this, the worker application doesn’t even need to be written in Laravel, it could be any platform capable of doing the work. The scope of the application that pushes things into the queue is much smaller, breaking the logic into smaller and less tightly coupled pieces. 

This is similar to working with events, and maybe the same as working with events since events can be queued. The main difference is that queues don’t need to be processed by the same application stack as the thing that initiated it. It can be an entirely separate codebase. Queues also rely on an external system like SQS, which has a cost.

You can also leverage microservices like Lambda to decouple code in some specific use cases, either through Laravel Vapor or not. Microservice design deserves its own article, but is a powerful option for splitting some bits of logic into scalable, serverless pieces. 

This might be a good alternative to using a queue for some use cases. For example, maybe you want a piece of logic to run in its own hardware in a self-contained way, but don’t need all the overhead of a separate worker (or don’t want to scale/maintain that worker). The result is basically the same, but can be more performant, maintainable, and affordable depending on the context.


Conclusion

MVC design is powerful because it allows developers to build applications in a predictable way, with logic split into separate concerns that any trained engineer can understand. In real world applications, developers might struggle with this, unsure how to decouple code only given the basic adage “fat model, thin controller”. 

We now see how we can use service (aka mediator/strategy) classes to abstract common operations, leverage events and observers, queues, and lambdas to improve our decoupling and ensure that our controllers are lightweight. 

Each option has pros and cons around clarity and performance. Using these techniques can not only improve the clarity of your controllers, it can greatly improve throughput and performance, too. It's in looking at these concepts that we once again are reminded why it's so important to use a framework like Laravel. It empowers us with many easy-to-utilize tools that help us write reliable, scalable, decoupled code.

« Back to Article List
Written By
Eric Koyanagi

I've been a software engineer for over 15 years, working in both startups and established companies in a range of industries from manufacturing to adtech to e-commerce. Although I love making software, I also enjoy playing video games (especially with my husband) and writing articles.

Article Home | My Portfolio | My LinkedIn
© All Rights Reserved