Search code examples
phplaravelinversion-of-control

What's the difference between Laravel automatic injection and manually specifying the dependencies in the constructor body?


I'm using a Repository pattern in my Laravel project. This pattern is not really explained in the official documentation, except for this snippet:

You may type-hint a repository defined by your application in a controller's constructor. The repository will automatically be resolved and injected into the class.

This is my code, in accordance with the documentation:

class CategoriesController extends Controller
{
    protected $repo;

    public function __construct(CategoriesRepository $repo)
    {
        $this->repo = $repo;
    }

I've type-hinted the CategoriesRepository so it gets automatically loaded by the Service Container.

However, if I directly create a new instance of the CategoriesController class (without using the Service Container), I have to specify that I need a new instance of the CategoriesRepository too, like this:

$example = new CategoriesController(new CategoriesRepository());

Now, let's suppose I write the following code.

class CategoriesController extends Controller
{
    protected $repo;

    public function __construct()
    {
        $this->repo = new CategoriesRepository();
    }

This way, I don't have to load the class through the Service Container, nor call it by passing a new instance of CategoriesRepository as the argument, because it's automatically created inside of the constructor.

So, my question is: would this be bad practice? What's the difference between type-hinting as a parameter and creating a new instance inside of the constructor?


Solution

  • Here's the beauty of dependency injection:

    Complex initialization

    class MyController {
    
         public function __construct(A $a) { }
    }
    
    class A {
         public function __construct(B $b) { }
    }
    class B {
         public function __construct(C $c) { }
    }
    
    class C {
         public function __construct(D $d) { }
    }
    class D {
         public function __construct() { }
    }
    

    Now you can ask laravel to create that class for you e.g:

    $controller = make(MyController::class);
    

    or you can do:

    $controller = new MyController(new A(new B(new C(new D())))));
    

    In addition you can specify more complex rules on how to create the variables:

    app()->bind(D::class, function ($app) {
           $d = new D();
           $d->setValueOfSomething($app->make(AnotherClass::class));
           return $d;
    });
    

    Testing

    That's one advantage of dependency injection over manual creation of things. Another is unit testing:

        public function testSomeFunctionOfC() {
            $this->app->bind(D::class, function () {
                   $dMock = $this->createMock(D::class);
            });
            $c = make(C::class);
        }
    

    Now when you create C the class D will be the mocked class instead which you can ensure works according to your specification.