Внедрить контейнер в классе контроллера

Я перевожу свое приложение из 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()):

  1. Тот, с которым я создаю себя $config = new Config();,
  2. Тот, который создается автоматически при $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тоже следует разрешить самому себе. Я вижу для этого два варианта:

  1. Чтобы зарегистрировать контейнер с тем же ключом, что и его имя класса, например, что DI\Container делает (кажется, это правильный способ сделать это)
  2. Ручная регистрация контейнера после его создания

Вот полностью рабочий пример:

<?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 использовался как жестко контейнер по умолчанию. Я ошибочно предположил, что они будут работать аналогично, но, хотя они и являются контейнерными решениями, обе библиотеки совершенно разные.

Другие вопросы по тегам