0

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?

Álvaro González
  • 142,137
  • 41
  • 261
  • 360
  • I tried your code and called `$config->set('sample-key', 'sample-value');` before passing it to `AppFactory::setContainer($config);`, and `var_dump` in your controller printed this key. So to me they seem to be the same instance. Or did I miss something here? – Nima Aug 23 '19 at 07:13
  • I'm curious, where is _the other_ container? The current code only dumps one container, thus there is nothing else to compare to. And another note, I read Slim code and I think if you don't provide an already created instance, Slim does not create a container (an implementation of `\Psr\Container\ContainerInterface`) at any point. So _the one that gets created automatically_ is ambiguous. – Nima Aug 23 '19 at 10:56
  • Thanks @Nima The code is working as expected. I deleted my answer. – odan Aug 23 '19 at 11:05
  • This is embarrassing. While simplifying my code for the question I apparently removed whatever was wrong. I'm looking at it right now and I'll either fix the test case or remove the question altogether. Sorry! – Álvaro González Aug 23 '19 at 14:45
  • @Nima I forgot to include the other `var_dump()` in the question, though I actually used Xdebug in my initial diagnostics. I've edited the question to fix that and other blatant errors. – Álvaro González Aug 23 '19 at 15:08
  • @odan I've fixed my question with a valid test case. And I can confirm that your answer was basically correct. The problem is fixed by declaring `\Foo::__construct()`'s parameter as `Container` rather than `Config`. – Álvaro González Aug 23 '19 at 15:08

3 Answers3

4

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.

odan
  • 4,757
  • 5
  • 20
  • 49
  • This does not seem to be accurate. `\DI\Container` implements `Psr\Container\ContainerInterface`, so the DI should be able to resolve it. Also, the DI will try to resolve objects based on type-hinting in the constructor definition, not what Slim uses. So no matter if you type-hint Foo constructor with `\DI\Container' or `Psr\Container\ContainerInterface` the DI is able to resolve the requested object (I tested this and DI is able to do so). The most important missing fact here is that OP's code is working as expected. Did you test it and get two instances of the container? – Nima Aug 23 '19 at 10:20
  • Nima has a point and as I study further I get increasingly confused. In my (corrected) code there's `\Config extends \DI\Container implements \Psr\Container\ContainerInterface`. I don't know what Slim/4 expects from me (this is Slim/3 + Pimp code being migrated to Slim/4 + PHP-DI). It makes perfect sense that `Foo::__construct(\DI\Container $config)` works with `\Config` but... Why does `Foo::__construct(\Config $config)` cause that the original `\Config` object is discarded in favour of a brand new instance... of the same class! – Álvaro González Aug 23 '19 at 16:17
  • @ÁlvaroGonzález I don't think it is what _Slim expects you_, it is related to how PHP-DI resolves dependencies. To be specific, how it resolves **itself**. I think it's better to post what I found as an answer not a comment. – Nima Aug 23 '19 at 18:19
  • Basically, and instance of PHP-DI only knows that it should resolve these four keys to itself : `DI\Conainer`, `Psr\Container\ContainerInterface`, `DI\FactoryInterface` and `Invoker\InvokerInterface`. If you need to type hint the container against any other class/interface, you'll need to let the container know it should be resolved to itself. This also makes my first comment invalid: just because a class implements some interface dose not help the container to resolve them. – Nima Aug 23 '19 at 21:41
  • @odan If it's not too much to ask, I could use a clarification about the **anti-pattern** comment. My code was original written as per the instructions in the Slim/3 user guide: 1) Have settings and dependencies in a container. 2) Pass the container as argument to App constructor. 3) Define route handlers as class references (`Foo::class` or `Foo::class . ':action'`). 4) Get container in route handler constructor. — Did it turned out to be a bad idea? Or perhaps I misunderstood something? – Álvaro González Aug 24 '19 at 08:02
  • @ÁlvaroGonzález I updated my answer. 1-2) good, 3) a matter of taste, I would use single action controllers. 4) Don't pass the container, pass only the dependencies your class needs (and let the DIC inject this dependecies for you). – odan Aug 24 '19 at 09:51
1

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).

Nima
  • 3,309
  • 6
  • 27
  • 44
  • To be honest, I have difficulties understanding PHP-DI reasonings. If I use e.g. `Psr\Log\LoggerInterface::class` as container key... How can I configure *two* loggers? How can consumer classes crash if they receive the wrong implementation? How can my IDE document and auto-complete methods and properties that extend the interface? (But I guess that's another story, the original question is pretty much answered.) – Álvaro González Aug 24 '19 at 07:55
  • It's new for me too. Using Pimple, we were responsible for everything, but PHP-DI handles a lot of things automatically and needs a better understanding. I have the same question about two instances of a common interface. – Nima Aug 24 '19 at 08:55
0

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.

Álvaro González
  • 142,137
  • 41
  • 261
  • 360
  • 1
    Some quick notes here: Slim 3 comes with Pimple as its _default_ container, but it is not _hard coded_, Related links are [this](http://www.slimframework.com/docs/v3/concepts/di.html) and [this](https://github.com/PHP-DI/Slim-Bridge). Also, if you disable autowiring, they actually become quite similar. But, because Pimple implements ArrayAccess, we (Slim 3 users) are used to define keys like `$conainer['key'] = closure`, which I think is Pimple specific. Change these to `$container->set('key', closure);` and also use `$container->get('key');` as well, and it should be OK to swap them. – Nima Aug 24 '19 at 08:16