Search code examples
phpslimphp-dipsr-11slim-4

Inject container in controller class


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?


Solution

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