Laravel имеет мощный IoC-контейнер, но, к сожалению, официальная документация Laravel не описывает все его возможности. Я решил изучить его и задокументировать для собственного использования.
Примеры в данной статье основаны на Laravel 5.4.26, другие версии могут отличаться.
Введение в Dependency Injection
Я не буду объяснять, что такое DI и IoC в этой статье — если вы не знакомы с этими принципами, вы можете прочитать статью "What is Dependency Injection?" от Fabien Potencier (создателя фреймворка Symfony).
Получение контейнера (Container)
В Laravel существует несколько способов получения сущности контейнера * и самый простой из них это вызов хелпера app()
:
$container = app();
Я не буду описывать другие способы, вместо этого я сфокусирую свое внимание на самом контейнере.
*
В Laravel есть класс Application, который наследуется от Container (именно поэтому хелпер называется app()
), но в этой статье я буду описывать только методы класса Container.
Использование Illuminate\Container вне Laravel
Для использования контейнера Laravel вне фреймворка необходимо установить его с помощью Composer, после чего мы можем получить контейнер так:
use Illuminate\Container\Container;
$container = Container::getInstance();
Пример использования
Самый простой способ использования контейнера — указать в конструкторе классы, которые необходимы вашему классу используя type hinting:
class MyClass
{
private $dependency;
public function __construct(AnotherClass $dependency)
{
$this->dependency = $dependency;
}
}
Затем, вместо создания объекта с помощью new MyClass
, вызовем метод контейнера make()
:
$instance = $container->make(MyClass::class);
Контейнер автоматически создаст и внедрит зависимости, что будет эквивалентно следующему коду:
$instance = new MyClass(new AnotherClass());
(За исключением того случая, когда у AnotherClass
есть свои зависимости. В таком случае контейнер автоматически создаст и внедрит его зависимости, зависимости его зависимостей и т.д.)
Реальный пример
Ниже показан более реальный пример, который взят из документации PHP-DI. В нем логика отправки сообщения отделена от логики регистрации пользователя:
class Mailer
{
public function mail($recipient, $content)
{
// Send an email to the recipient
// ...
}
}
class UserManager
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function register($email, $password)
{
// Create the user account
// ...
// Send the user an email to say hello!
$this->mailer->mail($email, 'Hello and welcome!');
}
}
use Illuminate\Container\Container;
$container = Container::getInstance();
$userManager = $container->make(UserManager::class);
$userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');
Связывание интерфейса и реализации
Для начала определим интерфейсы:
interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }
Затем создадим классы, реализующие эти интерфейсы. Они могут зависеть от других интерфейсов (или других классов, как это было ранее):
class MyClass implements MyInterface
{
private $dependency;
public function __construct(AnotherInterface $dependency)
{
$this->dependency = $dependency;
}
}
Теперь свяжем интерфейсы с реализацией с помощью метода bind()
:
$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);
И передадим название интерфейса вместо названия класса в метод make()
:
$instance = $container->make(MyInterface::class);
Примечание: Если вы забудете привязать интерфейс к реализации, вы получите немного странную ошибку:
Fatal error: Uncaught ReflectionException: Class MyInterface does not exist
Это происходит потому, что контейнер пытается создать экземпляр интерфейса (new MyInterface
), который не является классом.
Реальный пример
Ниже представлен реальный пример связывания интерфейса с конкретной реализацией — изменяемый драйвер кеша:
interface Cache
{
public function get($key);
public function put($key, $value);
}
class RedisCache implements Cache
{
public function get($key) { /* ... */ }
public function put($key, $value) { /* ... */ }
}
class Worker
{
private $cache;
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
public function result()
{
// Use the cache for something...
$result = $this->cache->get('worker');
if ($result === null) {
$result = do_something_slow();
$this->cache->put('worker', $result);
}
return $result;
}
}
use Illuminate\Container\Container;
$container = Container::getInstance();
$container->bind(Cache::class, RedisCache::class);
$result = $container->make(Worker::class)->result();
Связывание абстрактных и конкретных классов
Связывание может быть использовано и с абстрактным классом:
$container->bind(MyAbstract::class, MyConcreteClass::class);
Или для замены класса его потомком (классом, который наследуется от него):
$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
Custom Bindings
Если объект при создании требует дополнительной настройки, вы можете передать замыкание вторым параметром в метод bind()
вместо названия класса:
$container->bind(Database::class, function (Container $container) {
return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});
Каждый раз, когда будет запрашиваться класс Database, будет создан новый экземпляр MySQLDatabase с указанной конфигурацией (если нужно иметь только один экземпляр класса, используйте Singleton, о котором говорится ниже).
Замыкание получает в качестве первого параметра экземпляр класса Container, который может быть использован для создания других классов, если это необходимо:
$container->bind(Logger::class, function (Container $container) {
$filesystem = $container->make(Filesystem::class);
return new FileLogger($filesystem, 'logs/error.log');
});
Замыкание также можно использовать для настройки класса после создания:
$container->bind(GitHub\Client::class, function (Container $container) {
$client = new GitHub\Client;
$client->setEnterpriseUrl(GITHUB_HOST);
return $client;
});
Resolving Callbacks
Вместо того, чтобы полностью перезаписывать биндинг, мы может использовать метод resolving()
для регистрации коллбеков, которые будут вызваны после создания требуемого объекта:
$container->resolving(GitHub\Client::class, function ($client, Container $container) {
$client->setEnterpriseUrl(GITHUB_HOST);
});
Если было зарегистрировано несколько коллбеков, все они будут вызваны. Это также работает для интерфейсов и абстрактных классов:
$container->resolving(Logger::class, function (Logger $logger) {
$logger->setLevel('debug');
});
$container->resolving(FileLogger::class, function (FileLogger $logger) {
$logger->setFilename('logs/debug.log');
});
$container->bind(Logger::class, FileLogger::class);
$logger = $container->make(Logger::class);
Также есть возможность регистрации коллбека, который будет вызываться при создании любого класса (это может быть полезно для логгирования или при отладке):
$container->resolving(function ($object, Container $container) {
// ...
});
Расширение класса
Вы также можете использовать метод extend()
для того, чтобы обернуть оригинальный класс и вернуть другой объект:
$container->extend(APIClient::class, function ($client, Container $container) {
return new APIClientDecorator($client);
});
Класс возвращаемого объекта должен реализовывать тот же интерфейс, что и класс оборачиваемого объекта, иначе вы получите ошибку.
Singleton
Каждый раз, когда возникает необходимость в каком либо классе (если указано имя класса или биндинга, созданного с помощью метода bind()
), создается новый экземпляр требуемого класса (или вызывается замыкание). Для того, чтобы иметь только один экземпляр класса необходимо вызвать метод singleton()
вместо метода bind()
:
$container->singleton(Cache::class, RedisCache::class);
Пример с замыканием:
$container->singleton(Database::class, function (Container $container) {
return new MySQLDatabase('localhost', 'testdb', 'user', 'pass');
});
Для того, чтобы получить синглтон из класса, необходимо передать его, опустив второй параметр:
$container->singleton(MySQLDatabase::class);
Экземпляр синглтона будет создан только один раз, в дальнейшем будет использоваться тот же самый объект.
Если у вас уже есть сущность, которую вы хотите переиспользовать, то используйте метод instance()
. Например, Laravel использует это для того, чтобы у класса Container был только один экземпляр:
$container->instance(Container::class, $container);
Произвольное название биндинга
При биндинге вы можете использовать произвольную строку вместо названия класса или интерфейса, однако вы уже не сможете использовать type hinting и должны будете использовать метод make()
:
$container->bind('database', MySQLDatabase::class);
$db = $container->make('database');
Для того, чтобы одновременно иметь название класса и короткое имя, вы можете использовать метод alias()
:
$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, 'cache');
$cache1 = $container->make(Cache::class);
$cache2 = $container->make('cache');
assert($cache1 === $cache2);
Сохранение произвольного значения
Контейнер позволяет хранить и произвольные значения (например, данные конфигурации):
$container->instance('database.name', 'testdb');
$db_name = $container->make('database.name');
Также поддерживается array-access синтаксис, который выглядит более привычно:
$container['database.name'] = 'testdb';
$db_name = $container['database.name'];
Это может быть полезно при использовании его с биндингом-замыканием:
$container->singleton('database', function (Container $container) {
return new MySQLDatabase(
$container['database.host'],
$container['database.name'],
$container['database.user'],
$container['database.pass']
);
});
(Сам Laravel не использует контейнер для хранения конфигурации, для этого существует отдельный класс — Config, а вот PHP-DI так делает).
Совет: array-access синтаксис можно использовать для создания объектов вместо метода make()
:
$db = $container['database'];
Dependency Injection для функций и методов
До сих пор мы использовали DI только для конструкторов, но Laravel также поддерживает DI для произвольных функций:
function do_something(Cache $cache) { /* ... */ }
$result = $container->call('do_something');
Дополнительные параметры могут быть переданы как простой или ассоциативный массив:
function show_product(Cache $cache, $id, $tab = 'details') { /* ... */ }
// show_product($cache, 1)
$container->call('show_product', [1]);
$container->call('show_product', ['id' => 1]);
// show_product($cache, 1, 'spec')
$container->call('show_product', [1, 'spec']);
$container->call('show_product', ['id' => 1, 'tab' => 'spec']);
DI может использован для любых вызываемых методов:
Замыкания
$closure = function (Cache $cache) { /* ... */ };
$container->call($closure);
Статичные методы
class SomeClass
{
public static function staticMethod(Cache $cache) { /* ... */ }
}
$container->call(['SomeClass', 'staticMethod']);
// or:
$container->call('SomeClass::staticMethod');
Методы объекта
class PostController
{
public function index(Cache $cache) { /* ... */ }
public function show(Cache $cache, $id) { /* ... */ }
}
$controller = $container->make(PostController::class);
$container->call([$controller, 'index']);
$container->call([$controller, 'show'], ['id' => 1]);
Сокращения для вызова методов объекта
Container позволяет использовать сокращение вида ClassName@methodName
для создания объекта и вызова его метода. Пример:
$container->call('PostController@index');
$container->call('PostController@show', ['id' => 4]);
Контейнер используется для создания экземпляра класса, т.е.:
- Зависимости передаются в конструктор класса, а также в вызываемый метод
- Вы можете объявить класс как синглтон, если хотите переиспользовать один и тот же объект
- Вы можете использовать интерфейс или произвольное имя вместо названия класса
Пример ниже будет работать:
class PostController
{
public function __construct(Request $request) { /* ... */ }
public function index(Cache $cache) { /* ... */ }
}
$container->singleton('post', PostController::class);
$container->call('post@index');
Наконец, вы можете передать название "метода по умолчанию" в качестве третьего параметра. Если первым параметром передано название класса и не указано название метода, будет вызвать метод по умолчанию. Laravel использует это в обработчиках событий:
$container->call(MyEventHandler::class, $parameters, 'handle');
// Equivalent to:
$container->call('MyEventHandler@handle', $parameters);
Подмена методов объекта
Метод bindMethod()
позволяет переопределить вызов метода, например, для передачи параметров:
$container->bindMethod('PostController@index', function ($controller, $container) {
$posts = get_posts(...);
return $controller->index($posts);
});
Все примеры ниже будут работать, при этом будет вызвано замыкание вместо настоящего метода:
$container->call('PostController@index');
$container->call('PostController', [], 'index');
$container->call([new PostController, 'index']);
Однако любы дополнительные параметры, переданные в метод call()
, не будут переданы в замыкание и они не могут быть использованы:
$container->call('PostController@index', ['Not used :-(']);
Примечания: метод bindMethod()
не является частью интерфейса Container, он есть только в классе Container. См. Pull Request, в котором объясняется, почему параметры не передаются при переопределении.
Биндинг на основе контекста
Может случиться так, что вы захотите иметь разные реализации одного интерфейса в зависимости от места, где он необходим. Ниже показан немного измененный пример из документации Laravel:
$container
->when(PhotoController::class)
->needs(Filesystem::class)
->give(LocalFilesystem::class);
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(S3Filesystem::class);
Теперь контроллеры PhotoController и VideoController могут зависеть от интерфейса Filesystem, но каждый из низ получит свою реализацию этого интерфейса.
Также можно передать замыкание в метод give()
, как вы делаете это в методе bind()
:
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
Или можно использовать именованную зависимость:
$container->instance('s3', $s3Filesystem);
$container
->when(VideoController::class)
->needs(Filesystem::class)
->give('s3');
Биндинг параметров к примитивным типам
Помимо объектов, контейнер позволяет производить биндинг примитивных типов (строк, чисел и т.д.). Для этого нужно передать название переменной (вместо названия интерфейса) в метод needs()
, а в метод give()
передать значение, которое будет подставлено контейнером при вызове метода:
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(DB_USER);
Также мы можем передать замыкание в метод give()
, для того, чтобы отложить вычисление значения до тех пор, пока оно не понадобится:
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(function () {
return config('database.user');
});
Мы не можем передать в метод give()
название класса или именованную зависимость (например, give('database.user')
), потому, что оно будет возвращено как есть. Зато мы можем использовать замыкание:
$container
->when(MySQLDatabase::class)
->needs('$username')
->give(function (Container $container) {
return $container['database.user'];
});
Добавление тегов к биндингам
Вы можете использовать контейнер для добавления тегов к связанным (по назначению) биндингам:
$container->tag(MyPlugin::class, 'plugin');
$container->tag(AnotherPlugin::class, 'plugin');
И затем получить массив сущностей с указанным тегом:
foreach ($container->tagged('plugin') as $plugin) {
$plugin->init();
}
Оба параметра метода tag()
так же принимают и массив:
$container->tag([MyPlugin::class, AnotherPlugin::class], 'plugin');
$container->tag(MyPlugin::class, ['plugin', 'plugin.admin']);
Rebinding
Примечание: эта возможность контейнера используется довольно редко, поэтому вы можете смело пропустить ее описание.
Коллбэк, зарегистрированный с помощью метода rebinding()
, вызывается при изменении биндинга. В примере ниже сессия была заменена уже после того, как она была использована классом Auth, поэтому класс Auth должен быть проинформирован об изменении :
$container->singleton(Auth::class, function (Container $container) {
$auth = new Auth;
$auth->setSession($container->make(Session::class));
$container->rebinding(Session::class, function ($container, $session) use ($auth) {
$auth->setSession($session);
});
return $auth;
});
$container->instance(Session::class, new Session(['username' => 'dave']));
$auth = $container->make(Auth::class);
echo $auth->username(); // dave
$container->instance(Session::class, new Session(['username' => 'danny']));
echo $auth->username(); // danny
Больше информации на эту тему можно найти здесь и здесь.
refresh()
Существует также сокращение, которое может пригодиться в некоторых случаях — метод refresh()
:
$container->singleton(Auth::class, function (Container $container) {
$auth = new Auth;
$auth->setSession($container->make(Session::class));
$container->refresh(Session::class, $auth, 'setSession');
return $auth;
});
Оно так же возвращает существующий экземпляр класса или биндинг (если он существует), поэтому вы можете сделать так:
// это сработает, только если вы вызовете методы `singleton()` или `bind()` с названием класса
$container->singleton(Session::class);
$container->singleton(Auth::class, function (Container $container) {
$auth = new Auth;
$auth->setSession($container->refresh(Session::class, $auth, 'setSession'));
return $auth;
});
Лично мне такой синтаксис кажется немного запутанным, поэтому я предпочитаю более подробную версию, которая описана выше.
Примечание: эти методы не являются частью интерфейса Container, они есть только в классе Container.
Overriding Constructor Parameters
Метод makeWith()
позволяет вам передать дополнительные параметры в конструктор. При этом игнорируются существующие экземпляры или синглтоны (т.е. создается новый объект). Это может быть полезно при создании объектов с разными параметрами и у которых есть какие-либо зависимости:
class Post
{
public function __construct(Database $db, int $id) { /* ... */ }
}
$post1 = $container->makeWith(Post::class, ['id' => 1]);
$post2 = $container->makeWith(Post::class, ['id' => 2]);
Примечание: В Laravel >=5.3 этот метод называется просто make($class, $parameters)
. Он был удален в Laravel 5.4, но потом возвращен обратно под названием makeWith
в версии 5.4.16. Похоже, что в Laravel 5.5 его название будет снова изменено на make()
.
Прочие методы
Я описал все методы, которые показались мне полезными, но для полноты картины я опишу оставшиеся доступные методы.
bound()
Метод bound()
проверяет, существует класс или алиас, связанный с помощью методов bind()
, singleton()
, instance()
или alias()
:
if (! $container->bound('database.user')) {
// ...
}
Также можно использовать метод isset и array-access синтаксис:
if (! isset($container['database.user'])) {
// ...
}
Значение, указано в методах binding()
, instance()
, alias()
Может быть удалено с помощью unset()
:
unset($container['database.user']);
var_dump($container->bound('database.user')); // false
bindIf()
Метод bindIf()
делает то же самое, что и метод bind()
, за тем исключением, что он создает биндинг только если он не существует (см. описание метода bound()
выше). Теоретически его можно использовать в пакете для регистрации биндинга по умолчанию, позволяя пользователю переопределить его.
$container->bindIf(Loader::class, FallbackLoader::class);
Не существует метода singletonIf()
, вместо этого вы можете использовать bindIf($abstract, $concrete, true):
$container->bindIf(Loader::class, FallbackLoader::class, true);
Или написать код проверки самостоятельно:
if (! $container->bound(Loader::class)) {
$container->singleton(Loader::class, FallbackLoader::class);
}
resolved()
Метод resolved()
возвращает true, если экземпляр класса до этого был создан.
var_dump($container->resolved(Database::class)); // false
$container->make(Database::class);
var_dump($container->resolved(Database::class)); // true
Оно сбрасывается при вызове метода unset()
(см. описание метода bound()
выше).
unset($container[Database::class]);
var_dump($container->resolved(Database::class)); // false
factory()
Метод factory()
возвращает замыкание, которое не принимает параметров и при вызове вызывает метод make()
.
$dbFactory = $container->factory(Database::class);
$db = $dbFactory();
wrap()
Метод wrap()
оборачивает замыкание в еще одно замыкание, которое внедрит зависимости в оборачиваемое при вызове. Метод принимает массив параметров, которые будут переданы в оборачиваемое замыкание; возвращаемое замыкание не принимает никаких параметров:
$cacheGetter = function (Cache $cache, $key) {
return $cache->get($key);
};
$usernameGetter = $container->wrap($cacheGetter, ['username']);
$username = $usernameGetter();
Примечание: метод wrap()
не является частью интерфейса Container, он есть только в классе Container.
afterResolving()
Метод afterResolving()
работает точно так же, как и метод resolving()
, за тем исключением, что коллбэки, зарегистрированные с его помощью вызываются после коллбэков, зарегистрированных методом resolving()
.
И наконец…
isShared()
– Проверяет, существует ли синглтон/экземпляр для указанного типа
isAlias()
– Проверяет, существует ли алиас с указанным названием
hasMethodBinding()
– Проверяет, есть ли в контейнере биндинг для указанного метода
getBindings()
– Возвращает массив всех зарегистрированных биндингов
getAlias($abstract)
– Возращает алиас для указанного класса/биндинга
forgetInstance($abstract)
– Удаляет указанный экземпляр класса из контейнера
forgetInstances()
– Удаляет все экземпляры классов
flush()
– Удаляет все биндинги и созданные экземпляры классов, полностью очищая контейнер
setInstance()
– Заменяет объект, возвращаемый getInstance() (подсказка: используйте setInstance(null)
для очистки, в последующем будет создан новый экземпляр контейнера)
Примечание: ни один из этих методов не является частью интерфейса Container.