Laravel is traditionally an MVC framework but MVC just doesn’t seem to scale for larger projects. Typically what ends up happening is logic is crammed into each section: models, views and controllers as the application grows which becomes next to impossible to test. Taylor Otwell’s book mentions the repository pattern but even that doesn’t really solve this scalability problem. After a colleague showed me a blog post about the Hexagonal Pattern in rails, I decided to try the design in Laravel - the results were nice.
The basic idea is that routes and controllers are just for delivery. There is no logic in a controller, period.
A controller is much like your UPS man. He is given an address (the route) and delivers a box (model+view) to your address. He doesn't need worry about the contents of the box.
But then you say, the UPS guy still makes decisions. For example, if it is currently raining, the box might end up in your car otherwise probably on your front porch.
So where then do you put your logic? I call them *scenarios* but they are also called *use cases*. Uncle Bob calls them *interceptors*.
So let’s use our concept of a Scenario with our contrived UPS example. Instead of the UPS guy trying to figure out what to do with the box, he simply follows a strict set of predetermined guidelines given the scenario. He makes no assumptions and does exactly as he is told. So in the scenario in which it is raining, his rules are to leave a pretty note explaining the situation and not leave the package in the rain. This means the UPS guy is not responsible for any decisions. He is just a delivery mechanism following orders.
Implementation
Below are two classes: UserController and UserScenario. This walks through how a user might be created in Laravel using our Hexagonal pattern with Scenarios.
class UserController extends BaseController implements HexagonalInterface
{
public function __construct(UserScenario $scenario)
{
$this->scenario = $scenario;
$scenario->setDelegate($this);
}
public function create()
{
$user = $this->scenario->emptyUser();
$this->layout->nest('content', 'user.create', compact('user'));
}
public function store()
{
return $this->scenario->createUser(Input::get());
}
protected function createInvalid($validator, $input)
{
return Redirect::action('UserController@create')->withErrors($validator)->withInput($input);
}
protected function createSuccess($user)
{
Auth::loginUsingId($user->id);
return Redirect::action('HomeController@dashboard');
}
}
That handles the controller aspect of this but you’re probably wondering what a UserScenario
looks like.
class UserScenario extends BaseScenario
{
public function __construct(User $user)
{
parent::__construct();
$this->user = $user;
}
public function emptyUser()
{
return $this->user;
}
public function createUser($input)
{
$validation = $this->validator($input, $this->user->creationRules);
if ($validation->fails()) {
return $this->invoke('createInvalid', [$validation, $input]);
}
$this->user->create($input);
return $this->invoke('createSuccess', [$user]);
}
}
You might be wondering what the invoke()
function is? It comes from my BaseScenario class.
class BaseScenario
{
protected $delegate = null;
public function __construct(Input $input = null)
{
$this->input = $input ?: App::make('Input');
}
protected function validator($input, $rules, $messages = [])
{
return Validator::make($input, $rules, $messages);
}
protected function invoke($methodName, $args)
{
$obj = $this->getDelegate();
$method = new ReflectionMethod(get_class($obj), $methodName);
$method->setAccessible(true);
return $method->invokeArgs($obj, $args);
}
public function getDelegate()
{
return $this->delegate);
}
public function setDelegate(HexagonalInterface $delegate)
{
$this->delegate = $delegate;
}
}
The reflection allows me to see the actions which should not have routes tied to them, i.e. createInvalid
should not have a route and thus is a protected function. So there you have it, logic-less controllers using Scenarios.
Hopefully I didn’t scare you off with this post. There are still two more posts to go! In the next post I will be talking about how to keep Laravel views simple and clean using macros.