Внедрение зависимости c Inversion

11 February 2013
33

Inversion это простой и функциональный контейнер внедрения зависимости для PHP 5.3. Поддерживает сервис-ориентированную архитектуру, ссылки, PRS-0, и Composer.

Установить можно через packagist.org: granula/inversion либо скачав и добавив к PRS-0 совместимому загрузчику.

<?php
$container = new Inversion\Container();
$container['foo'] = 'My\Class\Foo';
// ...
$foo = $container('foo');

В вышеприведённом примере показана базовая функциональность контейнера. Разберем что там происходит. В первой строчки создаем экземпляр контейнера. Во второй создаем ассоциацию между “foo” и сервисом создающим экземпляр класса “My\Class\Foo”. Что по другому можно записать так:

<?php
$container->addService(new Service('My\Class\Foo'), 'foo');
Имя "foo" идёт вторым, т.к. его вообще можно опустить. Подробнее ниже.
В третей строчке мы получаем экземпляр объекта. Что по другому можно записать так:
<?php
$foo = $container('foo');
// или
$foo = $container->get('foo');
// или
$foo = $container['foo']->get();
// или
$foo = $container->getService('foo')->get();
Однако, рекомендую использовать сокращённый вариант, хотя все они допустимы.

Описание зависимостей

По умолчанию когда в контейнер передаётся строка она понимается как имя класса и подставляется в сервис Inversion\Servise. У данного сервиса есть несколько особенностей и функций. Первое это отложенная загрузка. Пока вы не будите использовать его, класс не будет загружен. Второе, вы можете указать зависимость от других сервисов и параметров. Объясню на примере. Пусть у нас есть класс Bar, который зависит от классов One и Two:
<?php
namespace My\Space;
class One {}
class Two {}
class Bar
{
    public function __construct(One $one, Two $two)
    {
    }
}

Опишем эту зависимость в Inversion:

<?php
use Inversion\Service;
//...
$container['one'] = 'My\Space\One';
$container['two'] = 'My\Space\Two';
$container['bar'] = new Service('My\Space\Bar', array($container['one'], $container['two']));

Теперь при вызове “bar”, они будут созданы и подставлены в конструктор. На самом деле можно ещё проще. Если вместо “one” и “two” указать их имена классов:

<?php
$container['My\Space\One'] = 'My\Space\One';
$container['My\Space\Two'] = 'My\Space\Two';
$container['My\Space\Bar'] = new Service('My\Space\Bar'); // "new Service" можно опустить

Это удобный способ описывать зависимости при использовании интерфейсов:

<?php
namespace My\Space;
class One implements OneInterface {}
class Two implements TwoInterface  {}
class Bar implements BarInterface
{
    public function __construct(OneInterface $one, TwoInterface $two)
    {
    }
}
<?php
$container['My\Space\OneInterface'] = 'My\Space\One';
$container['My\Space\TwoInterface'] = 'My\Space\Two';
$container['My\Space\BarInterface'] = 'My\Space\Bar';

Вообще имена интерфейсов, можно опустить. Они будут автоматически получены из классов:

<?php
$container[] = 'My\Space\One';
$container[] = 'My\Space\Two';
$container[] = 'My\Space\Bar';

Вот так вот просто.

Однако, нужно понимать что в таком случае классы будут сразу же загружены чтобы получить список интерфейсов через рефлексию. Поэтому лучше указывать имя интерфейса вручную.

Другие виды сервисов

В библиотеке идет несколько сервисов, однако вы можете создать свой имплементировав Inversion\ServiceInterface.

Closure

Класс: Inversion\Service\Closure Использование:
<?php
$container['closure'] = function () use ($container) {
    return new My\Class();
};

Можно также указать зависимости:

<?php
$container['closure'] = function (One $foo, Two $foo) use ($container) {
    return new My\Class();
};

Так же как и с Inversion\Service можно указать их явно:

<?php
$container['closure'] = new Closure(function (One $foo, Two $foo) use ($container) {
    return new My\Class();
}, array($container['one'], $container['two']));

Factory

Класс: Inversion\Service\Factory Использование:
<?php
$container['factory'] = new Factory('My\ClassFactory', 'create');

Так же можно указать зависимости для конструктора явно третьим параметром.

Object

Класс: Inversion\Service\Object Использование:
<?php
$container['object'] = new My\Class();

или

<?php
$container['object'] = new Object(new My\Class());

Prototype

Класс: Inversion\Service\Prototype Использование:
<?php
$container['prototype'] = new Prototype($object);

При каждом вызове будет создана новая копия: clone $object.

Data

Класс: Inversion\Service\Data Использование:
<?php
$container['data'] = new Data('what you want');

По умолчанию все массивы преобразуется в Data сервисы.

<?php
$container['data'] = array(...);

Эквивалентно:

<?php
$container['data'] = new Data(array(...));

Ссылки на сервисы

Inversion поддерживает ссылки. Что бы получить ссылку обратитесь к контейнеру как к массиву:
<?php
$container['foo'] = new Service(...);

$ref = $container['foo']; // Ссылка на сервис.

Таким образом можно создать алиас к любому сервису:

<?php
$container['My\Class\FooInterface'] = new Service('My\Class\Foo');
$container['foo'] = $container['My\Class\FooInterface'];
//...
$foo = $container('foo');

Теперь если кто-нибудь перезапишет “My\Class\FooInterface”, то “foo” будет по прежнему ссылаться на этот сервис:

<?php
//...
$container['My\Class\FooInterface'] = new Service('Another\FooImpl');
//...
$foo = $container('foo'); // $foo instanseof Another\FooImpl

Можно даже создавать ссылки на ссылки:

<?php
$container['foo'] = 'My\Class\Foo';
$container['ref'] = $container['foo'];
$container['ref2'] = $container['ref'];
$container['ref3'] = $container['ref2'];
//...
$foo = $container('ref3'); // $foo instanseof My\Class\Foo
$name = $container->getRealName('ref3'); // $name == 'foo'

Расширение сервисов

Например если мы хотим расширить какой-нибудь сервис, то такой способ не подойдет т.к. он перезапишет первый:
<?php
$container['My\Class\FooInterface'] = 'My\Class\Foo';
//...
$container['My\Class\FooInterface'] = function (FooInterface $foo) {
    $foo->extendSome(...);
    return $foo;
};

В результате будет зацикливание, что бы этого избежать, для расширения используйте следующую функцию:

<?php
$container['My\Class\FooInterface'] = 'My\Class\Foo';
//...
$container->extend('My\Class\FooInterface'], function (FooInterface $foo) {
    return new FooDecorator($foo);
});

Тесты

Библиотека Inversion полностью тестирована. Тесты находятся в отдельном репозитории (granula/test) вместе с другими тестами гранулы.

Как Singleton

Inversion спроектирована полностью без использования статических методов и синглетонов, однако редко бывает полезно иметь контейнер как синглетон:
<?php
$container = Inversion\Container::getInstanse();

Другие реализации

  • Symfony Dependency Injection - мощная и тяжёлая библиотека внедрения зависимости. Имеет хорошую документацию.
  • Pimple - простой и очень лёгкий (всего один файл) "контейнер" от создателя Symfony.


Hi, I’m Anton. If you liked the post share it
If you found a typo edit post on GitHub.