Библиотека 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)
- Получить зависимость по idhas(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.
- Библиотека laminas-config-aggregator
- Контейнер внедрения зависимостей в php
- Библиотека php-di - Текущая статья