Dependency Injection with inherited Controllers in Laravel 5

  • Reading time: 3 min
  • Published 4 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!

  1. The BlogController's constructor does not call the parent.
  2. The constructors do neither match nor can all required parameters be handed down.
  3. 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.