Skip to content

6562680/di

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dependency Injection / Container

Контейнер внедрения зависимостей с поддержкой кеша, ленивых сервисов и фабрик.

Поддерживает файловый кеш для рефлексии, однако сохраняет в рефлексию в рантайме.
Если вы хотите сохранить её в хранилище - вызовите метод ->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.');

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages