Внедрить контейнер в классе контроллера
Я перевожу свое приложение из 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()
,
Что я не так понял?
3 ответа
Контейнер пытается разрешить (и создать) новый экземпляр \DI\Container
класс, так как это не интерфейс, который использует Slim. Вместо этого попробуйте объявить PSR-11 ContainerInterface
, Затем DIC должен передать правильный экземпляр контейнера.
пример
use Psr\Http\Message\ServerRequestInterface;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
То же самое "правило" применяется к интерфейсу обработчика запросов.
Полный пример:
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);
}
}
Последнее замечание: впрыскивание контейнера - это анти-шаблон. Пожалуйста, объявите все классовые зависимости в вашем конструкторе явно.
Почему инъекция контейнера (в большинстве случаев) является анти-паттерном?
В Slim 3 "Сервисный локатор" (анти-шаблон) был "стилем" по умолчанию для внедрения целого (Pimple) контейнера и извлечения зависимостей из него.
Сервисный локатор (анти-паттерн) скрывает реальные зависимости вашего класса.
Сервисный локатор (анти-шаблон) также нарушает принцип SOLID, связанный с инверсией управления (IoC).
Q: Как я могу сделать это лучше?
A: использовать composition
,
Использовать (явный) конструктор внедрения зависимостей. Внедрение зависимостей - это программирующая практика передачи в объект своих коллег, а не самого объекта, который их создает.
Начиная с Slim 4, вы можете использовать современные DIC как PHP-DI
а также league/container
с потрясающей функцией "autow ire". Это означает: теперь вы можете объявить все зависимости явно в вашем конструкторе и позволить DIC внедрить эти зависимости для вас.
Чтобы быть более понятным: "Композиция" не имеет ничего общего с функцией "Autow ire" DIC. Вы можете использовать композицию с чистыми классами и без контейнера или чего-либо еще. Функция autow ire просто использует классы PHP Reflection для автоматического разрешения и внедрения зависимостей.
Это происходит в результате автоматической регистрации PHP-DI. На момент написания этого ответа контейнер PHP-DI автоматически регистрируется на ключDI\Container
, а также три реализованных интерфейса при создании (см. эти строки Container.php). В результате, если вы введете подсказку для параметра конструктора,DI\Container
или один из трех реализуемых интерфейсов (который включает Psr\Container\ContainerInterface
), PHP-DI может разрешиться самостоятельно.
ُ Проблема заключается в использовании self::class
(строка 110 этого файла) делает DI\Container
ключ как-то жестко запрограммирован, поэтому, хотя вы создаете дочерний класс DI\Container
(Config
) контейнер по-прежнему регистрируется с тем же ключом, что и раньше. Один из способов преодолеть это - сообщить контейнеру, чтоConfig
тоже следует разрешить самому себе. Я вижу для этого два варианта:
- Чтобы зарегистрировать контейнер с тем же ключом, что и его имя класса, например, что
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();
Обратите внимание: согласно передовой практике, вам не следует вводить подсказку для конкретного класса (DI\Container
или ваш Config
class), вместо этого вам следует подумать о подсказках типов для интерфейса (Psr\Container\ContainerInterface
).
Проблема заключается в неправильном использовании функции PHP-DI под названием autowiring:
Автопроводка - это экзотическое слово, которое представляет собой нечто очень простое: способность контейнера автоматически создавать и вставлять зависимости.
Для этого PHP-DI использует отражение PHP, чтобы определить, какие параметры нужны конструктору.
Если вы используете фабричный метод для создания контейнера, вы можете отключить автопроводку, и "странное" поведение прекращается:
$builder = new ContainerBuilder(Config::class);
$builder->useAutowiring(false);
$config = $builder->build();
Но я думаю, что лучшее решение - научиться правильно использовать автопроводку:)
Я упустил все эти детали, потому что мой код изначально был написан для Slim/3, в котором Pimple использовался как жестко контейнер по умолчанию. Я ошибочно предположил, что они будут работать аналогично, но, хотя они и являются контейнерными решениями, обе библиотеки совершенно разные.