Inject container in controller class

4.1k views Asked by At

I'm migrating my app from Slim/3 to Slim/4. Perhaps I'm confused because there're endless syntaxes for the same stuff but I composed this:

use DI\Container;
use Slim\Factory\AppFactory;
use Slim\Psr7\Request;
use Slim\Psr7\Response;

require dirname(__DIR__) . '/vendor/autoload.php';

class Config extends Container
{
}

class Foo
{
    protected $config;

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

    public function __invoke(Request $request, Response $response, array $args): Response {
        var_dump($this->config->get('pi'));
        return $response;
    }
}

$config = new Config();
$config->set('pi', M_PI);
var_dump($config->get('pi'));
AppFactory::setContainer($config);
$app = AppFactory::create();
$app->get('/', \Foo::class);
$app->run();

... and it isn't working as I expected because I get two entirely different instances of the container (as verified by setting a breakpoint in \DI\Container::__construct()):

  1. The one I create myself with $config = new Config();.
  2. One that gets created automatically at $app->run(); and is then passed as argument to \Foo::__construct().

What did I get wrong?

3

There are 3 answers

6
odan On BEST ANSWER

The container attempts to resolve (and create) a new instance of the \DI\Container class, since this is not the interface Slim uses. Instead, try declaring the PSR-11 ContainerInterface. Then the DIC should pass the correct container instance.

Example

use Psr\Http\Message\ServerRequestInterface;

public function __construct(ContainerInterface $container)
{
    $this->container = $container;
}

The same "rule" applies to the request handler interface.

Full example:

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class Foo
{
    private $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function __invoke(
        Request $request,
        Response $response,
        array $args = []
    ): Response {
        var_dump($this->container);
    }
}

Just a last note: Injecting the container is an anti-pattern. Please declare all class dependencies in your constructor explicitly instead.

Why is injecting the container (in the most cases) an anti-pattern?

In Slim 3 the "Service Locator" (anti-pattern) was the default "style" to inject the whole (Pimple) container and fetch the dependencies from it.

The Service Locator (anti-pattern) hides the real dependencies of your class.

The Service Locator (anti-pattern) also violates the Inversion of Control (IoC) principle of SOLID.

Q: How can I make it better?

A: Use composition and (explicit) constructor dependency injection.

Dependency injection is a programming practice of passing into an object it’s collaborators, rather the object itself creating them.

Since Slim 4 you can use modern DIC like PHP-DI and league/container with the awesome "autowire" feature. This means: Now you can declare all dependencies explicitly in your constructor and let the DIC inject these dependencies for you.

To be more clear: "Composition" has nothing to do with the "Autowire" feature of the DIC. You can use composition with pure classes and without a container or anything else. The autowire feature just uses the PHP Reflection classes to resolve and inject the dependencies automatically for you.

2
Nima On

This happens as a result of how PHP-DI auto-registers itself. As of writing this answer, PHP-DI container auto-registers itself to the key DI\Container, and also the three implemented interfaces, on creation (see these lines of Container.php). As a result, if you type hint your constructor parameter against DI\Container or one of the three interfaces it implements, (which includes Psr\Container\ContainerInterface), PHP-DI is able to resolve itself.

ُThe problem is the use of self::class (line 110 of that file) makes DI\Container key somehow hard-coded, so although you're creating a child class of DI\Container (Config) the container still registers to same key as before. One way to overcome this is to let the container know that Config should also be resolved to itself. I see two options for this:

  1. To register the container to same key as its class name, like what DI\Container does (This seems to be the right way to do it)
  2. Manually registering the container after instantiating it

Here is a fully working example:

<?php
require '../vendor/autoload.php';
use DI\Container;
use Slim\Factory\AppFactory;

use Psr\Container\ContainerInterface;
use DI\Definition\Source\MutableDefinitionSource;
use DI\Proxy\ProxyFactory;

class Config extends Container
{
    public function __construct(
        MutableDefinitionSource $definitionSource = null,
        ProxyFactory $proxyFactory = null,
        ContainerInterface $wrapperContainer = null
    ) {
        parent::__construct($definitionSource, $proxyFactory, $wrapperContainer);
        // Register the container to a key with current class name
        $this->set(static::class, $this);
    }
}

class Foo
{
    public function __construct(Config $config)
    {
        die($config->get('custom-key'));
    }
}

$config = new Config();
$config->set('custom-key', 'Child container can resolve itself now');
// Another option is to not change Config constructor,
// but manually register the container in intself with new class name
//$config->set(Config::class, $config);
AppFactory::setContainer($config);
$app = AppFactory::create();
$app->get('/', \Foo::class);
$app->run();

Please note: As best practices suggest, you should not type hint against a concrete class (DI\Container or your Config class), instead you should consider type hinting against the interface (Psr\Container\ContainerInterface).

1
Álvaro González On

The problem is a misuse of a PHP-DI feature called autowiring:

Autowiring is an exotic word that represents something very simple: the ability of the container to automatically create and inject dependencies.

In order to achieve that, PHP-DI uses PHP's reflection to detect what parameters a constructor needs.

If you use a factory method to create the container you can disable autowiring and the "strange" behaviour stops:

$builder = new ContainerBuilder(Config::class);
$builder->useAutowiring(false);
$config = $builder->build();

But I guess a better solution is to learn how to use autowiring properly :)

I had overlooked all these details because my code was originally written for Slim/3, which used Pimple as hard-coded default container. I had wrongly assumed they would work similarly but, albeit being container solutions, both libraries are quite different.