MVC frameworks are everywhere. We use them for back-end development and front-end development. They tend to solve a lot of problems, eliminate repetitive tasks, help developing faster, and make you a better developer. However, sometimes we find ourselves in the framework way of doing things and forget that not every situation is a perfect model-view-controller situation and the object-oriented principles that we used to.

How to solve that

To put it simply, using the Repository Pattern.

I'll be using Laravel's controllers to demonstrate the pitfalls. However, the concept applies to the controller part of any model-view-controller framework.

Here we have a user controller, and in the store method, we need to register a new user, and if that user has been created, we need to send a welcome email to him/her.

class UserController extends Controller
{
  public function store(Request $request)
  {
    $user = new User($request->all());

    if ($user->save()) {
      Mail::queue('user.welcome', compact('user'), function($message) use ($user) {
        $message->to($user->email)->subject('Welcome to Our Application');
      });

      return redirect()->route('login')->with('success', 'User has been created successfully.');
    } else {
      return redirect()->route('register')->with('error', 'Sorry! We faced an internal problem, please try again later.');
    }
  }
}

Now in a real world application, you will need to validate the data of course, but this is a demo, and that's why we skipped that part.

In the store method which is based off an original route, we are creating a new user, and if that user has been set up, send a welcome email and redirect to the login page with a success message. If the user couldn't be created, just redirect to the registration page with an error message.

This seems ok since the business logic is not that nested and that we are using it for a web application. However, we might need to execute that on the command line, say for testing purposes or what have you, then this needs to arise differently.

class UserCreator
{
private $errors = [];

public function getErrors()
{
  return $this->errors;
}

public function isSuccess()
{
  return empty($this->getErrors());
}

  public function createUser($data)
  {
    $user = new User($data);

    if ($user->save()) {
      Mail::queue('user.welcome', compact('user'), function($message) use ($user) {
        $message->to($user->email)->subject('Welcome to Our Application');
      });
    } else {
      $this->error[] = 'No user created!';
    }

    return $user;
  }
}

We moved our business logic to a new class called UserCreator, you can name it whatever you like, in this case, it does make sense to have this name. Now let's reflect the changes on our controller.

class UserController extends Controller
{
  public function store(Request $request)
  {
    $creator = new UserCreator();

    $user = $creator->createUser($request->all());

    if ($creator->isSuccess()) {
      return redirect()->route('login')->with('success', 'User has been created successfully.');
    } else {
      return redirect()->route('register')->with('error', 'Sorry! We faced an internal problem, please try again later.');
    }
  }
}

Using the methods that we have on the UserCreator class, we were able to shorten our controller.

Notice how the controller is now slim and tidier, and it didn't take much effort to do so. More importantly, it's reusable and definitely more testable.

When should I use it?

The above method tends to abstract your code and put a layer between the business logic and the framework way of doing things which ensure that you won't face problems when the business logic changes or challenges while testing your code. However, not all projects need it, and it could be overkill for small projects. It's your call to see where you need to apply it and where not.