Компонент DependencyInjection

Дата обновления перевода 2023-09-01

Компонент DependencyInjection

В предыдущей главе мы очистили класс Simplex\Framework, расширив класс HttpKernel из эпонимного компонента. Увидем этот пустой класс, вам может захотеть переместить в него часть кода из фронт контроллера:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;

class Framework extends HttpKernel\HttpKernel
{
    public function __construct($routes)
    {
        $context = new Routing\RequestContext();
        $matcher = new Routing\Matcher\UrlMatcher($routes, $context);
        $requestStack = new RequestStack();

        $controllerResolver = new HttpKernel\Controller\ControllerResolver();
        $argumentResolver = new HttpKernel\Controller\ArgumentResolver();

        $dispatcher = new EventDispatcher();
        $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));

        parent::__construct($dispatcher, $controllerResolver, $requestStack, $argumentResolver);
    }
}

Код фронт контроллера станет более лаконичным:

1
2
3
4
5
6
7
8
9
10
11
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';

$framework = new Simplex\Framework($routes);

$framework->handle($request)->send();

Наличие лаконичного фронт контроллера позволяет вам иметь несколько фронт контроллеров в одном приложении. Почему это будет полезно? Чтобы позволить наличие разных конфигураций для окружений разработки и производста, к примеру. В окружении разработки вы можете захотеть включить отчёт об ошибках, и отображение ошибок в браузере, чтобы облегчить отладку:

1
2
ini_set('display_errors', 1);
error_reporting(-1);

... но вы точно не захотите ту же конфигурацию в окружении производства. Наличие двух разных фронт контроллеров даёт вам возможность иметь немного разную конфигурацию для каждого из них.

Так что перемещение кода из фронт контроллера в класс фреймворка делает его более конфигурируемым, но в то же время вызывает множество проблем:

  • Мы больше не можем регистрировать пользовательские слушатели, так как диспетчер недоступен вне класса фреймворка (простым обходным путём может быть добавление метода Framework::getEventDispatcher());
  • Мы потеряли гибкость, которую имели раньше, вы больше не можете изменить реализацию UrlMatcher или ControllerResolver;
  • Связано с предыдущим пунктом, мы не может больше с лёгкостью тестировать наш фреймворк, так как невозможно макетировать внутренние объекты;
  • Мы больше не можем изменить набор символов, переданный ResponseListener (обходным путём может быть передача его в качестве аргумента конструктора).

Предыдущий код не выявлял таких же проблем, так как мы использовали внедрение зависимости; все зависимости наших объектов были внедрены в их конструкторы (например, диспетчеры событий были внедрены в фреймворк, чтобы у нас был полный контроль его создания и конфигурации).

Значит ли это, что мы должны сделать выбор между гибкостью, настройкой, лёгкостью тестирования и не копировать и вставлять один и тот же код в каждый фронт контроллер приложения? Как вы можете ожидать, решение сушествует. Мы можем решить все эти проблемы, а также некоторые другие, используя контейнер внедрения зависимостей Symfony:

1
$ composer require symfony/dependency-injection

Создайте новый файл в качестве хоста конфигурации контейнера внедрения зависимости:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// example.com/src/container.php
use Simplex\Framework;
use Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher;
use Symfony\Component\HttpFoundation;
use Symfony\Component\HttpKernel;
use Symfony\Component\Routing;

$container = new DependencyInjection\ContainerBuilder();
$container->register('context', Routing\RequestContext::class);
$container->register('matcher', Routing\Matcher\UrlMatcher::class)
    ->setArguments([$routes, new Reference('context')])
;
$container->register('request_stack', HttpFoundation\RequestStack::class);
$container->register('controller_resolver', HttpKernel\Controller\ControllerResolver::class);
$container->register('argument_resolver', HttpKernel\Controller\ArgumentResolver::class);

$container->register('listener.router', HttpKernel\EventListener\RouterListener::class)
    ->setArguments([new Reference('matcher'), new Reference('request_stack')])
;
$container->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
    ->setArguments(['UTF-8'])
;
$container->register('listener.exception', HttpKernel\EventListener\ErrorListener::class)
    ->setArguments(['Calendar\Controller\ErrorController::exception'])
;
$container->register('dispatcher', EventDispatcher\EventDispatcher::class)
    ->addMethodCall('addSubscriber', [new Reference('listener.router')])
    ->addMethodCall('addSubscriber', [new Reference('listener.response')])
    ->addMethodCall('addSubscriber', [new Reference('listener.exception')])
;
$container->register('framework', Framework::class)
    ->setArguments([
        new Reference('dispatcher'),
        new Reference('controller_resolver'),
        new Reference('request_stack'),
        new Reference('argument_resolver'),
    ])
;

return $container;

Цель этого файла - сконфигурировать ваши объекты и их зависимости. Во время этого шага конфигурации ничего не инстанциируется. Это просто статическое описание объектов, которыми вам нужно управлять и того, как их создать. Объекты будут созданы по запросу, когда вы получите доступ к ним из контейнера или когда они понадобятся контейнеру для создания других объектов.

Например, чтобы создать слушатель маршрутизатора, мы говорим Symfony, что его класс имени - Symfony\Component\HttpKernel\EventListener\RouterListener и что его конструктор берёт объект сопоставителя (new Reference('matcher')). Как вы можете увидеть, на каждый объект ссылаются по имени, строкой, которая уникально идентифицирует каждый объект. Это имя позволяет нам получить объект и сослаться на него в других определениях объектов.

Note

По умолчанию, каждый раз, когда вы получаете объект из контейнера, он возвращает точно тот же экземпляр. Это потому, что контейнер управляет вашими "глобальными" объектами.

Теперь фронт контроллер заключается только в монтировании всего вместе:

1
2
3
4
5
6
7
8
9
10
11
12
13
// example.com/web/front.php
require_once __DIR__.'/../vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;

$routes = include __DIR__.'/../src/app.php';
$sc = include __DIR__.'/../src/container.php';

$request = Request::createFromGlobals();

$response = $sc->get('framework')->handle($request);

$response->send();

Так как все объекты теперь создаются в контейнере внедрения зависимости, код фреймворка должен быть предыдущей простой версией:

1
2
3
4
5
6
7
8
// example.com/src/Simplex/Framework.php
namespace Simplex;

use Symfony\Component\HttpKernel\HttpKernel;

class Framework extends HttpKernel
{
}

Note

Если вы хотите лёгкую альтернативудля вашего контейнера, рассмотрите Pimple, простой контейнер внедрения зависимости, состоящий из примерно 60 строк PHP-кода.

Теперь, вот как вы можете зарегистрировать пользовательский слушатель в фронт контроллере:

1
2
3
4
5
6
7
// ...
use Simplex\StringResponseListener;

$sc->register('listener.string_response', StringResposeListener::class);
$sc->getDefinition('dispatcher')
    ->addMethodCall('addSubscriber', array(new Reference('listener.string_response')))
;

Кроме описания ваших объектов, контейнер внедрения зависимости также может быть сконфигурирован через параметры. Давайте создадим такой, который будет определять, находимся ли мы в режиме отладки:

1
2
3
$sc->setParameter('debug', true);

echo $sc->getParameter('debug');

Эти параметры могут быть использованы при указании определений объектов. Давайте сделаем набор символов конфигурируемым:

1
2
3
4
// ...
$sc->register('listener.response', HttpKernel\EventListener\ResponseListener::class)
    ->setArguments(array('%charset%'))
;

После этого изменения, вы должны установить набор символов до использования объекта слушателя запросов:

1
$sc->setParameter('charset', 'UTF-8');

Вместо того, чтобы полагаться на соглашение о том, что маршруты определяются переменными $routes, давайте снова используем параметр:

1
2
3
4
// ...
$sc->register('matcher', Routing\Matcher\UrlMatcher::class)
    ->setArguments(array('%routes%', new Reference('context')))
;

И связанной переменой в фронт контроллере:

1
$sc->setParameter('routes', include __DIR__.'/../src/app.php');

Понятно, что мы едва коснулись того, что вы можете сделать с контейнером: от имён классов в качестве параметров, до переопределени существующих определений объектов; от поддержки общих сервисов до сброса контейнера в простой PHP-класс и многое другое. Контейнер внедрения зависимости очень мощный и может уравлть любым видом PHP-класса.

Не кричите на меня, если вы не хотите использовать котейнер внедрения зависимостей в вашем фреймворке. Если вам он не нравится - не используйте. Это ваш фреймворк, а не мой.

Это (уже) последняя глава этой книги о создании фреймворка над компонентами Symfony. Я знаю, чо большинство тем не были раскрыты детально, но надеюсь, что дал вам достаточно информации, чтобы начать самостоятельно и лучше понимать то, как работает фреймворк Symfony внутренне.

Если вы хотите узнать больше, прочтите исходный код микро-фреймворка Silex, в особенности его класс Application.

Повеселитесь!