Skip to content

Instantly share code, notes, and snippets.

@greabock
Last active May 4, 2022 08:55
Show Gist options
  • Save greabock/02c13c428304c5ce9ec4 to your computer and use it in GitHub Desktop.
Save greabock/02c13c428304c5ce9ec4 to your computer and use it in GitHub Desktop.
IoC

#Понимание IoC

Инверсия управления (англ. Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения зацепления в компьютерных программах[источник не указан 66 дней]. Также архитектурное решение интеграции, упрощающее расширение возможностей системы, при котором контроль над потоком управления программы остаётся за каркасом. © Wikipedia.

Сегодня хотелось бы по говорить о реализации инверсии управления в Laravel. Это один из самых важных аспектов организации слабой связанности компонентов в любимом нами фреймворке и его понимание играет ключевую роль при создании качественных пакетов и приложений.

Когда мы говорим об IoC в Laravel, то следует знать, что он состоит стоит на трех китах:

  1. Внедрение зависимостей (Dependency Injection)
  2. Сервис-контейнер (ServiceContainer)
  3. Отражения (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);
    }
    #...
}
```php
Это круто и классно работает. Ровно до тех пор, пока вам нравится ваш мэйлер; Допустим, что этот мэйлер, организовывал переписку между пользователями посредством электронной почты. И вам внезапно захотелось вести переписку между пользователями локально - в вашем приложении. Тогда, вы заменяете класс `App\Mailers\Mailer` на `App\Mailers\LocalMailer`, но тут есть два "но".
Во-первых, нет никаких гарантий, что новый LocalMailer имеет те же публичные методы для работы.
Во-вторых, если этот старый мэйлер используется в куче различных контроллеров, то вам придется руками заменить его везде, где он создается. Вот здесь к нам на помощь и приходят итерфейсы и внедрение зависимостей;

Для гарантии того, что оба мэйлера имеют одинаковые публичные методы, нужно определить интерфейс которому они должны удовлетворять.
```php
<?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 решает две основных задачи:

  1. Он может хранить информацию о том, как получить данные;
  2. Он может хранить данные;

хотя я здесь говрю "данные" ( и это действительно так ), на практике, в контейнере чаще всего хранится объект или нформация о том, как его получить.

Зарзберем каждый пункт подробнее.

###Хранение информации о получении объекта

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

####Связывание

$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

И так, мы имеем:

  1. Внедрение зависимостей. Это когда мы не думаем о том с чем мыработаем, а думаем лишь о том как с этим можно работать. А что именно это будет - определяется вне контекста исполнения.
  2. Сервис контейнер. Он обеспечивает доступ к объектам, или информации о том, как их правильно создавать.
  3. Рефлексии. Они позволяют нам получить информацию о том что хочет получить объект или класс.

###Как это работает

Каждый раз, когда мы вызываем что-то контейнера

$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. Ну и не стесняйтесь исследовать сорцы - там самое интересное.

Всем спасибо! шумите в чатике, поглядывайте в ленту в группе

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment