#Понимание IoC
Инверсия управления (англ. Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения зацепления в компьютерных программах[источник не указан 66 дней]. Также архитектурное решение интеграции, упрощающее расширение возможностей системы, при котором контроль над потоком управления программы остаётся за каркасом. © Wikipedia.
Сегодня хотелось бы по говорить о реализации инверсии управления в Laravel. Это один из самых важных аспектов организации слабой связанности компонентов в любимом нами фреймворке и его понимание играет ключевую роль при создании качественных пакетов и приложений.
Когда мы говорим об IoC в Laravel, то следует знать, что он стоит на трех китах:
- Внедрение зависимостей (Dependency Injection)
- Сервис-контейнер (ServiceContainer)
- Отражения (Reflection)
Сначала, мы поговорим каждом из них в отдельности. А потом, о том как они связаны между собой.
##Внедрение зависимостей
Предположим, что у нас есть контроллер. И для его успешной работы ему нужен объект некоего служебного класса (сервиса), который выполнит для него какую-то работу. Для примера, пусть этим служебным классом будет некий хитрый мэйлер, который посылает почту каким-то хитрым образом (сейчас это не важно). Мы без труда можем создать объект мэйлера прямо в методе контроллера.
<?php namepace App\Http\Controllers
use Request;
use Mailers\Mailer;
use Models\User;
use Response
use App\Http\Controllers\Controller;
class MailController extends Controller
{
#...
public function sendMail()
{
//Создали объект мэйлера
$mailer = new Mailer;
$mailer
->from(Request::get('sender_id'))
->to(Request::get('receiver_id'))
->subject(Request::get('subject'));
$result = $mailer->send();
return Responce::json($result);
}
#...
}
Теперь предположим, что у нас во многих наших методах используется мэйлер. Логично вынести создание мэйлера в конструктор.
<?php namepace App\Http\Controllers
use Request;
use App\Mailers\Mailer;
use App\Models\User;
use Response
use App\Http\Controllers\Controller;
class MailController extends Controller
{
#...
public function __construct()
{
//вынесли создание объекта мэйлера в метод-конструктор
$this->mailer = new Mailer;
}
public function sendMail()
{
$this->mailer
->from(Request::get('sender_id'))
->to(Request::get('receiver_id'))
->subject(Request::get('subject'));
$result = $this->mailer->send();
return Responce::json($result);
}
#...
}
Это круто и классно работает. Ровно до тех пор, пока вам нравится ваш мэйлер; Допустим, что этот мэйлер, организовывал переписку между пользователями посредством электронной почты. И вам внезапно захотелось вести переписку между пользователями локально - в вашем приложении. Тогда, вы заменяете класс App\Mailers\Mailer
на App\Mailers\LocalMailer
, но тут есть два "но".
Во-первых, нет никаких гарантий, что новый LocalMailer имеет те же публичные методы для работы.
Во-вторых, если этот старый мэйлер используется в куче различных контроллеров, то вам придется руками заменить его везде, где он создается. Вот здесь к нам на помощь и приходят итерфейсы и внедрение зависимостей;
Для гарантии того, что оба мэйлера имеют одинаковые публичные методы, нужно определить интерфейс которому они должны удовлетворять.
<?php namespace App\Mailers\Contracts;
class MailerInterface {
public function from();
public function to();
public function subject();
public function send();
}
и сами мэйлеры должны реализовывать, этот интерфейс.
<?php namespace App\Mailers;
use App\Mailers\Contracts\MailerInterface;
class Mailer implements MailerInterface {
}
Теперь при создании контроллера конкретный мэйлер можно внедрить
use Request;
use App\Mailers\Mailer;
use App\Models\User;
use Response
use App\Http\Controllers\Controller;
class MailController extends Controller
{
#...
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
#...
}
Теперь наш контроллер может одинаково хорошо работать с любым полученым в конструкторе мэйлером вне зависимости от конкретной его реализации локальной или почтовой. Внедрение этой зависимости происходит снаружи и контроллеру вообще не обязательно знать какой это конкретно мэйлер - главное, что в нем есть все необходимые методы для работы - это гарантирует интерфейс, который мы указали при типизации аргумента.
Обычное внедрение зависимости снаружи класса выглядит так:
use App\Mailers\Mailer;
use App\Mailers\LocalMailer;
#...
$controller = new MailController(new LocalMailer);
//или
$controller = new MailController(new Mailer);
Это и называется Dependency Injection.
**Выжимка:**Внедрение зависимостей, это когда конкретный объект создается не в контексте исполнения (функции или метода класса), а приходит туда извне в виде аргумента. А гарантия доступности публичных методов обеспечивается типизацией интерфейса, абстрактного (или конкретного) класса.
Однако Laravel не стал бы столь популярным если бы в нем небыло волшебства. Двинемся дальше в поисках магии...
##Сервис-контейнер
Сервси-контейнер в Laravel решает две основных задачи:
- Он может хранить информацию о том, как получить данные;
- Он может хранить данные;
хотя я здесь говрю "данные" ( и это действительно так ), на практике, в контейнере чаще всего хранится объект или нформация о том, как его получить.
Зарзберем каждый пункт подробнее.
###Хранение информации о получении объекта
Все примеры приведены так, как если бы они использовались в сервис-провайдере.
####Связывание
$this->app->bind('some', 'App\SomeClass');
Теперь, каждый раз, когда мы будем обращаться к сервис-контейнеру таким образом:
$some = $this->app->make('some');
Он будет возвращать вновь созданный объект класса App\SomeClass
.
Если мы хотим, чтобы сервисконтейнер не только возвращал нам класс, но и пресетировал (преднастраивал) его или если создание объекта класса требует каких-то особых аргументов, мы можем передать вторым параметром не название класса, а замыкание и описать все необходимые действия;
$this->app->bind('some', function($app){
$some = new \App\SomeClass('argument_1', 'argument_2');
$some->setSomething('example');
return $some;
});
Обратите внимание, что замыкание единственным аргументом принимает принимает объект класса Illuminate\Foundation\Application
, который и является этим самым app, который доступе через алиас фасада App
, хелпер app(), а также как $this->app
во многих классах Laravel. Таким образом, внутри замыкания можно вызывать данные из контейнера, которые были определены ранее. Например так:
$this->app->bind('some', function($app){
return new \App\SomeClass($app->make('some.else'));
});
####Одиночки
Следующий пример, делает точно тоже самое что и предыдущий, однако объект класса будет создан однажды - при первом вызове, а все последующие обращения к тому же контейнеру будут возвращать созданный ранее объект:
$this->app->singleton('some', 'App\SomeClass');
$check = $this->app->make('some') === $this->app->make('some') ; // true - это один и тот же объект;
####Инстансы Связывание инстансов (экземпляров) вдедет себя точно так же как и связывание одиночек, с той лишь разницей, что объект в контейнере создается не в момент первого вызова, а еще до помещения в контейнер.
$some = new Some;
$this->app->instance('some', $some);
само собой, здесь нет необходимости в замыканиях, так как всё необходимое пресетирование можно сделать еще до помещения в контейнер.
####Немного магии Если попытаться вызвать из контейнера объект, который не был туда помещен, просто по имени класса
$this->app->make('App\SomeClass');
то контейнер попытается создать объект этого класса, и в случае успеха поместит этот объект в себя. После чего он (объект) будет доступен также как singleton или instance, и все последующие вызовы будут возвращать тот же объект.
##Рефлексии (они же Отражения, они же Симметрии)
В PHP5 существует набор классов - так называемый Reflection API - который позволяет исследовать существующие классы и собирать их мета-данные. Что это означает? Это означает, что через рефлексии можно получить информаю о методах класса, типизации их аргументов, а также собирать информацию из док-блоков. Я думаю, что многие из вас видели или даже работали с генераторами документации по API приложения. Они работают как раз через исследование рефлексий. В контексте IoC, нас интересует в первую очередь именно типизация аргументов.
Я не буду вдаваться в подробности работы с рефлексиями, так как это материал не на одну статью (да и опыта с ними, у меня не так много). Если есть желание "расчехлится", то добро пожаловать в соответствующий раздел php.net
На самом деле, я умолчал об одном важном моменте. Вся необходимая работа с рефлексиями уже включена в механизм сервис-контейнера, и самим с ними делать ничего не придется. Упомянул я о них лишь для того, чтобы не оставалось белых пятен в понимании того, как контейнер узнает о том, что мы хотим получить в нашем конкретном контроллере или каком-то сервисном классе.
##Собираем все в кучу, и готовим IoC
И так, мы имеем:
- Внедрение зависимостей. Это когда мы не думаем о том с чем мыработаем, а думаем лишь о том как с этим можно работать. А что именно это будет - определяется вне контекста исполнения.
- Сервис контейнер. Он обеспечивает доступ к объектам, или информации о том, как их правильно создавать.
- Рефлексии. Они позволяют нам получить информацию о том что хочет получить объект или класс.
###Как это работает
Каждый раз, когда мы вызываем что-то контейнера
$this->app->make('App\SomeClass');
, для создания чего небыло определено замыкания (то есть: он либо был привязан без замыкания bind/singleton
, либо вообще не был помещен в контейнер ), контейнер с помощью рефлексии исследует конструктор этого класса, и пытается разрешить эти зависимости снова вызывая $this->app->make('<Класс зависимости>'), и так рекусивно снова и снова, пока все зависимости не будут разрешены.
###Как это применять
Помните мы в самом начеле статьи, мы говорили про Mailer?
и у нас в конструкторе была определена зависимость:
#...
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
#...
Так вот, контрллеры в Laravel создаются через этот самый App::make()
, а это означает что все зависимости будут по возможности разрешены. Ну а так как, в нашем случа,е мы используем для типизации интерфейс (и, как мы помним из мануала php, экземпляры интерфейсов и абстрактных классов созданы быть не могут), а не конкретный класс, то нам в сервис провайдере нужно определить разрешение для этой зависимости:
$this->bind('App\Mailers\Contracts\MailerInterface', 'App\Mailers\LocalMailer');
И теперь во всех контроеллерах, и классах созданных через App::make() где будет внедрен MailerInterface
, для разрешения зависимости будет поставлен LocalMailer
. И если нам когда взбредет в голову моменять его на что-то иное, то нам будет достаточно заменить его в нашем сервис-провайдере на то, что нам нужно. Само собой разумеется, что это "что-то" должно реализовывать MailerInterface
.
Но нам не всегда нужно или хочется гордить интерфейсы. Если нам достаточно получить объект какогото конкретного класса, то мы можем просто Типизировать аргумент этим классом,и контейнер попытается создать его через App::make()
:
#...
public function __construct(LocalMailer $mailer)
{
$this->mailer = $mailer;
}
#...
В этом случае, нам вообще не нужно прописывать привязку в сервис-провайдере.
На последок следует сказать, что разрешение зависимостей возможно не только при создании объектов, но и при вызове функций/замыканий/методов.
App::call();
При работе с сервис-контейнером есть множество трюков, которые позволяют легко жонглировать зависимостями. Большинство из них описаны в документации en, ru. Ну и не стесняйтесь исследовать сорцы - там самое интересное.
Всем спасибо! шумите в чатике, поглядывайте в ленту в группе