delta9
delta9

Reputation: 403

PHP Dependency Injection and Loose Coupling

I am pondering a few different approaches here and would really appreciate some input! I am considering the two choices below. There are 2 things going on there I have questions on.

  1. Is it preferred to inject the dependencies into the constructor of the main "container" class, or to instead create new instances inside the container class?

  2. In the second example, the class' dependencies are injected via constructor and then maintained within via a property of the class. Then when the methods (route(), render()) are called, the dependencies are called from within. I began with this approach, but am now favoring something more along the lines of the first example. I think the first example is preferable, but are there any benefits to using the DI approach in the second example?

There really is no need to store anything in the class as a property. I can probably rearrange everything to use that technique without much trouble, and I think I like it better. This way I can also move all of the of work out of the constructors, and simply access everything via method later. Am I on the right track here?

class App
{
    private $config;
    private $router;
    private $renderer; 

    public function __construct(IConfig $config, IRouter $router, IRenderer $renderer)
    {
        $this->config = $config;
        $this->router = $router;
        $this->renderer = $renderer;

        $this->run();           
    }

    public function run()
    {
        $data = $this->router->route(new Request, $config->routes);         
        $this->renderer->render($data);
    }   

}


class App
{
    private $config;
    private $router;
    private $renderer; 

    public function __construct()
    {
        $this->config = new Config;

        $this->run();               
    }

    public function run()
    {
        $this->router = new Router(new Request, $config->routes);
        $this->router->route();

        $this->renderer = new Renderer($this->router->getData());
        $this->renderer->render();
    }

}

Upvotes: 7

Views: 3428

Answers (2)

Elias Van Ootegem
Elias Van Ootegem

Reputation: 76408

While Orangepill makes a good point, I thought I'd chip in, too. I tend to define my constructors with a clear constructor, too, but I don't expect the required objects to be passed when creating an instance.
Sometimes, you create an instance that retrieves data either from a DB, or some sort of Http request. In your case, the first example expects three dependencies to be passed, but who's to say that you'll always need all three of them?

Enter Lazy-Loading. The code sample below is quite lengthy, but it is (IMO) well worth looking into. If I use a service, I don't want to load all dependancies unless I'm sure I'll be using them. That's why I defined the constructor so that I can create an instance in either one of the following ways:

$foo = new MyService($configObj);
$bar = new MyService($configObj, null, $dbObj);//don't load curl (yet)
$baz = new MyService($configObj, $curlObj);//don't load db (yet)

If I wanted to run some test, I can still inject the dependencies when constructing my instance, or I can rely on a test-config object or I could use the setDb and setCurl methods, too:

$foo->setCurl($testCurl);

Sticking to the first way of constructing the instance, I can safely say that, if I only invoke the getViaCurl method, the Db class will never be loaded.
The getViaDb method is a bit more elaborate (as is the getDb method). I don't recommend you working with methods like that, but it's just to show you how flexible this approach can be. I can pass an array of parameters to the getViaDb method, which can contain a custom connection. I can also pass a boolean that'll control what I do with that connection (use it for just this one call, or assign the connection to the MyService instance.

I hope this isn't too unclear, but I am rather tired, so I'm not all too good at explaining this stuff ATM.
Here's the code, anyway... it should be pretty self explanatory.

class MyService
{
    private $curl = null;
    private $db = null;
    private $conf = null;
    public function __construct(Config $configObj, Curl $curlObj = null, Db $dbObj = null)
    {
        $this->conf = $configObj;//you'll see why I do need this in a minute
        $this->curl = $curlObj;//might be null
        $this->db = $dbObj;
    }

    public function getViaCurl(Something $useful)
    {
        $curl = $this->getCurl();//<-- this is where the magic happens
        return $curl->request($useful);
    }

    public function getViaDb(array $params)
    {
        if (isset($params['custom']))
        {
            $db = $this->getDb($params['custom'], $params['switch']);
        }
        else
        {//default
            $db = $this->getDb();
        }
        return $db->query($params['request']);
    }

    public function getCurl()
    {//return current Curl, or load default if none set
        if ($this->curl === null)
        {//fallback to default from $this->conf
            $this->curl = new Curl($this->conf->getSection('CurlConf'));
        }
        return $this->curl;
    }

    public function setCurl(Curl $curlObj)
    {//inject after instance is created here
         if ($this->curl instanceof Curl)
         {//close current connection
             $this->curl->close();
         }
         $this->curl = $curlObj;
    }

    public function setDb(Db $dbObj)
    {
        if ($this->db instanceof Db)
        {//commit & close
            $this->db->commit();
            $this->db->close();
        }
        $this->db = $dbObj;
    }

    //more elaborate, even:
    public function getDb(Db $custom = null, $switch = false)
    {
        if ($custom && !!$swith === true)
        {
            $this->setDb($custom);
            return $this->db;
        }
        if ($custom)
        {//use custom Db, only this one time
            return $custom;
        }
        if ($this->db === null)
        {
            $this->db = new Db($this->conf->getSection('Db'));
        }
        return $this->db;
    }
}

Upvotes: 2

Orangepill
Orangepill

Reputation: 24655

It is better to inject dependencies into the constructor.

Creating instances within the constructor creates a tight coupling between the two classes. With a constructor with a clear signature like

 public function __construct(IConfig $config, IRouter $router, IRenderer $renderer)

I can immediately tell what this component needs to do it's job.

Given a constructor like

public function __construct(); 

There is no clue what the component needs to function. It creates a strong coupling to specific implementations of your each your router, your request and to your renderer, none of which are apparent until you dig down into the guts of your class.

In summary the first approach is well documented, extendable and testable. the second approach is opaque, highly coupled, and not easily testable.

Upvotes: 8

Related Questions