Контейнер внедрения зависимостей с поддержкой кеша, ленивых сервисов и фабрик.
Поддерживает файловый кеш для рефлексии, однако сохраняет в рефлексию в рантайме.
Если вы хотите сохранить её в хранилище - вызовите метод $di->saveCache();
в конце работы скрипта.
Функция разогрева не имеет смысла, потому что это требует "создать все возможные обьекты во всех возможных комбинациях".
composer require gzhegow/di;
<?php
require_once __DIR__ . '/vendor/autoload.php';
// > настраиваем PHP
ini_set('memory_limit', '32M');
// > настраиваем обработку ошибок
(new \Gzhegow\Lib\Exception\ErrorHandler())
->useErrorReporting()
->useErrorHandler()
->useExceptionHandler()
;
// > добавляем несколько функция для тестирования
function _debug(...$values) : string
{
$lines = [];
foreach ( $values as $value ) {
$lines[] = \Gzhegow\Lib\Lib::debug()->type($value);
}
$ret = implode(' | ', $lines) . PHP_EOL;
echo $ret;
return $ret;
}
function _dump(...$values) : string
{
$lines = [];
foreach ( $values as $value ) {
$lines[] = \Gzhegow\Lib\Lib::debug()->value($value);
}
$ret = implode(' | ', $lines) . PHP_EOL;
echo $ret;
return $ret;
}
function _dump_array($value, int $maxLevel = null, bool $multiline = false) : string
{
$content = $multiline
? \Gzhegow\Lib\Lib::debug()->array_multiline($value, $maxLevel)
: \Gzhegow\Lib\Lib::debug()->array($value, $maxLevel);
$ret = $content . PHP_EOL;
echo $ret;
return $ret;
}
function _assert_output(
\Closure $fn, string $expect = null
) : void
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
\Gzhegow\Lib\Lib::assert()->output($trace, $fn, $expect);
}
function _assert_microtime(
\Closure $fn, float $expectMax = null, float $expectMin = null
) : void
{
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
\Gzhegow\Lib\Lib::assert()->microtime($trace, $fn, $expectMax, $expectMin);
}
// >>> ЗАПУСКАЕМ!
// > сначала всегда фабрика
$factory = new \Gzhegow\Di\DiFactory();
// > создаем конфигурацию
$config = new \Gzhegow\Di\DiConfig();
$config->configure(function (\Gzhegow\Di\DiConfig $config) {
// > инжектор
$config->injector->fetchFunc = \Gzhegow\Di\Injector\DiInjector::FETCH_FUNC_GET;
// > кэш рефлектора
$config->reflectorCache->cacheMode = \Gzhegow\Di\Reflector\DiReflectorCache::CACHE_MODE_STORAGE;
//
$cacheDir = __DIR__ . '/var/cache';
$cacheNamespace = 'gzhegow.di';
$cacheDirpath = "{$cacheDir}/{$cacheNamespace}";
$config->reflectorCache->cacheDirpath = $cacheDirpath;
//
// $symfonyCacheAdapter = new \Symfony\Component\Cache\Adapter\FilesystemAdapter(
// $cacheNamespace, $defaultLifetime = 0, $cacheDir
// );
// $redisClient = \Symfony\Component\Cache\Adapter\RedisAdapter::createConnection('redis://localhost');
// $symfonyCacheAdapter = new \Symfony\Component\Cache\Adapter\RedisAdapter(
// $redisClient,
// $cacheNamespace = '',
// $defaultLifetime = 0
// );
// $config->reflectorCache->cacheMode = \Gzhegow\Di\Reflector\ReflectorCache::CACHE_MODE_STORAGE;
// $config->reflectorCache->cacheAdapter = $symfonyCacheAdapter;
});
// > создаем кэш рефлектора
// > кэш наполняется и сохраняется автоматически по мере наполнения контейнера
$reflectorCache = new \Gzhegow\Di\Reflector\DiReflectorCache(
$config->reflectorCache
);
// > создаем рефлектор
$reflector = new \Gzhegow\Di\Reflector\DiReflector($reflectorCache);
// > создаем инжектор
$injector = new \Gzhegow\Di\Injector\DiInjector(
$reflector,
$config->injector
);
// > создаем DI
$di = new \Gzhegow\Di\Di(
$factory,
$injector,
$reflector
);
// > сохраняем DI статически
\Gzhegow\Di\Di::setInstance($di);
// > можно обернуть в контейнер Psr, для стандартизации (не обязательно)
// $container = new \Gzhegow\Di\Container\ContainerPsr($di); // composer require psr/container
// $container = new \Gzhegow\Di\Container\ContainerPsr10000($di); // composer require psr/container:~1.0
// > так можно очистить кеш принудительно (обычно для этого делают консольный скрипт и запускают вручную или кроном, но если использовать symfony/cache можно и просто установить TTL - время устаревания)
$di->clearCache();
// > так можно сбросить кэш принудительно (чтобы запросить его из хранилища заново в режиме STORAGE)
// $di->resetCache();
// > так можно сохранить кеш (обычно в конце скрипта)
// $di->saveCache();
// >>> Регистрируем сервисы
// > Можно зарегистрировать класс в контейнере (смысл только в том, что метод get() не будет выбрасывать исключение)
// $di->bind(MyClassOneOne::class);
// > Можно привязать на интерфейс (а значит объект сможет пройти проверки зависимостей на входе конструктора)
// $di->bind(MyClassOneInterface::class, MyClassOneOne::class, $isSingleton = false);
// > Можно сразу указать, что созданный экземпляр будет одиночкой (все запросы к нему вернут тот же объект)
// $di->bindSingleton(MyClassOneInterface::class, MyClassOneOne::class);
// > объявим фабричный метод - при создании класса будет использоваться именно он
$fnNewMyClassOne = static function () use ($di) {
$object = $di->make('\Gzhegow\Di\Demo\MyClassOneOne', [ 123 ]);
return $object;
};
// > теперь сохраним его результат как одиночку ("singleton"), то есть при втором вызове get()/ask() вернется тот же экземпляр
$di->bindSingleton('\Gzhegow\Di\Demo\MyClassOneInterface', $fnNewMyClassOne);
// > а также зарегистрируем алиас на наш интерфейс по имени - можно будет применять паттерн "сервис-локатор", вместо того чтобы создавать файлы под интерфейс и наследник имеющегося класса
// > в добавок, если мы задаем алиас в виде строки (с именем класса '\MyClass'), а не в виде php-константы `\MyClass::class`,
// > то мы избегаем подгрузки классов через autoloader (откладывая её до того момента, как мы запросим зависимость), а значит ускоряем запуск программы
$di->bind('one', '\Gzhegow\Di\Demo\MyClassOneInterface');
// > известно, например, что сервис MyClassTwo долго выполняет __construct(), пусть, соединяется по сети
// > регистриуем как обычно, а дальше - запросим через $di->getLazy()
$di->bindSingleton('\Gzhegow\Di\Demo\MyClassTwoInterface', '\Gzhegow\Di\Demo\MyClassTwo');
$di->bind('two', '\Gzhegow\Di\Demo\MyClassTwoInterface');
// > зарегистрируем класс как синглтон (первый вызов создаст объект, второй - вернет созданный)
$di->bindSingleton('\Gzhegow\Di\Demo\MyClassThree');
$di->bind('three', '\Gzhegow\Di\Demo\MyClassThree');
// > MyClassThree требует сервисов One и Two, а чтобы не фиксировать сигнатуру конструктора, мы добавим их с помощью Интерфейсов и Трейтов
// > некоторые контейнеры зависимостей в интернетах позволяют это делать не только по интерфейсам, но и по тегам, как например в symfony. Теги штука удобная, однако по сути своей теги это интерфейсы
$di->extend(\Gzhegow\Di\Demo\MyClassOneAwareInterface::class, static function (\Gzhegow\Di\Demo\MyClassOneAwareInterface $aware) use ($di) {
$one = $di->get('one');
$aware->setOne($one);
});
$di->extend(\Gzhegow\Di\Demo\MyClassTwoAwareInterface::class, static function (\Gzhegow\Di\Demo\MyClassTwoAwareInterface $aware) use ($di) {
$two = $di->getLazy('two');
$aware->setTwo($two);
});
// > TEST
// > получить сервис (с автоматическим заполнением зависимостей, "автовайрингом")
// > если в настройках установлено $config->injector->fetchFunc = 'GET', то при попытке заполнения необъявленных зависимостей будет выброшено исключение
// > иначе, если $config->injector->fetchFunc = 'TAKE', то инжектор попытается создать новый экземпляр, если в качестве `id` передано имя класса
$fn = function () use ($di) {
_dump('TEST 1');
echo PHP_EOL;
// > Используя параметр $contractT можно задавать имя класса, который поймет PHPStorm как генерик и будет давать подсказки
// > метод ask() возвращает экземпляр или NULL
// $object = $di->ask(MyInterface::class, $contractT = MyClass::class, $forceInstanceOf = false, $parametersWhenNew = []); // > использует get() если зарегистрировано, NULL если не зарегистрировано
// > get() бросит исключение, если не зарегистрировано в контейнере
// $object = $di->get(MyInterface::class, $contractT = MyClass::class, $forceInstanceOf = false, $parametersWhenNew = []);
// > make() вернет всегда новый экземпляр с параметрами
// $object = $di->make(MyInterface::class, $parameters = [], $contractT = MyClass::class);
// > take() выполнит get() если зарегистрирован, или make() если нет
// $object = $di->take(MyInterface::class, $parametersWhenNew = [], $contractT = MyClass::class); // > get() если зарегистрировано, make() если не зарегистрировано
// > fetch() выполнит то, что указано в $config->injector->fetchFunc = 'GET'/'TAKE'
// $object = $di->fetch(MyInterface::class, $parametersWhenNew = [], $contractT = MyClass::class); // > get() если зарегистрировано, make() если не зарегистрировано
$result = $di->get('three');
_dump($result);
// > Если класс помечен как сиглтон, запросы его вернут один и тот же экземпляр
$result1 = $di->get('\Gzhegow\Di\Demo\MyClassThree');
$result2 = $di->get('\Gzhegow\Di\Demo\MyClassThree');
$result3 = $di->get('three');
_dump($result1, $result2, $result3);
_dump($result1 === $result2);
_dump($result2 === $result3);
};
_assert_output($fn, '
"TEST 1"
{ object # Gzhegow\Di\Demo\MyClassThree }
{ object # Gzhegow\Di\Demo\MyClassThree } | { object # Gzhegow\Di\Demo\MyClassThree } | { object # Gzhegow\Di\Demo\MyClassThree }
TRUE
TRUE
');
// > TEST
// > дозаполнить аргументы уже существующего объекта, который мы не регистрировали в контейнере
$fn = function () use ($di) {
_dump('TEST 2');
echo PHP_EOL;
// $di->autowireInstance($four, $customArgs = [], $customMethod = '__myCustomAutowire');
$four1 = new \Gzhegow\Di\Demo\MyClassFour();
$di->autowireInstance($four1);
_dump($four1);
_dump($four1->one);
// > зависимость по интерфейсу, зарегистрированная как одиночка, будет равна в двух разных экземплярах
$four2 = new \Gzhegow\Di\Demo\MyClassFour();
$di->autowireInstance($four2);
_dump($four2);
_dump($four2->one);
_dump($four1->one === $four2->one);
};
_assert_output($fn, '
"TEST 2"
{ object # Gzhegow\Di\Demo\MyClassFour }
{ object # Gzhegow\Di\Demo\MyClassOneOne }
{ object # Gzhegow\Di\Demo\MyClassFour }
{ object # Gzhegow\Di\Demo\MyClassOneOne }
TRUE
');
// > TEST
// > дозаполнить аргументы уже существующего объекта, который мы не регистрировали в контейнере, имеющий зависимости, которые тоже не были зарегистрированы
$fn = function () use ($di, $config) {
_dump('TEST 3');
echo PHP_EOL;
// > попытка заполнить зависимости, которые не зарегистрированы в контейнере с `fetchFunc = GET' приведет к ошибке
try {
$five1 = new \Gzhegow\Di\Demo\MyClassFive();
$di->autowireInstance($five1);
}
catch ( \Gzhegow\Di\Exception\Runtime\NotFoundException $e ) {
}
_dump('[ CATCH ] ' . $e->getMessage());
// > переключаем режим (на продакшене лучше включить его в начале приложения и динамически не менять)
$config->configure(function (\Gzhegow\Di\DiConfig $config) {
$config->injector->fetchFunc = \Gzhegow\Di\Injector\DiInjector::FETCH_FUNC_TAKE;
});
$config->validate();
$five1 = new \Gzhegow\Di\Demo\MyClassFive();
$di->autowireInstance($five1);
_dump($five1);
_dump($five1->four);
$five2 = new \Gzhegow\Di\Demo\MyClassFive();
$di->autowireInstance($five2);
_dump($five2);
_dump($five2->four);
_dump($five1->four !== $five2->four);
$config->configure(function (\Gzhegow\Di\DiConfig $config) {
$config->injector->fetchFunc = \Gzhegow\Di\Injector\DiInjector::FETCH_FUNC_GET;
});
$config->validate();
};
_assert_output($fn, '
"TEST 3"
"[ CATCH ] Missing bound `argReflectionTypeClass` to resolve parameter: [ 0 ] $four : Gzhegow\Di\Demo\MyClassFour"
{ object # Gzhegow\Di\Demo\MyClassFive }
{ object # Gzhegow\Di\Demo\MyClassFour }
{ object # Gzhegow\Di\Demo\MyClassFive }
{ object # Gzhegow\Di\Demo\MyClassFour }
TRUE
');
// > TEST
// > вызовем произвольную функцию и заполним её аргументы
$fn = function () use ($di, $config) {
_dump('TEST 4');
echo PHP_EOL;
$fn = static function (
$arg1,
\Gzhegow\Di\Demo\MyClassThree $three,
$arg2
) {
_dump($arg1);
_dump($arg2);
return get_class($three);
};
$args = [
'arg1' => 1,
'arg2' => 2,
];
$result = $di->callUserFuncArrayAutowired($fn, $args);
_dump($result);
// > можно и так, но поскольку аргументы передаются по порядку - придется указать NULL для тех, что мы хотим распознать
// $args = [ 1, null, 2 ];
// $result = $di->callUserFuncAutowired($fn, ...$args);
};
_assert_output($fn, '
"TEST 4"
1
2
"Gzhegow\Di\Demo\MyClassThree"
');
// > TEST
// > некоторые сервисы слишком долго выполняют конструктор (например, подключаются к внешней апи)
// > запросим его как ленивый (правда, при этом подстановка в аргументы конструктора будет невозможна)
// > В PHP, к сожалению, нет возможности создать анонимный класс, который расширяет ("extend") имя класса из строковой переменной, поэтому, приходится использовать только такие LazyService.
$lazy1 = null;
$lazy2 = null;
$lazy3 = null;
$fn = function () use (
$di,
//
&$lazy1,
&$lazy2,
&$lazy3
) {
_dump('TEST 5');
echo PHP_EOL;
// $object = $di->getLazy(MyInterface::class, $contractT = MyClass::class, $parametersWhenNew = []);
// $object = $di->makeLazy(MyInterface::class, $parameters = [], $contractT = MyClass::class);
// $object = $di->takeLazy(MyInterface::class, $parametersWhenNew = [], $contractT = MyClass::class);
// $object = $di->fetchLazy(MyInterface::class, $parametersWhenNew = [], $contractT = MyClass::class);
// > make создаст новый объект, но не перепишет имеющийся синглтон, и использует переданные параметры
$lazy1 = $di->getLazy('two', $contractT = null, [ 'hello' => 'User1' ]);
_dump($lazy1);
// > make создаст новый объект, но не перепишет имеющийся синглтон, и использует переданные параметры
$lazy2 = $di->makeLazy('two', [ 'hello' => 'User2' ]);
_dump($lazy2);
// > а здесь мы уже получим сохранненный как синглтон экземпляр
$lazy3 = $di->getLazy('two');
_dump($lazy3);
};
_assert_output($fn, '
"TEST 5"
{ object # Gzhegow\Di\LazyService\LazyService }
{ object # Gzhegow\Di\LazyService\LazyService }
{ object # Gzhegow\Di\LazyService\LazyService }
');
// > TEST
// > вызовем действия на ленивом сервисе
$fn = function () use (
$di,
//
&$lazy1,
&$lazy2,
&$lazy3
) {
// > Это вызовет конструктор ленивого сервиса и займет 3 секунды...
$lazy1->do();
// > Это вызовет конструктор ленивого сервиса и займет 3 секунды...
$lazy2->do();
// > В этом случае время не потребуется, поскольку объект был создан ранее
$lazy3->do();
};
_assert_microtime($fn, 7.0, 6.0);
// > Теперь сохраним кеш сделанной за скрипт рефлексии для следующего раза
// > в примере мы чистим кеш в начале скрипта, то есть это смысла не имеет
// > на проде кеш вычищают вручную или не трогают вовсе
$di->saveCache();