Пост

Библиотека php-di

Рассмотрим библиотеку php-di, как ее использовать в приложениях

В предыдущей статье https://lexusalex.site/posts/php-dependency-injection-container/ мы поняли что такое контейнер внедрения зависимостей, разобрали где он может быть полезен.

Сегодня на примере библиотеки https://php-di.org/ посмотрим, как использовать контейнер внедрения зависимостей у себя в проектах.

Абстрактный пример

Допустим у нас есть абстрактный код где мы не используем контейнер, пусть это будет подсчет скидок в интернет магазине.

Как примерно может работать вызов функций:

1
2
3
4
5
6
Приложение()
    Товары пользователя()
        Цены на товары()
            Доступна ли скидка()
                Расчет скидки()
                    Итоговый подсчет цены на товары()

То есть у нас есть зависимые функции, мы должны их создать и вызвать перед использованием другой функции.

Теперь как это может работать с использованием контейнера.

Определяем в контейнере все функции (допустим определили)

Вызываем $container->get(Приложние())

Что происходит за кадром. Так как для работы функции Приложение() нужны другие функции, контейнер сам позаботиться о вызове других зависимых функций, то есть функции будут вызваны тогда, когда они потребуются другой функцией. И об этом позаботится контейнер php-di.

1
2
3
4
5
Итоговый подсчет цены на товары()
Расчет скидки()
Доступна ли скидка()
Цены на товары()
Товары пользователя()

Установка

Ставим стандартным образом через composer.

1
2
# На текущий момент (октябрь 2024) актуальная версия 7.0.7
composer require php-di/php-di

Базовое использование

Чтобы начать использовать php-di нужно создать класс и передать туда зависимости, к примеру так:

1
2
3
4
5
6
7
8
$container = new Container([
    'depend1' => [1], // Зависимость 1
    'depend2' => function (\Psr\Container\ContainerInterface $c) { // Зависимость 2
       return $c->get('depend1'); // 2 зависимость зависит от первой
    }
]);

print_r($container->get('depend2')); // Достав первую зависисмоть получаем [1]

Применим эти знания к абстрактному примеру выше.

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
// Создадим контейнер
$container = new Container([
    'application' => function (ContainerInterface $c) {
        print_r("подсчет товаров \n");
        return $c->get("goods");
    },
    'goods' => function (ContainerInterface $c) {
        print_r("подсчет цен\n");
        return $c->get("prices");
    },
    'prices' => function (ContainerInterface $c) {
        print_r("скидка\n");
        return $c->get("checkDiscount");
    },
    'checkDiscount' => function (ContainerInterface $c) {
        print_r("подсчет скидки\n");
        return $c->get("calculateDiscount");
    },
    'calculateDiscount' => function (ContainerInterface $c) {
        print_r("итоговый результат\n");
        return $c->get("result");
    },
    'result' => "result\n"

]);
// Достанем зависимость
print_r($container->get('application'));
/**
подсчет товаров 
подсчет цен
скидка
подсчет скидки
итоговый результат
result
 */

Мы определили все зависимости, достали из контейнера application, которое по цепочке вызывает все остальные зависимости.

Получаем линейный код, над которым мы имеем полный контроль.

Еще один пример. Допустим есть набор классов:

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
Class One
{
    public function __construct(){
        echo "One";
    }
}

Class Two
{
    public function __construct(){
        (new One());
        echo "Two";
    }
}

Class Three
{
    public function __construct(){
        (new Two());
        echo "Three";
    }
}

$container = new DI\Container([
    Three::class => function($container){new Three();}
]);

$container->get(Three::class); // OneTwoThree

В контейнере определили последний класс, который подключит все зависимые классы.

Методы контейнера

Контейнер реализует стандарт PSR-11. По стандарту доступны 2 метода:

  • get(id) - Получить зависимость по id
  • has(id) - Проверить существует ли зависимость по id

Плюс к этому доступны еще несколько методов:

set

set используется для установки значения в контейнер

1
2
$container->set('test','123');
print_r($container->get('test')); // 123

Но, все-таки рекомендуют использовать установку через зависимости.

make

Метод make позволяет создать объект, который не хранится внутри контейнера. То есть для которого не важно состояние.

1
2
3
4
5
6
7
8
9
class SuperTest
{
    public function __construct($in, $out)
    {
        echo 1;
    }
}

$container->make(SuperTest::class, ['in' => '1', 'out' => '2']); // 1

Объект будет сразу же создан, при этом он не находится в контейнере.

call

call позволяет вызвать любой вызываемый объект, то есть callble.

1
print_r($container->call(function (ContainerInterface $c) {return $c->get("result");})); // result

Autowiring

Autowiring представляет собой способность контейнера получать другой сервис в качестве зависимости, если он указан в конструкторе объекта, это является ключевой особенностью, что и называется автоматическим разрешением зависимостей или Autowiring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyTest
{
    public function __construct()
    {
        echo __CLASS__ . "\n";
    }
}

class Test
{
    public function __construct(myTest $myTest)
    {

    }
}

$container = new Container([
    'Test' => Test::class,
]);

$container->get(MyTest::class); // MyTest

В этом примере мы вручную через оператор new не создаем объекты, это за нас сделал php-di.

Зависимости

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

1
2
3
4
5
6
7
8
9
10
// Стандартное создание контейнера 
$container = new DI\Container([]);

// Создание контейнера через builder
$builder = new \DI\ContainerBuilder();
// ... опции конфигурации
$builder->addDefinitions([]);
// Либо подключить файл с зависимостями
$builder->addDefinitions('config.php');
$builder->build();

Контейнер php-di загружает ваши зависимости и использует их как инструкции для создания объектов.

Объекты будут созданы только тогда, когда они запрашиваются из контейнера через get() либо, когда его нужно внедрить в другой объект. То есть если у нас 10 зависимостей, а используются только 2, то 2 объекта и будут созданы.

Что же мы можем определить в качестве зависимостей.

Несколько примеров:

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
return [
    Logger::class => new Logger(), // Не рекомендуется объявлять так
    LoggerInterface::class => static function () { // Лучше так
        $logger = new Logger('tech');
        return $logger;
    },
    'Test' => 'localhost'
    'Test2' => ['one', 'two'],
    NormalizerInterface::class => get(SerializerInterface::class),
    DenormalizerInterface::class => get(SerializerInterface::class),
    // Целая инструкция для создания symfony serializer
    SerializerInterface::class => static function (): SerializerInterface {
        return new Serializer([
            new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader())),
            new DateTimeNormalizer(),
            new PropertyNormalizer(
                propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [
                    new ReflectionExtractor(),
                    new PhpDocExtractor(),
                ])
            ),
            new ArrayDenormalizer(),
        ], [
            new JsonEncoder(),
        ]);
    },
]

Кеш

Для ускорения работы контейнера, его итоговый массив можно скомпилировать в php код следующим образом.

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
$builder = new \DI\ContainerBuilder();
$builder->enableCompilation(__DIR__ . '/var/cache');
$builder->addDefinitions([
    'application' => function (ContainerInterface $c) {
        print_r("подсчет товаров \n");
        return $c->get("goods");
    },
    'goods' => function (ContainerInterface $c) {
        print_r("подсчет цен\n");
        return $c->get("prices");
    },
    'prices' => function (ContainerInterface $c) {
        print_r("скидка\n");
        return $c->get("checkDiscount");
    },
    'checkDiscount' => function (ContainerInterface $c) {
        print_r("подсчет скидки\n");
        return $c->get("calculateDiscount");
    },
    'calculateDiscount' => function (ContainerInterface $c) {
        print_r("итоговый результат\n");
        return $c->get("result");
    },
    'result' => "result1\n"

]);
$builder->build();

При этом будет создан файл, но только один раз, это как раз и будет обеспечивать максимальную производительность.

Чтобы, обновить файл, и выкатить новую версию, нужно просто удалить файл.

1
2
3
4
// Проверка на production
if (is_production()) {
    $containerBuilder->enableCompilation(__DIR__ . '/var/cache');
}

Что в итоге

Библиотека php-di мощный инструмент для построения приложений для которой можно найти массу применений.

Это третья статья из серии статей про построение приложений на php.

  1. Библиотека laminas-config-aggregator
  2. Контейнер внедрения зависимостей в php
  3. Библиотека php-di - Текущая статья
Авторский пост защищен лицензией CC BY 4.0 .