Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency injection on custom classes stuck in indefinite loop in L4.2?

I've been changing my Controllers and helper classes to use dependency injection, and it seems I've got my helper classes stuck in an infinite loop.

Below is my custom ServiceProvider and the two sample helper classes. As you can see, they inject each other, so they keep going back and forth.

What's the solution to this issue? What mistake do I seem to be making? What can I do so that I can run tests on helper classes like General and Person, while mocking the helper classes that are called from inside of them?

One way that I guess could work is in my ServiceProvider, do the following:

if (isset($appmade->General)) { 
    // inject the General app that's already instantiated 
} else { 
    $abc = app::make('\Lib\MyOrg\General'); 
    $appmade->General = $abc; 
} 

Is that the correct way?

// /app/providers/myorg/MyOrgServiceProvider.php

namespace MyOrg\ServiceProvider;
use Illuminate\Support\ServiceProvider;
class MyOrgServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('\Lib\MyOrg\General', function ($app) {
            return new \Lib\MyOrg\General(
                $app->make('\Lib\MyOrg\Person'),
                $app->make('\App\Models\User')
            );
        });

        $this->app->bind('\Lib\MyOrg\Person', function ($app) {
            return new \Lib\MyOrg\Person(
                $app->make('\Lib\MyOrg\General'),
                $app->make('\App\Models\Device')
            );
        });
    }
}

// /app/libraries/myorg/general.php

namespace Lib\MyOrg;
use App\Models\User;
use Lib\MyOrg\Person;
class General
{
    protected $model;
    protected $class;

    public function __construct(Person $personclass, User $user) 
    {
    }
}

// /app/libraries/myorg/person.php

namespace Lib\MyOrg;
use App\Models\Device;
use Lib\MyOrg\General;
class Person
{
    protected $model;
    protected $class;

    public function __construct(General $generalclass, Device $device) 
    {
    }
}
like image 957
Luke Shaheen Avatar asked Oct 21 '14 21:10

Luke Shaheen


1 Answers

Your helper classes have a case of circular dependency which, when mixed with dependency injection, make them un-instantiable...and there's no good way to get them to work and be easily testable.

There are some hack-y ways to get this to work:

  • You could use setter injection, injecting the dependency in a setter method when the helper is used, rather than in the constructor when it is instantiated. This has many disadvantages, poor testability being chief among them. (See http://symfony.com/doc/current/components/dependency_injection/types.html)

  • In Laravel 5, you could use method injection, particularly if the dependency is only pertinent to one or two methods. (See http://mattstauffer.co/blog/laravel-5.0-method-injection)

  • You could do an existence check like you propose above, or something with an event listener. But all of this is masking the fact that...

A circular dependency usually indicates that a design change is in order, and is best solved by reconsidering how your classes interact with each other.

  • If your Person and General classes depend fully on each other, then it's possible they share a single responsibility and should be combined into a single class.

  • If, however, they rely on a subset of each other's functionality, then there's probably a separate class with its own responsibility hiding in there somewhere, and then the best solution would be extract the common functionality into a 3rd class. That way, rather than having Class A rely on Class B and Class B rely on Class A, both Classes A and B would instead rely on Class C. You can safely instantiate them all, and they all remain easily testable.

How can you easily figure out what your new Class C should contain? A great rule of thumb comes from http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/... "List all of the methods in your class A used by class B, and all the methods in your class B used by class A. The shorter of the two lists is your hidden Class C."

like image 178
damiani Avatar answered Oct 15 '22 12:10

damiani