Компонент 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
27
28
29
30
31
// example.com/src/Simplex/Framework.php
namespace Simplex;

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

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\ErrorListener(
            'Calendar\Controller\ErrorController::exception'
        ));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher, $requestStack));
        $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8'));
        $dispatcher->addSubscriber(new StringResponseListener());

        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';
$container = include __DIR__.'/../src/container.php';

$request = Request::createFromGlobals();

$response = $container->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;

$container->register('listener.string_response', StringResponseListener::class);
$container->getDefinition('dispatcher')
    ->addMethodCall('addSubscriber', [new Reference('listener.string_response')])
;

Окрім опису ваших обʼєктів, контейнер впровадження залежноті також може бути сконфігурований через параметри. Давайте створимо такий, який визначатиме, чи знаходимося ми у режимі налагодження:

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

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

Ці параметри можуть бути викристані при вказанні визначень обʼєктів. Давайте зробимо набір символів конфігурованим:

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

Після цієї зміни, ви повинні встановити набір символів до використання обʼєкта слухача запитів:

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

Замість того, щоб покладатия на угоду про те, що маршрути визначаютья змінними $routes, давайте знову використаємо параметр:

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

І повʼязану змінну у фронт-контролері:

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

Зрозуміло, що ми ледь торкнулия того, що ви можете зробити з контейнером: від імен класів в якості параметрів, до перевизначення існуючих визначень обʼєктів; від підтримки спільних сервісів до скидання контейнера у простий PHP-клас та багато іншого. Контейнер впровадження залежності дуже потужний і може управляти будь-яким видом PHP-класу.

Не кричіть на мене, якщо ви не хочете використовувати контейнер впровадження залежностей у вашому фреймворку. Якщо він вам не подобається - не використовуйте. Це ваш фреймворк, а не мій.

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

Повеселіться!