Dependency Injection with inherited Controllers in Laravel 5
- Reading time: 3 min
- Published 8 years ago
Usually, when setting up my controllers in a Laravel 5 application, I end up creating one or several base controllers for different entry points (e.g. APIController, FrontendController, ...) While I deem this a good practice, it can lead to problems with Laravel's otherwise fantastic Dependency Injection. To illustrate, take a look at the following:
// FrontendController.php
class FrontendController extends Controller
{
public function __construct(\Illuminate\Contracts\Events\Dispatcher $dispatcher)
{
$dispatcher->listen('composing: frontend.base', function ()
{
// handle root view compose
}
);
}
}
// BlogController.php
class BlogController extends FrontendController
{
protected $repo = null;
public function __construct(TextRepository $repo)
{
$this->repo = $repo;
}
}
As some IDEs and testing tools would say: There are multiple problems!
- The
BlogController
's constructor does not call the parent. - The constructors do neither match nor can all required parameters be handed down.
- Actually there is no number three but lists with two points always seem awkward.
Look at the mess I made!
Not calling the parent constructor which probably does important things? Bad idea. Could be fixed with changing the constructor of BlogController
to:
public function __construct(TextRepository $repo)
{
$this->repo = $repo;
parent::__construct();
}
But then again that's not quite right too, is it? It should be more along the lines of this:
public function __construct(\Illuminate\Contracts\Events\Dispatcher $dispatcher, TextRepository $repo)
{
$this->repo = $repo;
parent::__construct($dispatcher);
}
Which is ugly as hell. Let alone remembering that for all of the other descendants of the FrontendController
.
Let's make that pretty
Right around the time I realized that I didn't want to write that second version even once, I thought "hey, Eloquent models have this boot() method. That could be a way to go."
Said and done. I refactored my code to this:
// FrontendController.php
...
public function __construct(\Illuminate\Contracts\Events\Dispatcher $dispatcher)
{
// the dispatcher stuff
if (method_exists($this, 'boot')) $this->boot();
}
// BlogController.php
public class BlogController extends FrontendController
{
public function boot(TextRepository $repo)
{
$this->repo = $repo;
}
}
Which, of course, did not work. And - as too often is the case - the error was all too obvious to me once I had committed the crime. So, after a while, I figured that if Laravel can resolve class names to inject from type hints, why shouldn't I. Thus followed some research on method reflection and I ended up with this:
// FrontendController.php
...
public function __construct(\Illuminate\Contracts\Events\Dispatcher $dispatcher)
{
// the dispatcher stuff
if (method_exists($this, 'boot'))
{
// resolve the boot dependencies
$reflect = new \ReflectionMethod($this, 'boot');
$reflectedParameters = $reflect->getParameters();
$bootArguments = [];
foreach ($reflectedParameters as $reflectedParameter)
{
preg_match("/.*<required> (.+) \${$reflectedParameter->getName()}/", $reflectedParameter, $typeHint);
if (count($typeHint) == 2)
{
$className = $typeHint[1];
$resolved = app()->make($className);
array_push($bootArguments, $resolved);
}
}
call_user_func_array([&$this, 'boot'], $bootArguments);
}
}
And this, finally, works just as expected. One thing to note though: Bad things will happen if you have anything but type-hinted to-be-resolved classes as attributes to the boot()
method.