Fat controllers and models: an inevitable problem for most large-scale projects based on MVC frameworks such as Yii and Laravel. The primary thing that fattens controllers and models is the Active Record, a powerful and essential component of such frameworks.

The Problem: Active Records and Its Violation of the SRP

The Active Record is an architectural pattern, an approach to accessing data in a database. It was named by Martin Fowler in his 2003 book Patterns of Enterprise Application Architecture and is widely used in PHP Frameworks.

Despite the fact that it is a very necessary approach, the Active Record (AR) pattern violates the Single Responsibility Principle (SRP) because AR models:

  • Deal with querying and data saving.
  • Know too much about the other models in the system (through relationships).
  • Are often directly involved in the application’s business logic (because the implementation of data storage is closely linked to said business logic).

This violation of the SRP is a good tradeoff for rapid development when you need to create an application prototype as soon as possible, but it is quite harmful when the application grows into a middle or a large-scale project. “God” models and fat controllers are difficult to test and maintain, and freely using models everywhere in controllers leads to tremendous difficulties when you inevitably have to change the database structure.

The solution is simple: divide the Active Record’s responsibility into several layers and inject cross-layer dependencies. This approach will also simplify testing because it allows you to mock those layers not currently being tested.

The Solution: A Layered Structure for PHP MVC Frameworks

A “fat” PHP MVC application has dependencies everywhere, interlocking and error-prone, while a layered structure uses dependency injection to keep things clean and clear cut.

There are five primary layers that we’ll cover:

  • The controller layer
  • The service layer
  • DTOs, a subset of the service layer
  • View decorators, a subset of the service layer
  • The repository layer

A Layered PHP Structure

To implement a layered structure, we need a dependency injection container, an object that knows how to instantiate and configure objects. You don’t need to create a class because the framework handles all the magic. Consider the following:

class SiteController extends \Illuminate\Routing\Controller
{
   protected $userService;

   public function __construct(UserService $userService)
   {
       $this->userService = $userService;
   }

   public function showUserProfile(Request $request)
   {
       $user = $this->userService->getUser($request->id);
       return view('user.profile', compact('user'));
   }
}

class UserService
{
   protected $userRepository;

   public function __construct(UserRepository $userRepository)
   {
       $this->userRepository = $userRepository;
   }

   public function getUser($id)
   {
       $user = $this->userRepository->getUserById($id);
       $this->userRepository->logSession($user);
       return $user;
   }
}

class UserRepository
{
   protected $userModel, $logModel;

   public function __construct(User $user, Log $log)
   {
       $this->userModel = $user;
       $this->logModel = $log;
   }

   public function getUserById($id)
   {
       return $this->userModel->findOrFail($id);
   }

   public function logSession($user)
   {
       $this->logModel->user = $user->id;
       $this->logModel->save();
   }
}

In the above example, UserService is injected into SiteController, UserRepository is injected into UserService and the AR models User and Logs are injected into the UserRepository class. This container code is fairly straightforward, so let’s talk about the layers.

The Controller Layer

Modern MVC frameworks like Laravel and Yii take on many of the traditional controller challenges for you: Input validation and pre-filters are moved to another part of the application (In Laravel, it’s in what’s called middleware whereas, in Yii, it’s called behavior) while routing and HTTP verb rules are handled by the framework. This leaves a very narrow functionality for the programmer to code into a controller.

The essence of a controller is to get a request and deliver the results. A controller shouldn’t contain any application business logic; otherwise, it’s difficult to reuse code or change how the application communicates. If you need to create an API instead of rendering views, for example, and your controller doesn’t contain any logic, you just change the way you return your data and you’re good to go.

This thin controller layer often confuses programmers, and, since a controller is a default layer and the top-most entry point, many developers just keep adding new code to their controllers without any additional thinking about architecture. As a result, excessive responsibilities get added, responsibilities like:

  • Business logic (which it makes impossible to reuse business logic code).
  • Direct changes of model states (in which case any changes in the database would lead to tremendous changes everywhere in the code).
  • Model relation logic (such as complex queries, joining of multiple models; again, if something is changed in the database or in the relation logic, we would have to change it in all controllers).

Let’s consider an over-engineered controller example:

//A bad example of a controller
public function user(Request $request)
{
   $user = User::where('id', '=', $request->id)
   ->leftjoin('posts', function ($join) {
       $join->on('posts.user_id', '=', 'user.id')
           ->where('posts.status', '=', Post::STATUS_APPROVED);
   })
   ->first();
   if (!empty($user)) {
       $user->last_login = date('Y-m-d H:i:s');
   } else {
       $user = new User();
       $user->is_new = true;
       $user->save();
   }
   return view('user.index', compact('user'));
}

Why is this example bad? For numerous reasons:

  • It contains too much business logic.
  • It works with the Active Record directly, so if you change something in the database, like rename the last_login field, you have to change it in all controllers.
  • It knows about database relations, so if something changes in database we have to change it everywhere.
  • It’s not reusable, leading to code repetition.

A controller should be thin; really, all it should do is take a request and return results. Here’s a good example:

//A good example of a controller
public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 return view('user.index', compact('user'));
}

But where does all that other stuff go? It belongs in the service layer.

The Service Layer

The service layer is a layer of business logic. Here, and only here, information about business process flow and interaction between the business models should be situated. This is an abstract layer and it will be different for each application, but the general principle is independence from your data source (the responsibility of a controller) and data storage (the responsibility of a lower layer).

This is the stage with the most potential for growth problems. Often, an Active Record model is returned to a controller, and as a result, the view (or in the case of API response, the controller) must work with the model and be aware of its attributes and dependencies. This makes things messy; if you decide to change a relation or an attribute of an Active Record model, you have to change it everywhere in all your views and controllers.

Here’s a common example you might come across of an Active Record model being used in a view:

<h1>{{$user->first_name}} {{$user->last_name}}</h1>
<ul>
@foreach($user->posts as $post)
<li>{{$post->title}}</li> 
@endforeach
</ul>

It looks straightforward, but if I rename the first_name field, suddenly I have to change all views that use this model’s field, an error-prone process. The easiest way to avoid this conundrum is to use data transfer objects, or DTOs.

Data Transfer Objects

Data from the service layer needs to be wrapped into a simple immutable object—meaning it can’t be changed after it is created—so we don’t need any setters for a DTO. Furthermore, the DTO class should be independent and not extend any Active Record models. Careful, though—a business model is not always the same as an AR model.

Consider a grocery delivery application. Logically, a grocery store order needs to include delivery information, but in the database, we store orders and link them to a user, and the user is linked to a delivery address. In this case, there are multiple AR models, but the upper layers shouldn’t know about them. Our DTO class will contain not only the order but also delivery information and any other parts that are in line with a business model. If we change AR models related to this business model (for example, we move delivery information into the order table) we will change only field mapping in the DTO object, rather than changing your usage of AR model fields everywhere in the code.

By employing a DTO approach, we remove the temptation to change the Active Record model in the controller or in the view. Secondly, the DTO approach solves the problem of connectivity between the physical data storage and the logical representation of an abstract business model. If something needs to be changed on the database level, the changes will affect the DTO object rather than the controllers and views. Seeing a pattern?

Let’s take a look at a simple DTO:

//Example of simple DTO class. You can add any logic of conversion from an Active Record object to business model here 
class DTO
{
   private $entity;

   public static function make($model)
   {
       return new self($model);
   }

   public function __construct($model)
   {
       $this->entity = (object) $model->toArray();
   }

   public function __get($name)
   {
       return $this->entity->{$name};
   }

}

Using our new DTO is just as straightforward:

//usage example
public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 return view('user.index', compact('user'));
}

View Decorators

For separating view logic (like choosing a button’s color based on some status), it makes sense to use an additional layer of decorators. A decorator is a design pattern that allows embellishment of a core object by wrapping it with custom methods. It usually happens in the view with a somewhat special piece of logic.

While a DTO object can perform a decorator’s job, it really only works for common actions like date formatting. A DTO should represent a business model, whereas a decorator embellishes data with HTML for specific pages.

Let’s look at a snippet of a user profile status icon that doesn’t employ a decorator:

<div class="status">
   @if($user->status == \App\Models\User::STATUS_ONLINE)
       <label class="text-primary">Online</label>
   @else
       <label class="text-danger">Offline</label>
   @endif   
</div>
<div class="info"> {{date('F j, Y', strtotime($user->lastOnline))}} </div>        

While this example is straightforward, it’d be easy for a developer to get lost in more complicated logic. This is where a decorator comes in, to clean up the HTML’s readability. Let’s expand our status icon snippet into a full decorator class:

class UserProfileDecorator
{
   private $entity;

   public static function decorate($model)
   {
       return new self($model);
   }

   public function __construct($model)
   {
       $this->entity = $model;
   }

   public function __get($name)
   {
       $methodName = 'get' . $name;
       if (method_exists(self::class, $methodName)) {
           return $this->$methodName();
       } else {
           return $this->entity->{$name};
       }
   }

   public function __call($name, $arguments)
   {
       return $this->entity->$name($arguments);
   }

   public function getStatus()
   {
       if($this->entity->status == \App\Models\User::STATUS_ONLINE) {
           return '<label class="text-primary">Online</label>';
       } else {
           return '<label class="text-danger">Offline</label>';
       }
   }

   public function getLastOnline()
   {
       return  date('F j, Y', strtotime($this->entity->lastOnline));
   }
}

Using the decorator is easy:

public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 $user = UserProfileDecorator::decorate($user);
 return view('user.index', compact('user'));
}

Now we can use model attributes in the view without any conditions and logic, and it’s much more readable:

<div class="status"> {{$user->status}} </div>    
<div class="info"> {{$user->lastOnline}} </div>

Decorators also can be combined:

public function user (Request $request)
{
 $user = $this->userService->getUserById($request->id);
 $user = DTO::make($user);
 $user = UserDecorator::decorate($user);
 $user = UserProfileDecorator::decorate($user);
 return view('user.index', compact('user'));
}

Each decorator will do its job and decorate only its own part. This recursive embedding of several decorators allows for a dynamic combination of their features without introducing additional classes.

The Repository Layer

The repository layer works with the concrete implementation of data storage. It’s best to inject the repository through an interface for flexibility and easy replacement. If you change your data storage, you have to create a new repository that implements your repository interface, but at least you don’t have to change the other layers.

The repository plays the role of a query object: It gets data from the database and conducts the work of several Active Record models. Active Record models, in this context, play the role of single data model entities—any object in the system that you care to model and store information about. While each entity contains information, it doesn’t know how it appeared (if it was created or obtained from the database), or how to save and change its own state. The responsibility of the repository is to save and/or update an entity; this provides better separation of concerns by keeping management of entities in the repository and making entities simpler.

Here’s a straightforward example of a repository method that builds a query using knowledge about the database and Active Record relations:

public function getUsers()
{
return User::leftjoin('posts', function ($join) {
    $join->on('posts.user_id', '=', 'user.id')
        ->where('posts.status', '=', Post::STATUS_APPROVED);
   })
   ->leftjoin('orders', 'orders.user_id', '=', 'user.id')
   ->where('user.status', '=', User::STATUS_ACTIVE)
   ->where('orders.price', '>', 100)
   ->orderBy('orders.date')
   ->with('info')
   ->get();
}

Keeping Slim with Single Responsibility Layers

In a newly created application, you’ll only find folders for сontrollers, models, and views. Neither Yii nor Laravel add additional layers in their example application’s structure. Easy and intuitive, even for novices, the MVC structure simplifies work with the framework, but it is important to understand that their sample application is an example; it isn’t a standard or a style, and it doesn’t impose any rules about application architecture. By dividing tasks into separate, single responsibility layers, we get a flexible and extensible architecture that is easy to maintain. Remember:

  • Entities are single data models.
  • Repositories fetch and prepare data.
  • The service layer has only business logic.
  • Controllers communicate with all external sources like user input or a 3rd party service.

So if you start a complex project or a project that has a chance to grow in the future, consider a clear division of responsibilities into the controller, the service, and the repository layers.

About the author

Elvira Sheina, Uzbekistan
member since November 2, 2015
Elvira is a seasoned PHP developer with twelve years of professional experience in PHP, MySQL, PostgreSQL, and JavaScript. She is a dedicated, efficient individual with strong problem-solving and analytical skills. [click to continue...]
Hiring? Meet the Top 10 Freelance PHP Developers for Hire in October 2017

Comments

Grant Wesley Parks
"Business logic" is a bad term. There is domain knowledge and application logic. Domain knowledge belongs in models. Application logic belongs in controllers. That's what the controller is; the essence of the application flow -- it "controls" the flow of that application. A model's domain knowledge is usually transferrable across applications within a given business and belongs in that model.
Hari K T
Thank you for the write up. The title got a bit misleading for I was expecting an article about Slimphp . But later I understood, it is not about slimphp but to make the application slim. It would have been good if the examples code are properly formatted. I believe the example of `DTO` is wrong. The $model->toArray() call makes it array. So ``` public function __get($name) { return $this->entity->{$name} } ``` is wrong, but you could get it as ``` public function __get($name) { return isset($this->entity[$name]) ? $this->entity[$name] : null; } ``` I don't probably get the usage of `make` methods when the constructor itself is public. If you are using something like that I prefer to make constructor as private.
Rick Jolly
Every abstraction layer adds complexity and maintenance costs. The cure to fat models is to split them up. Also, Active Record isn't essential. Parameterized queries are one less abstraction and are often simpler and more readable.
Smychagin Alexey
10 years ago I thought the more code the more layers the better, but now I think this is very unpractical code style. So many classes just for one Entity. I can imagine what the hell is going to be if you have 50 different Entities.
Elvira Sheina
If you have 50 different Entities you probably will have about 10 repositories and at most 5 services. Can you imagine that your Entity needs to be changed and you probably use it at least in 100 places in the code. What is simpler - create 15 addition classes for 50 Entities once or change 100 places every time when something needs to be changed?
Elvira Sheina
This article is about how to not spread parameterized queries everywhere in the code as this makes code dependant from DB structure and hardly changeable. It is better to encapsulate parameterized queries in the Repository layer.
Sergey Poskachey
I agree with the above comments. Every abstraction later brings potential problem in performance. More files for PHP to process, more time for response. So only add an abstraction layer when it's really needed and make sure it won't become the showstopper. Finding a trade off is an art.
Elvira Sheina
In DTO array is converted to object, so that it can lose all relations and functions of AR model. (object) $model->toArray(); It doesn't matter how you create your DTO: new DTO($model) or DTO::make($model) It is just about your own preferences. I think there is no need to make the constructor private, DTO::make($model) is just a convenient wrapper for a constructor call.
Hari K T
oh, I missed to see it was converted to object :-) .
David Bruchmann
The used code is more complex than you describe in your simplified MVC-logic. Above DTOs and Services are used i.e. to keep the controllers slim and to avoid concrete data-fields in the controllers. In your terms "application logic" is handled by services while the controllers just delegate. Apart from that the article is primary about solving a mess of logic and about ideal conditions in the second place. So you're not wrong but just missed a few points of the article.
Jorge S.
Application logic should not belong to controllers. Controllers are just part of the View in the architecture.
Rick Jolly
That would be a simple search and replace and only if a field name required changing. You're not considering the cost to code and maintain these abstraction layers.
David Bruchmann
concerning application logic: Right, thought that would be already clear now. In your second sentence you're mistaking IMHO as the view depends on the logic in the controller, else the word "controller" never had any meaning anymore. A controller-decision also could mean not to use any view.
Jorge S.
I mean the Controller is part of the Presentation Layer for HTTP Requests.
David Bruchmann
Very good article, thank you.
David Bruchmann
I don't mind if you use it like that in your own logic, the schoolbook-like article would condemn it as wrong as controller and view are just two different layers. On the other hand you're somehow right as the sense of the controller is mostly to get any output, so it's more or less the front-side of the app, while the back-side can be bootstrapper, dispatcher etc.
David Bruchmann
if you've changes of data-types it can be much more complicated than search and replace.
Parth Barot
Nice article! This is exactly what Zend 3 framework does. If you need such complexity that means you are dealing with something big and then its better to code it in ready-made - Zend3/Spring MVC and other J2EE frameworks/.NET/DJango using Python - No need to reinvent the wheel and spend time on designing everything from scratch. Thanks
Elvira Sheina
I wrote this article as a proof - it doesn't matter what framework is used, a framework is a tool that can be used in different ways. Good architecture is available in any framework and language. What's more important is that a developer understands what all these layers mean and why he needs it. I've seen a project based on Java Spring framework, and, unfortunately, using it doesn't mean that good architecture is guaranteed.
Elvira Sheina
I believe that messy code that is all in one file brings more problems with maintaining application and makes changes a more error-prone process. A couple of milliseconds worths confidence that everything is working as expected.
Elvira Sheina
I consider here the cost of maintenance of the whole app, that's why I prefer to think about the architecture ahead. Even with field renaming, it can be a problem if for example "title" is used in multiple models like a product, blog post, etc. Wasting time for searching all usages of "title" and trying to understand to which model it belongs to, costs much more than keeping all things clear and flexible from scratch. And I am not saying about moving a field to another table or changing tables relations.
Parth Barot
Exactly, It really does not matter what language or tool you are using as a developer. Only thing matters is to know how things work together, and how can you achieve what you want - plug-n-play design/scalability/automated tests/performance - it could be anything! Thanks
Sergey Poskachey
What I'm saying is don't introduce any abstraction unless really needed for particular case. And don't afraid to refactor. Every complex software will have it's maturity periods. If your code is well organized, adding a new abstraction level at a time is painfull.
Julio Cesar Villalva Trujillo
I wonder, could you build an EntityServiceResolver class and inject that instead so you do now have to manually get the service yourself?
Ramiro Varandas Jr
It's a really good article! I noticed that the UserProfileDecorator needs a ucfirst() call in line $methodName = 'get' . $name;, otherwise in won't be able to find the method.
DevCircus
I utilize Laravel and Slim in various projects depending on my needs. I've reviewed many apps using Laravel, Symfony, Slim, Silex and Cake. The only factor that determines whether or not an app is written badly is the developer. The framework provides the tools needed and it's up to the developer to make wise choices with those tools. I've seen apps written using a micro framework with huge controllers and Laravel apps with concise, focused controllers.
Thomas Radnetter
This is a very good article, thank you for that. At first it seems to be more complicated, but in the long run changes are a piece of cake ;-)
comments powered by Disqus
Subscribe
The #1 Blog for Engineers
Get the latest content first.
No spam. Just great engineering posts.
The #1 Blog for Engineers
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Elvira Sheina
PHP Developer
Elvira is a seasoned PHP developer with twelve years of professional experience in PHP, MySQL, PostgreSQL, and JavaScript. She is a dedicated, efficient individual with strong problem-solving and analytical skills.