DI Container - correct way of doing it?

I have a long list of dependency injections to display a page with an article, navigation, etc. And currently I put them in a file called index.php to glue them together.

index.php,

use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
use MyCustomVerndorName\Slug\Mapper\SlugMapper;
.... (more)

$ConstantService = new ConstantService();
$ConstantController = new ConstantController();

$ArticleService = new ArticleService();
$ArticleController = new ArticleController();

// Prepare Nav model.
$NavModel = new NavModel();
$NavMapper = new NavMapper($PdoAdapter);
$NavService->setMapper($NavMapper)->setModel($NavModel);

// Prepare Article model.
$ArticleModel = new ArticleModel();
$ArticleMapper = new ArticleMapper($PdoAdapter);

// Prepare components.
$ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
... (more)

// Inject components.
$ArticleMapper->addComponent($ArticleContentComponent);
... (more)

$NavChildrenComponent = new NavChildrenComponent($PdoAdapter);
... (more)

// Inject components.
$NavMapper->addComponent($NavChildrenComponent);
$NavMapper->addComponent($NavLanguageComponent);

// Controll the slug.
$SlugController->setService($SlugService)->fetchRow([
    "url"   =>  $url
]);

// Control the nav.
$NavController->setService($NavService)->fetchRows();

// Controll the article.
$ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
$ArticleController->setService($ArticleService)->fetchRow([
    "url"   =>  $url
]);

// Prepare template.
$PageTemplate = new PageTemplate();

// Prepare view.
$ArticleView = new ArticleView($PageTemplate, $ArticleModel);
$ArticleView->setSlug($SlugModel);
$ArticleView->setNav($NavModel);

// Render the view.
echo $ArticleView->render('index.php');

in my router.php (I’m using AltoRouter),

use AltoRouter as Router;

$Router = new Router();.
$Router->map('GET', '/', '/article/container');
... (and other routes)

if($match)
{
    $target = $match['target'];
    $url = isset($match['params']['url']) ? $match['params']['url'] : DEFAULT_HOMEPAGE;
    $language = isset($match['params']['language']) ? $match['params']['language'] : null;

    include __DIR__ . $target . '.php';
    
}

I’m thinking to make the list of dependency injections in index.php into a container class so I can call this class whenever it is needed,

For instace in the router.php,

$Router->map('GET', '/', 'MyCustomVerndorName\Article\Container\ArticleContainer');
....
new $target($PdoAdapter, $url, $language);

And this container class,

namespace MyCustomVerndorName\Article\Container;

// Mapper.
use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
...

class ArticleContainer
{
    
    /*
     * Construct dependency.
     */	
    public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
    {
        $ConstantService = new ConstantService();
        $ConstantController = new ConstantController();

        // Slug.
        $SlugService = new SlugService();
        $SlugController = new SlugController();

        // Nav.
        $NavService = new NavService();
        $NavController = new NavController();

        // Article.
        $ArticleService = new ArticleService();
        $ArticleController = new ArticleController();

        // Prepare Article model.
        $ArticleModel = new ArticleModel();
        $ArticleMapper = new ArticleMapper($PdoAdapter);

        // Prepare components.
        $ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
        ...
        
        // Inject components.
        $ArticleMapper->addComponent($ArticleContentComponent);
        ...

        // Control the nav.
        $NavController->setService($NavService)->fetchRows();

        // Controll the article.
        $ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
        $ArticleController->setService($ArticleService)->fetchRow([
            "url"   =>  $url
        ]);

        
        // Prepare template.
        $PageTemplate = new PageTemplate();

        // Prepare view.
        $ArticleView = new ArticleView($PageTemplate, $ArticleModel);
        $ArticleView->setSlug($SlugModel);
        $ArticleView->setNav($NavModel);

        // Render the view.
        echo $ArticleView->render('index.php');
    }
}

Basically, I put all the dependency list into __construct,

public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
{
...
}

So, is this the correct way of doing a DI Container without relying on the well known containers out there for PHP (such as Pimple, Symfony\DependencyInjection, etc)?

It’s a good use of dependency injection, but it’s not really a DI container. One of the fundamental features of a DI container is that it stores shared instances. That’s why it’s called “container”. It also shouldn’t be doing any of your view rendering. Those are definitely different responsibilities.

Here’s just an example of a rudimentary DI container.

<?php

/**
 * Manages object creation, configuration, lifetime, and dependencies.
 */
class ServiceContainer
{
    private $parameters;
    private $singletons = [];

    public function __construct($parameters = [])
    {
        $this->parameters = $parameters;
    }

    public function getPdoAdapter()
    {
        if (!isset($this->singletons['pdo_adapter'])) {
            $pdoAdapter = // ... construct your PDO adapter

            // Save instance
            $this->singletons['pdo_adapter'] = $pdoAdapter;
        }

        return $this->singletons['pdo_adapter'];
    }

    public function getArticleController()
    {
        $ArticleController = new ArticleController();
        $ArticleController->setService($this->getArticleService())

        return $ArticleController;
    }

    public function getArticleService()
    {
        if (!isset($this->singletons['article_service'])) {
            $ArticleService = new ArticleService();
            $ArticleService
                ->setMapper($this->getArticleMapper())
                ->setModel($this->getArticleModel())
                ;

            $this->singletons['article_service'] = $ArticleService;
        }

        return $this->singletons['article_service'];
    }

    public function getArticleMapper()
    {
        $ArticleMapper = new ArticleMapper($this->getPdoAdapter());
        $ArticleMapper->addComponent($this->getArticleContentComponent());

        return $ArticleMapper;
    }

    public function getArticleModel()
    {
        return new ArticleModel();
    }

    public function getArticleContentComponent()
    {
        return new ArticleContentComponent($this->getPdoAdapter());
    }

    public function getArticleView()
    {
        $ArticleView = new ArticleView($this->getPageTemplate(), $ArticleModel);
        $ArticleView->setSlug($SlugModel);
        $ArticleView->setNav($NavModel);

        return $ArticleView;
    }

    public function getPageTemplate()
    {
        return new PageTemplate();
    }
}
2 Likes

@Jeff_Mott Thanks for the help. Can I ask - how do I use ServiceContainer to render the page with the requested information then?

$container = new ServiceContainerr($url, $language);

$container->getPdoAdapter();
$container->getArticleController();
$container->getArticleService();
... and so on

Am I right?

You should only need to get the controller. If the controller has any dependencies, such as on PDO or ArticleService, then the container injects those. Then once you have your controller instance, do whatever it is you do to invoke the action.

// Hypothetical example
$controller = $container->getArticleController();
$response = $controller->handleRequest($request);
1 Like

how do I use ServiceContainer to render the page with the requested information then?

You shouldn’t the container should only be concerned with constructing an object with all its dependencies.

Jeff’s example is a great start. The next step is making the container extensible. At the moment you have to manually code a specific function inside the container for each object it creates. While it works for a single project, if you want to use the container on projects where the objects may be configured differently it’s not very useful. The next evolutionary step is allowing the container to be configurable:

The container:

class ServiceContainer {
	private $callbacks = [];
	private $singletons = [];

	public function register($name, $callback) {
		$this->callbacks[$name] = $callback;
	}

	public function get($name) {
		return $this->callbacks[$name]();
	}

	public function getSingleton($name) {
		return $this->singletons[$name];
	}

	public function setSingleton($name, $obj) {
		$this->singletons[$name] = $obj;
	}

	public function hasSingleton($name) {
		return isset($this->singletons[$name]);
	}
}

You’ll notice this doesn’t contain any knowledge of any of the classes it manages, only some generic getters/setters. This can then be used by the application which can provide its own specific implementations for each object:

$container = new ServiceContainer();
$container->register('ArticleService', function() use ($container) {
	if (!$container->hasSingleton('ArticleService')) {
		$ArticleService = new ArticleService();
   		$ArticleService
               ->setMapper($container->get('ArticleMapper'))
               ->setModel($container->get('ArticleModel'));

               $container->setSingleton('ArticleService', $ArticleService);
	}
	
     return $container->getSingleton($ArticleService);
});

$container->register('ArticleController', function() use ($container) {
	 $ArticleController = new ArticleController();
         $ArticleController->setService($container->get('ArticleService'));
         return $ArticleController;
});

This is basically a limited version of Pimple.

The problem with this approach, of course, is that it’s requires configuration for each possible dependency. If you have 100 classes managed by the container you need to register all 100 of those classes with the container.

The next step forward is to use type hinting to work out what dependencies are required.

Given the class:


class ArticleController {

	public function __construct(ArticleService $service) {
		
	}
}

You can use reflection ( http://php.net/manual/en/reflectionparameter.getclass.php ) to infer that the ArticleController class requires an instance of ArticleService and omit all that configuration code. Shameless plug: This is what my own container Dice and others such as Orno do.

3 Likes

@TomB thanks for the help.

It seems that having a container is ‘tedious’. What is the difference between the way I glue the classes together in index.php and a container then? Glue them in index.php seems a bit easier and intuitive, doesn’t it?

What are the benefits of container at all? I really fail to see any…

There are three benefits that we can highlight from @TomB 's code straight off the bat (Nice job btw)

The first is that we gain the ability to “lazy load” everything through closures. For example

We’re registering the closure (anonymous function) on the container under the ‘ArticleController’ key - the actual ArticleController object doesn’t get instantiated until it’s asked for. This is a great performance benefit - you only use the resources that you need to process a request and serve the response.

The second benefit is the manner in which singletons are provided for. It used to common (still is) for a singleton to be implemented via a static method (typically called ‘getInstance’). But singletons accessed via a static method are difficult to create mock objects for when unit testing. Code that refers directly to the static getInstance method of a singleton automatically means that the singleton is going to get tested along with the code, unless you write the corresponding setInstance method to specifically pass in a mock. Since the database connection is probably the most common singleton going, you need to do this - unit testing that relies on the database is a right royal pain in the butt.

Since you can set the intended singleton into the container without writing any extra code, you can also set a mocked singleton instance and consequently allow your unit tests to only test the bits that you need testing. For the database connection singleton (mock) this is particularly important - unit tests run much much faster without dragging a database behind them.

The last one is significant

By using reflection, the DIC can inspect the thing that your asking for, in this case the ArticleController, and see that it needs an ArticleService instance to be able to work (i.e. the ArticleController typehints for an ArticleService on its constructor). The DIC will fetch an ArticleService instance and construct (new) the ArticleController with it before passing it back.

As Tom said, that let’s you miss out a great deal of that messy configuration code.

TL;DR: Lazy load all the things. Easily stub/mock the singletons. Use typehinting to get the object instances you need already set up with the dependencies that they need injected into them.

3 Likes

How do I use reflection to fetch an ArticleService instance and construct (new) the ArticleController?

It has only a few examples at http://php.net/manual/en/reflectionparameter.getclass.php …

It’s fairly straight forward:

<?php
class ArticleController {

	public function __construct(ArticleService $service) {
		
	}
}

class ArticleService {

}


//Reflect the class
$reflection = new ReflectionClass('ArticleController');
//Get the constructor
$constructor = $reflection->getConstructor();

//Check to see if their is a constructor
if ($constructor) {

	//Loop through the constructor's parameters
	foreach ($constructor->getParameters() as $param) {
		//Echo the parameter's class name
		echo $param->getClass()->name;
	}
}
2 Likes

Thanks Tom!

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.