在控制器中注入容器 class
Inject container in controller class
我正在将我的应用程序从 Slim/3 迁移到 Slim/4。也许我很困惑,因为同样的东西有无穷无尽的语法,但我是这样写的:
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();
... 它没有像我预期的那样工作,因为我得到了两个完全不同的容器实例(通过在 \DI\Container::__construct()
中设置断点进行验证):
- 我自己用
$config = new Config();
创建的那个。
- 在
$app->run();
自动创建一个,然后作为参数传递给 \Foo::__construct()
。
我错了什么?
容器尝试解析(并创建)\DI\Container
class 的新实例,因为这不是 Slim 使用的接口。相反,尝试声明 PSR-11 ContainerInterface
。然后 DIC 应该传递正确的容器实例。
例子
use Psr\Http\Message\ServerRequestInterface;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
相同的 "rule" 适用于请求处理程序接口。
完整示例:
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);
}
}
最后一点:注入容器是一种反模式。请改为在构造函数中显式声明所有 class 依赖项。
为什么注入容器(在大多数情况下)是一种反模式?
在 Slim 3 中,"Service Locator"(反模式)是默认的 "style" 注入整个(Pimple)容器并从中获取依赖项。
服务定位器(反模式)隐藏了 class 的真正依赖项。
服务定位器(反模式)也违反了 SOLID 的控制反转 (IoC) 原则。
问:我怎样才能让它变得更好?
A:使用composition
和(显式)构造函数依赖注入.
依赖注入是一种编程实践,它传递给它的协作者对象,而不是对象本身创建它们。
从 Slim 4 开始,您可以使用 PHP-DI
和 league/container
等现代 DIC,并具有出色的 "autowire" 功能。这意味着:现在您可以在构造函数中显式声明所有依赖项,并让 DIC 为您注入这些依赖项。
更明确地说:"Composition" 与 DIC 的 "Autowire" 功能无关。您可以使用纯 classes 组合,而无需容器或其他任何东西。自动装配功能仅使用 PHP Reflection classes 自动为您解析和注入依赖项。
这是 PHP-DI 自动注册的结果。在撰写此答案时,PHP-DI 容器在创建时自动将自身注册到密钥 DI\Container
以及三个已实现的接口(请参阅 these lines of Container.php)。因此,如果您针对 DI\Container
或它实现的三个接口之一(包括 Psr\Container\ContainerInterface
)键入提示您的构造函数参数,PHP-DI 能够自行解析。
ُ问题是 self::class
的使用(该文件的第 110 行)使 DI\Container
密钥以某种方式硬编码,因此尽管您正在创建子 class DI\Container
(Config
) 容器仍然注册到与以前相同的密钥。克服这个问题的一种方法是让容器知道 Config
也应该解析为 本身 。我看到了两个选项:
- 将容器注册到与其 class 名称相同的密钥,就像
DI\Container
所做的那样(这似乎是正确的做法)
- 实例化后手动注册容器
这是一个完整的示例:
<?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();
请注意:As best practices suggest,您不应针对具体的 class(DI\Container
或您的Config
class),相反,您应该考虑针对接口 (Psr\Container\ContainerInterface
) 进行类型提示。
问题是误用了名为 autowiring:
的 PHP-DI 功能
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.
如果您使用工厂方法创建容器,您可以禁用自动装配并且 "strange" 行为停止:
$builder = new ContainerBuilder(Config::class);
$builder->useAutowiring(false);
$config = $builder->build();
但我想更好的解决办法是学习如何正确使用自动装配:)
我忽略了所有这些细节,因为我的代码最初是为 Slim/3 编写的,它使用 Pimple 作为 硬编码 默认容器。我错误地认为它们的工作方式相似,但是,尽管是容器解决方案,但两个库却大不相同。
我正在将我的应用程序从 Slim/3 迁移到 Slim/4。也许我很困惑,因为同样的东西有无穷无尽的语法,但我是这样写的:
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();
... 它没有像我预期的那样工作,因为我得到了两个完全不同的容器实例(通过在 \DI\Container::__construct()
中设置断点进行验证):
- 我自己用
$config = new Config();
创建的那个。 - 在
$app->run();
自动创建一个,然后作为参数传递给\Foo::__construct()
。
我错了什么?
容器尝试解析(并创建)\DI\Container
class 的新实例,因为这不是 Slim 使用的接口。相反,尝试声明 PSR-11 ContainerInterface
。然后 DIC 应该传递正确的容器实例。
例子
use Psr\Http\Message\ServerRequestInterface;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
相同的 "rule" 适用于请求处理程序接口。
完整示例:
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);
}
}
最后一点:注入容器是一种反模式。请改为在构造函数中显式声明所有 class 依赖项。
为什么注入容器(在大多数情况下)是一种反模式?
在 Slim 3 中,"Service Locator"(反模式)是默认的 "style" 注入整个(Pimple)容器并从中获取依赖项。
服务定位器(反模式)隐藏了 class 的真正依赖项。
服务定位器(反模式)也违反了 SOLID 的控制反转 (IoC) 原则。
问:我怎样才能让它变得更好?
A:使用composition
和(显式)构造函数依赖注入.
依赖注入是一种编程实践,它传递给它的协作者对象,而不是对象本身创建它们。
从 Slim 4 开始,您可以使用 PHP-DI
和 league/container
等现代 DIC,并具有出色的 "autowire" 功能。这意味着:现在您可以在构造函数中显式声明所有依赖项,并让 DIC 为您注入这些依赖项。
更明确地说:"Composition" 与 DIC 的 "Autowire" 功能无关。您可以使用纯 classes 组合,而无需容器或其他任何东西。自动装配功能仅使用 PHP Reflection classes 自动为您解析和注入依赖项。
这是 PHP-DI 自动注册的结果。在撰写此答案时,PHP-DI 容器在创建时自动将自身注册到密钥 DI\Container
以及三个已实现的接口(请参阅 these lines of Container.php)。因此,如果您针对 DI\Container
或它实现的三个接口之一(包括 Psr\Container\ContainerInterface
)键入提示您的构造函数参数,PHP-DI 能够自行解析。
ُ问题是 self::class
的使用(该文件的第 110 行)使 DI\Container
密钥以某种方式硬编码,因此尽管您正在创建子 class DI\Container
(Config
) 容器仍然注册到与以前相同的密钥。克服这个问题的一种方法是让容器知道 Config
也应该解析为 本身 。我看到了两个选项:
- 将容器注册到与其 class 名称相同的密钥,就像
DI\Container
所做的那样(这似乎是正确的做法) - 实例化后手动注册容器
这是一个完整的示例:
<?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();
请注意:As best practices suggest,您不应针对具体的 class(DI\Container
或您的Config
class),相反,您应该考虑针对接口 (Psr\Container\ContainerInterface
) 进行类型提示。
问题是误用了名为 autowiring:
的 PHP-DI 功能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.
如果您使用工厂方法创建容器,您可以禁用自动装配并且 "strange" 行为停止:
$builder = new ContainerBuilder(Config::class);
$builder->useAutowiring(false);
$config = $builder->build();
但我想更好的解决办法是学习如何正确使用自动装配:)
我忽略了所有这些细节,因为我的代码最初是为 Slim/3 编写的,它使用 Pimple 作为 硬编码 默认容器。我错误地认为它们的工作方式相似,但是,尽管是容器解决方案,但两个库却大不相同。