Минималистичный dependency injection container

fixxxer

К.О.
Партнер клуба
PHP:
class Injector {                                                                                                                                                           
                                                                                                                                                                           
    protected $registry = [];                                                                                                                                              
                                                                                                                                                                           
    public function reuse(Closure $ctor) {                                                                                                                                 
        return function($self) use ($ctor) {                                                                                                                               
            static $instance = null;                                                                                                                                       
            return null === $instance ? $instance = $ctor($self) : $instance;                                                                                              
        };                                                                                                                                                                 
    }                                                                                                                                                                      
                                                                                                                                                                           
    public function setClosure($name, Closure $value) {                                                                                                                    
        $this->$name = function() use ($value) {                                                                                                                           
            return $value;                                                                                                                                                 
        };                                                                                                                                                                 
    }                                                                                                                                                                      
                                                                                                                                                                           
    public function __set($name, $value) {                                                                                                                                 
        $this->registry[$name] = $value;                                                                                                                                   
    }                                                                                                                                                                      
                                                                                                                                                                           
    public function __get($name) {                                                                                                                                         
        if (!array_key_exists($name, $this->registry)) {                                                                                                                   
            throw new InvalidArgumentException("Undefined dependency: '$name'");                                                                                           
        }                                                                                                                                                                  
        return $this->registry[$name] instanceof Closure ? $this->registry[$name]($this) : $this->registry[$name];                                                         
    }                                                                                                                                                                      
                                                                                                                                                                           
}
Тест
PHP:
class InjectorTest extends PHPUnit_Framework_TestCase {

    public function testObjectDependency() {
        $inj = new Injector;
        $inj->dep = function() {
            return new stdClass;
        };
        $this->assertInstanceOf('stdClass', $inj->dep);
    }

    public function testValueDependency() {
        $inj = new Injector;
        $inj->dep = 'test';
        $this->assertEquals('test', $inj->dep);
    }

    public function testInstancesAreDifferent() {
        $inj = new Injector;
        $inj->dep = function() {
            return new stdClass;
        };
        $this->assertNotSame($inj->dep, $inj->dep);
    }

    public function testReusedInstancesAreSame() {
        $inj = new Injector;
        $inj->dep = $inj->reuse(function() {
            return new stdClass;
        });
        $this->assertSame($inj->dep, $inj->dep);
    }

    public function testUsingArguments() {
        $inj = new Injector;
        $inj->flag = 1;
        $inj->dependency = function() {
            return new Dependency;
        };
        $inj->service = function($inj) {
            return new Service($inj->dependency, $inj->flag);
        };
        $service = $inj->service;
        $this->assertInstanceOf('Service', $service);
        $this->assertInstanceOf('Dependency', $service->dependency);
        $this->assertEquals(1, $service->flag);
    }

    public function testClosureDependency() {
        $inj = new Injector;
        $closure = function() { return true; }; 
        $inj->setClosure('closure', $closure);
        $this->assertInstanceOf('Closure', $inj->closure); 
        $this->assertTrue($inj->closure->__invoke());
    }

}

class Dependency {}
class Service {
    public function __construct(Dependency $dependency, $flag) {
        $this->dependency = $dependency;
        $this->flag = $flag;
    }
}
Кто короче? :)

Вполне юзабельно кстати, не true DI конечно, но зато безо всяких там тормозных reflection.
 

ksnk

прохожий
А почему функции определяются явным присвоением пропертей, а атрибуты - заполнением cвойства registry? Почему свойства не присваивать в то-же манере?
Что за непоследовательность? :)
 

fixxxer

К.О.
Партнер клуба
нееее setClosure это чтобы отличить closure-as-a-depencency от factory closure

ну можно сделать $inj->smth = $inj->wrapClosure(function(){ }), так наверное понятнее даже, и в одном стиле с $inj->reuse()
 

AmdY

Пью пиво
Команда форума
Не хватает __call, чтобы дёргать зависимость с параметрами.
$ioc->getDb('master');
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
отлично, жаль только, что на 5.3 лямбды громоздкие из-за $this


я бы, наверное reuse сделал по дефолту как более привычное поведение полей классов, а new - отдельно,
хотя, для DI более востребовано может быть и new на каждый вызов, но читабельней будет указывать это явно
 

fixxxer

К.О.
Партнер клуба
В общем, щас так, вовсю юзается, вполне удобно.
PHP:
/**
 * Simple pseudo-IoC implementation.
 */
class DIContainer {

    protected $registry = [];

    /**
     * Reuse the instance created by $ctor
     *
     * @param Closure $ctor
     * @return Closure
     */
    public function reuse(Closure $ctor) {
        return function($self) use ($ctor) {
            static $instance = null;
            return null === $instance ? $instance = $ctor($self) : $instance;
        };
    }

    /**
     * Register a value
     *
     * @param string $name
     * @param mixed $value
     */
    public function __set($name, $value) {
        $this->registry[$name] = $value;
    }

    /**
     * Returns a closure wrapped for usage as a dependency,
     * to distinguish from a ctor-closure.
     *
     * @param Closure $value
     * @return Closure
     */
    public function wrapClosure(Closure $value) {
        return function() use ($value) {
            return $value;
        };
    }

    /**
     * Get the depencency by its name
     *
     * @param $name
     * @return mixed
     * @throws \InvalidArgumentException
     */
    public function __get($name) {
        if (!array_key_exists($name, $this->registry)) {
            throw new InvalidArgumentException("Undefined dependency: '$name'");
        }
        return $this->registry[$name] instanceof Closure ? $this->registry[$name]($this) : $this->registry[$name];
    }

    /**
     * Call the ctor function for $name dependency with $arguments
     *
     * @param string $name
     * @param array $arguments
     * @return mixed
     * @throws \InvalidArgumentException
     */
    public function __call($name, array $arguments) {
        if (!array_key_exists($name, $this->registry)) {
            throw new InvalidArgumentException("Undefined dependency: '$name'");
        }
        if (!$this->registry[$name] instanceof Closure) {
            throw new InvalidArgumentException("Dependency is not constructable: '$name'");
        }
        array_unshift($arguments, $this);
        return call_user_func_array($this->registry[$name], $arguments);
    }

}
PHP:
class Corelib_DIContainerTest extends PHPUnit_Framework_TestCase {

    public function testObjectDependency() {
        $inj = new DIContainer;
        $inj->dep = function() {
            return new stdClass;
        };
        $this->assertInstanceOf('stdClass', $inj->dep);
    }

    public function testValueDependency() {
        $inj = new DIContainer;
        $inj->dep = 'test';
        $this->assertEquals('test', $inj->dep);
    }

    public function testInstancesAreDifferent() {
        $inj = new DIContainer;
        $inj->dep = function() {
            return new stdClass;
        };
        $this->assertNotSame($inj->dep, $inj->dep);
    }

    public function testReusedInstancesAreSame() {
        $inj = new DIContainer;
        $inj->dep = $inj->reuse(function() {
            return new stdClass;
        });
        $this->assertSame($inj->dep, $inj->dep);
    }

    public function testUsingArguments() {
        $inj = new DIContainer;
        $inj->flag = 1;
        $inj->dependency = function() {
            return new Corelib_DIContainerTest_Dependency;
        };
        $inj->service = function($inj) {
            return new Corelib_DIContainerTest_Service($inj->dependency, $inj->flag);
        };
        $service = $inj->service;
        $this->assertInstanceOf('Corelib_DIContainerTest_Service', $service);
        /** @noinspection PhpUndefinedFieldInspection */
        $this->assertInstanceOf('Corelib_DIContainerTest_Dependency', $service->dependency);
        /** @noinspection PhpUndefinedFieldInspection */
        $this->assertEquals(1, $service->flag);
    }

    public function testUsingCallArguments() {
        $inj = new DIContainer;
        $inj->dependency = function() {
            return new Corelib_DIContainerTest_Dependency;
        };
        $inj->service = function($inj, $flag) {
            return new Corelib_DIContainerTest_Service($inj->dependency, $flag);
        };
        /** @noinspection PhpUndefinedMethodInspection */
        $service = $inj->service(1);
        $this->assertInstanceOf('Corelib_DIContainerTest_Service', $service);
        $this->assertInstanceOf('Corelib_DIContainerTest_Dependency', $service->dependency);
        $this->assertEquals(1, $service->flag);
    }

    public function testClosureDependency() {
        $inj = new DIContainer;

        $inj->closure = $inj->wrapClosure(function() {
            return true;
        });

        $this->assertInstanceOf('Closure', $inj->closure);
        /** @noinspection PhpUndefinedMethodInspection */
        $this->assertTrue($inj->closure->__invoke());
    }

}

class Corelib_DIContainerTest_Dependency {}
class Corelib_DIContainerTest_Service {
    public function __construct(Corelib_DIContainerTest_Dependency $dependency, $flag) {
        $this->dependency = $dependency;
        $this->flag = $flag;
    }
}
 

Ragazzo

TDD interested
fixxxer
а можно как-то для "тупых" разъяснить что это и для чего? а то я что-то даже из тестов мало понял зачем куча lambda и один registry что-то эмулируют :) Это какой-то service-locator или что это? :S
 

fixxxer

К.О.
Партнер клуба
Да, это сервис локатор с IoC-подходом, но без автоматизации - замыканиями-конструкторами сам его учишь зависимостям, зависимости тупо именованы.

Dependency management with factory lambdas, вот. =)

На самом деле я такое сначала написал для JS (где по-другому, в общем-то, и никак - только если парсить function.toString, как в angular - да и там сводится к примерно тому же в итоге), но потом подумал что и в PHP ведь так же можно :)
 

MiksIr

miksir@home:~$
А что, рефлексия медленная? Ну кешировать ее, кешировать ;) var_dump-ом ;) Интересно, а опкод кешер ускоряет рефлексию?
 

fixxxer

К.О.
Партнер клуба
Медленная. Кэшировать в какой-нибудь apc можно, можно вообще CacherFactory и пачку кэшеров, но это уже, простите, слишком.

Плюс - это нужно только для фреймворкообразующих центральных классов (и тут заходит вопрос о сокращении времени бутстрапа как раз). IoC на все подряд это бред - замучаешься везде вместо new писать строки с fully qualified name. Только если аспекты еще прикрутить, но нет уж, нет уж.
 

MiksIr

miksir@home:~$
Я делал просто анализ конструктора и создание соответствующих объектов. В инжектор при старте только минимум передавался - конфиг, мапинг интрефейс-класс... ну это в общем было достаточно простое ПО, так что этого хватало.
 

fixxxer

К.О.
Партнер клуба
Да мне тоже phemto хватило бы. Просто нафиг лишний оверхед.

Ну и писать '\Foo\Bar\Bazz\ClassName' не хочется. :) Я таки понял зачем нужен ::class из php 5.5 ;)
 

fixxxer

К.О.
Партнер клуба
grigori
Это для гипотетической ситуации
PHP:
class SomeService {
    public function __construct(\Closure $fn) { }
}
Исключительно полноценности и универсальности ради. :) Если такое точно никогда не нужно - можно смело выбросить.
 
Сверху