gzhegow / di
There is no license information available for the latest version (1.5.6) of this package.
1.5.6
2024-06-28 10:13 UTC
Requires
- php: ^7.2|^8.0
- psr/container: ^1.0|^2.0
README
Контейнер внедрения зависимостей с поддержкой кеша, ленивых сервисов и фабрик.
Поддерживает файловый кеш для рефлексии, однако сохраняет в рефлексию в рантайме.
Если вы хотите сохранить её в хранилище - вызовите метод ->flushCache() в конце работы скрипта.
Функция разогрева не имеет смысла, потому что это требует "создать все возможные обьекты во всех возможных комбинациях".
Установка
composer require gzhegow/di;
Пример
<?php use Gzhegow\Di\Lib; use Gzhegow\Di\Demo\MyClassFour; use Gzhegow\Di\Demo\MyClassFive; use Gzhegow\Di\Demo\MyClassThree; use Gzhegow\Di\Reflector\ReflectorCache; use Gzhegow\Di\Demo\MyClassOneAwareInterface; use Gzhegow\Di\Demo\MyClassTwoAwareInterface; require_once __DIR__ . '/vendor/autoload.php'; // >>> Настраиваем PHP ini_set('memory_limit', '32M'); // >>> Настраиваем отлов ошибок error_reporting(E_ALL); set_error_handler(static function ($severity, $err, $file, $line) { if (error_reporting() & $severity) { throw new \ErrorException($err, -1, $severity, $file, $line); } }); set_exception_handler(static function ($e) { var_dump(Lib::php_dump($e)); var_dump($e->getMessage()); var_dump(($e->getFile() ?? '{file}') . ': ' . ($e->getLine() ?? '{line}')); die(); }); // >>> INITIALIZE & CONFIGURE // >>> Создаем контейнер $di = (new \Gzhegow\Di\DiFactory())->newDi(); $di::setInstance($di); // >>> Ставим режим работы контейнера $di->setInjectorSettings([ 'injectorResolveUseTake' => true, // > будет использовать take() вместо get(), то есть при незарегистированных зависимостях создаст новые экземпляры и передаст их в конструктор (условно это "test/staging") // 'injectorResolveUseTake' => false, // > будет использовать get() вместо take(), а значит при незарегистированных зависимостях выбросит исключение (условно это "production") ]); // >>> Настраиваем кеш для рефлексии функций и конструкторов $cacheDir = __DIR__ . '/var/cache'; $cacheNamespace = 'app.di'; // >>> Можно использовать путь к файлу, в этом случае кеш будет сделан через file_{get|put}_contents() + (un)serialize() $cacheDirpath = "{$cacheDir}/{$cacheNamespace}"; $di->setCacheSettings([ // 'reflectorCacheMode' => ReflectorCache::CACHE_MODE_NO_CACHE, // > не использовать кеш совсем // 'reflectorCacheMode' => ReflectorCache::CACHE_MODE_RUNTIME, // > использовать только кеш памяти на время текущего скрипта 'reflectorCacheMode' => ReflectorCache::CACHE_MODE_STORAGE, // > использовать файловую систему или адаптер (хранилище) // 'reflectorCacheDirpath' => $cacheDirpath, ]); // >>> Либо можно установить пакет `composer require symfony/cache` и использовать адаптер, чтобы запихивать в Редис например // $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 // ); // $di->setCacheSettings([ // 'reflectorCacheMode' => ReflectorCache::CACHE_MODE_STORAGE, // // // 'reflectorCacheAdapter' => $symfonyCacheAdapter, // ]); // >>> Так можно очистить кеш принудительно (обычно для этого делают консольный скрипт и запускают вручную или кроном, но если использовать symfony/cache можно и просто установить TTL - время устаревания) print_r('Clearing cache...' . PHP_EOL); $di->clearCache(); print_r('Cleared.' . PHP_EOL); print_r(PHP_EOL); // >>> SERVICE REGISTRATION // >>> Можно зарегистрировать класс в контейнере (смысл только в том, что метод 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; }; // >>> И его результат будет сохранен как одиночка, то есть при втором вызове get()/ask() вернется тот же экземпляр $di->bindSingleton('\Gzhegow\Di\Demo\MyClassOneInterface', $fnNewMyClassOne); // >>> А также зарегистрируем алиас на наш интерфейс по имени (это позволяет нам использовать сервис-локатор не создавая интерфейсы под каждую настройку зависимости) // > Если мы задаем конфигурацию в виде строк, а не в виде 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(MyClassOneAwareInterface::class, static function (MyClassOneAwareInterface $aware) use ($di) { $one = $di->get('one'); $aware->setOne($one); }); $di->extend(MyClassTwoAwareInterface::class, static function (MyClassTwoAwareInterface $aware) use ($di) { $two = $di->getLazy('two'); $aware->setTwo($two); }); // >>> RUNTIME // >>> Пример. "Дай сервис c заполненными зависимостями" print_r('Case1:' . PHP_EOL); // // > Используя параметр $contractT можно задавать имя класса, который поймет PHPStorm как генерик и будет давать подсказки // $object = $di->get(MyInterface::class, $contractT = MyClass::class, $forceInstanceOf = false, $parametersWhenNew = []); // > get() бросит исключение, если не зарегистрировано в контейнере // $object = $di->ask(MyInterface::class, $contractT = MyClass::class, $forceInstanceOf = false, $parametersWhenNew = []); // > использует get() если зарегистрировано, NULL если не зарегистрировано // $object = $di->make(MyInterface::class, $parameters = [], $contractT = MyClass::class); // > всегда новый экземпляр с параметрами // $object = $di->take(MyInterface::class, $parametersWhenNew = [], $contractT = MyClass::class); // > get() если зарегистрировано, make() если не зарегистрировано // $three = $di->get('three'); // var_dump(get_class($three)); // string(28) "Gzhegow\Di\Demo\MyClassThree" Lib::assert_true(get_class($three) === 'Gzhegow\Di\Demo\MyClassThree'); // // >>> Если класс помечен как сиглтон, запросы его вернут один и тот же экземпляр $three1 = $di->get('\Gzhegow\Di\Demo\MyClassThree'); $three2 = $di->get('\Gzhegow\Di\Demo\MyClassThree'); $threeByAlias = $di->get('three'); Lib::assert_true($three1 === $three2); Lib::assert_true($three1 === $threeByAlias); print_r(PHP_EOL); // >>> Ранее мы говорили, что этот сервис слишком долго выполняет конструктор. Запросим его как ленивый. При этом подстановка в аргументы конструктора конечно будет невозможна, но как сервис-локатор - удобная вещь! // >>> В PHP к сожалению нет возможности создать анонимный класс, который расширяет ("extend") имя класса, который лежит в переменной. Поэтому, к сожалению, только такие LazyService... print_r('Case2:' . 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); // $two1 = $di->getLazy('two', $contractT = null, [ 'hello' => 'User1' ]); $two2 = $di->makeLazy('two', [ 'hello' => 'User2' ]); // > make создаст новый объект, но не перепишет имеющийся синглтон $two1Again = $di->getLazy('two'); // > то есть тут мы получим $twoWithUser, а не $twoWithUser2 // var_dump(get_class($two1)); // string(34) "Gzhegow\Di\LazyService\LazyService" var_dump(get_class($two2)); // string(34) "Gzhegow\Di\LazyService\LazyService" var_dump(get_class($two1Again)); // string(34) "Gzhegow\Di\LazyService\LazyService" Lib::assert_true(get_class($two1) === 'Gzhegow\Di\LazyService\LazyService'); Lib::assert_true(get_class($two2) === 'Gzhegow\Di\LazyService\LazyService'); Lib::assert_true(get_class($two1Again) === 'Gzhegow\Di\LazyService\LazyService'); // // >>> При вызове первого метода объект внутри LazyService будет создан с аргументами, что указали в __configure() или без них (только зависимости), если не указали echo 'MyClassTwo загружается (3 секунды)...' . PHP_EOL; // MyClassB загружается (3 секунды)... $two1->do(); // Hello, [ User1 ] ! $two1Again->do(); // Hello, [ User1 ] ! Lib::assert_true($two1 !== $two1Again); Lib::assert_true($two1->instance === $two1Again->instance); echo 'MyClassTwo загружается (3 секунды)...' . PHP_EOL; // MyClassB загружается (3 секунды)... $two2->do(); // Hello, [ User2 ] ! Lib::assert_true($two1 !== $two2); Lib::assert_true($two1->instance !== $two2->instance); print_r(PHP_EOL); // >>> Еще пример. "Дозаполним аргументы уже существующего объекта, который мы не регистрировали в контейнере" print_r('Case3:' . PHP_EOL); $four = new MyClassFour(); // // $di->autowire($four, $customArgs = [], $customMethod = '__myCustomAutowire'); // $di->autowire($four); // var_dump(get_class($four)); // string(27) "Gzhegow\Di\Demo\MyClassFour" var_dump(get_class($four->one)); // string(29) "Gzhegow\Di\Demo\MyClassOneOne" Lib::assert_true(get_class($four) === 'Gzhegow\Di\Demo\MyClassFour'); Lib::assert_true(get_class($four->one) === 'Gzhegow\Di\Demo\MyClassOneOne'); // $four2 = new MyClassFour(); $di->autowire($four2); Lib::assert_true($four->one === $four2->one); // > зависимость по интерфейсу, зарегистрированная как одиночка, будет равна в двух разных экземплярах print_r(PHP_EOL); // >>> Еще пример. "Дозаполним аргументы уже существующего объекта, который мы не регистрировали, и который имеет зависимости, которые мы тоже не регистрировали" print_r('Case4:' . PHP_EOL); $five = new MyClassFive(); $di->autowire($five); // var_dump(get_class($five)); // string(27) "Gzhegow\Di\Demo\MyClassFive" var_dump(get_class($five->four)); // string(27) "Gzhegow\Di\Demo\MyClassFour" Lib::assert_true(get_class($five) === 'Gzhegow\Di\Demo\MyClassFive'); Lib::assert_true(get_class($five->four) === 'Gzhegow\Di\Demo\MyClassFour'); // $five2 = new MyClassFive(); $di->autowire($five2); Lib::assert_true($five->four !== $five2->four); print_r(PHP_EOL); // >>> Еще пример. "Вызовем функцию, подбросив в неё зависимости" print_r('Case5:' . PHP_EOL); $fn = static function ( $arg1, MyClassThree $three, $arg2 ) { Lib::assert_true($arg1 === 1); Lib::assert_true($arg2 === 2); return get_class($three); }; $args = [ 'arg1' => 1, 'arg2' => 2, ]; $result = $di->callUserFuncArray($fn, $args); // // > можно и так, но поскольку аргументы передаются по порядку - придется указать NULL для тех, что мы хотим распознать, так что всегда разумнее применять call_user_func_array // $args = [ 1, null, 2 ]; // $result = $di->callUserFunc($fn, ...$args); // var_dump($result); // string(28) "Gzhegow\Di\Demo\MyClassThree" Lib::assert_true($result === 'Gzhegow\Di\Demo\MyClassThree'); print_r(PHP_EOL); // >>> Теперь сохраним кеш сделанной за скрипт рефлексии для следующего раза (в примере мы чистим кеш в начале скрипта, то есть это смысла не имеет, но на проде кеш вычищают вручную или не трогают вовсе) print_r('Saving cache...' . PHP_EOL); $di->flushCache(); print_r('Saved.');