Статьи

Копаться в IoC-контейнере Laravel

Инверсия управления, или IoC, — это метод, который позволяет инвертировать управление по сравнению с классическим процедурным кодом. Самая известная форма IoC — это, конечно, Dependency Injection или DI. Контейнер IoC Laravel является одной из наиболее часто используемых функций Laravel, но, вероятно, наименее понятен.

Вот очень быстрый пример использования Dependency Injection для достижения Inversion of Control.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
 
class JeepWrangler
{
    public function __construct(Petrol $fuel)
    {
        $this->fuel = $fuel;
    }
     
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
}
 
class Petrol
{
    public function getPrice()
    {
        return 130.7;
    }
}
 
$petrol = new Petrol;
$car = new JeepWrangler($petrol);
 
$cost = $car->refuel(60);

Используя инжекцию конструктора, мы теперь делегировали создание нашего экземпляра Petrol обратно самой вызывающей стороне, тем самым достигая инверсии управления. Нашему JeepWrangler не нужно знать, откуда берется Petrol , если он его получает.

Так, что все это имеет отношение к Laravel? На самом деле довольно много. Laravel, если вы не знали, на самом деле является контейнером IoC. Контейнер — это объект, который, как вы можете ожидать, содержит вещи. Контейнер Laravel IoC используется для хранения множества различных привязок. Все, что вы делаете в Laravel, в какой-то момент будет взаимодействовать с контейнером IoC. Это взаимодействие, как правило, имеет форму решаемой привязки.

Если вы откроете какого-либо из существующих поставщиков услуг Laravel, вы, скорее всего, увидите что-то подобное в методе register (пример сильно упрощен).

1
2
3
$this->app[‘router’] = $this->app->share(function($app) {
    return new Router;
});

Это очень, очень базовая привязка. Он состоит из имени привязки ( router ) и преобразователя (замыкания). Когда эта привязка будет решена из контейнера, мы получим экземпляр Router .

Laravel обычно группирует похожие имена привязок, такие как session и session.store .

Чтобы разрешить привязку, мы можем просто вызвать метод непосредственно из него или использовать метод make для контейнера.

1
$router = $this->app->make(‘router’);

Это то, что делает контейнер в своей основной форме. Но, как и большинство вещей, Laravel, в этом есть гораздо больше, чем просто связывание и разрешение классов.

Если вы просмотрели несколько поставщиков услуг Laravel, вы заметите, что большинство привязок определены, как в предыдущем примере. Здесь это снова:

1
2
3
$this->app[‘router’] = $this->app->share(function($app) {
    return new Router;
});

Эта привязка использует метод share для контейнера. Laravel использует статическую переменную для хранения предыдущего разрешенного значения и просто повторно использует это значение при повторном разрешении привязки. Это в основном то, что делает метод share .

01
02
03
04
05
06
07
08
09
10
$this->app[‘router’] = function($app) {
    static $router;
      
    if (is_null($router)) {
        $router = new Router;
    }
      
    return $router;
      
};

Другой способ написать это — использовать метод bindShared .

1
2
3
$this->app->bindShared(‘router’, function($app) {
    return new Router;
});

Вы также можете использовать методы singleton и instance для достижения общей привязки. Итак, если они все достигают одного и того же, какая разница? Не очень много, на самом деле. Я лично предпочитаю использовать метод bindShared .

Могут быть случаи, когда вы хотите связать что-то с контейнером, но только когда это еще не было связано ранее. Есть несколько способов сделать это, но самый простой — использовать метод bindIf .

1
2
3
$this->app->bindIf(‘router’, function($app) {
    return new ImprovedRouter;
});

Это будет связано только с контейнером, если привязка router еще не существует. Единственное, о чем следует знать здесь, — это как использовать условную привязку. Для этого вам нужно предоставить третий параметр методу bindIf со значением true .

Одной из наиболее часто используемых функций контейнера IoC является его способность автоматически разрешать зависимости для классов, которые не связаны. Что это значит, точно? Во-первых, нам на самом деле не нужно связывать что-либо с контейнером для разрешения экземпляра. Мы можем просто make экземпляр практически любого класса.

01
02
03
04
05
06
07
08
09
10
class Petrol
{
    public function getPrice()
    {
        return 130.7;
    }
}
 
// In our service provider…
$petrol = $this->app->make(‘Petrol’);

Контейнер будет создавать экземпляр нашего класса Petrol для нас. Самое приятное в этом то, что это также разрешит зависимости конструктора для нас.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class JeepWrangler
{
    public function __construct(Petrol $fuel)
    {
        $this->fuel = $fuel;
    }
     
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
     
}
 
// In our service provider…
$car = $this->app->make(‘JeepWrangler’);

Первым делом контейнер проверяет зависимости класса JeepWrangler . Затем он попытается разрешить эти зависимости. Итак, поскольку наш тип JeepWrangler намекает на класс Petrol , контейнер автоматически разрешает и внедряет его как зависимость.

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

Laravel автоматически разрешает ваши зависимости — это здорово и упрощает процесс создания классов вручную. Однако бывают случаи, когда вы хотите внедрить конкретную реализацию, особенно когда вы используете интерфейсы. Этого легко достичь, используя в качестве привязки полное имя класса. Чтобы продемонстрировать это, мы будем использовать новый интерфейс под названием Fuel .

1
2
3
4
interface Fuel
{
    public function getPrice();
}

Теперь наш класс JeepWrangler может JeepWrangler интерфейс, и мы позаботимся о том, чтобы наш класс Petrol реализовывал интерфейс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class JeepWrangler
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
     
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
}
 
class Petrol implements Fuel
{
    public function getPrice()
    {
        return 130.7;
    }
}

Теперь мы можем привязать наш интерфейс Fuel к контейнеру и разрешить ему разрешить новый экземпляр Petrol .

1
2
3
4
5
6
$this->app->bind(‘Fuel’, ‘Petrol’);
 
// Or, we could instantiate it ourselves.
$this->app->bind(‘Fuel’, function ($app) {
    return new Petrol;
});

Теперь, когда мы создаем новый экземпляр нашего JeepWrangler , контейнер увидит, что он запрашивает Fuel , и он будет знать, как автоматически добавить экземпляр Petrol .

Это также позволяет легко менять реализацию, так как мы можем просто изменить привязку в контейнере. Чтобы продемонстрировать, мы могли бы начать заправлять нашу машину бензином премиум-класса, который немного дороже.

01
02
03
04
05
06
07
08
09
10
class PremiumPetrol implements Fuel
{
    public function getPrice()
    {
        return 144.3;
    }
}
 
// In our service provider…
$this->app->bind(‘Fuel’, ‘PremiumPetrol’);

Обратите внимание, что контекстные привязки доступны только в Laravel 5.

Контекстная привязка позволяет вам привязать реализацию (так же, как мы делали выше) к определенному классу.

01
02
03
04
05
06
07
08
09
10
11
12
abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
 
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
}

Затем мы создадим новый класс NissanPatrol который расширяет абстрактный класс, и мы обновим наш JeepWrangler чтобы также расширить его.

1
2
3
4
5
6
7
8
9
class JeepWrangler extends Car
{
    //
}
 
class NissanPatrol extends Car
{
    //
}

Наконец, мы создадим новый класс Diesel который реализует Fuel интерфейс.

1
2
3
4
5
6
7
class Diesel implements Fuel
{
    public function getPrice()
    {
        return 135.3;
    }
}

Теперь наш Jeep Wrangler будет заправляться бензином, а наш Nissan Patrol будет заправляться дизельным топливом. Если бы мы попытались использовать тот же метод, что и раньше, привязав реализацию к интерфейсу, то оба этих автомобиля получили бы один и тот же тип топлива, а это не то, что нам нужно.

Таким образом, чтобы убедиться, что каждый автомобиль заправляется правильным топливом, мы можем сообщить контейнеру, какую реализацию использовать в каждом контексте.

1
2
$this->app->when(‘JeepWrangler’)->needs(‘Fuel’)->give(‘Petrol’);
$this->app->when(‘NissanPatrol’)->needs(‘Fuel’)->give(‘Diesel’);

Обратите внимание, что тегирование доступно только в Laravel 5.

Возможность разрешать привязки из контейнера очень важна. Обычно мы можем разрешить что-либо, только если знаем, как это было связано с контейнером. С Laravel 5 мы теперь можем пометить наши привязки, чтобы разработчики могли легко разрешить все привязки, имеющие один и тот же тег.

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

1
2
3
4
5
6
7
$this->app->tag(‘awesome.plugin’, ‘plugin’);
 
// Or an array of tags.
 
$tags = [‘plugin’, ‘theme’];
 
$this->app->tag(‘awesome.plugin’, $tags);

Теперь, чтобы разрешить все привязки для данного тега, мы можем использовать метод tagged .

1
2
3
4
5
$plugins = $this->app->tagged(‘plugin’);
 
foreach ($plugins as $plugin) {
    $plugin->doSomethingFunky();
}

Когда вы связываете что-то с контейнером с тем же именем более одного раза, это называется перепривязкой. Laravel заметит, что вы снова связываете что-то и вызовет отскок

Самое большое преимущество здесь — это когда вы разрабатываете пакет, который позволяет другим разработчикам расширять его, связывая компоненты в контейнере. Чтобы использовать его, нам нужно внедрить сеттер-инъекцию в наш автореферат.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
abstract class Car
{
    public function __construct(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
 
    public function refuel($litres)
    {
        return $litres * $this->fuel->getPrice();
    }
     
    public function setFuel(Fuel $fuel)
    {
        $this->fuel = $fuel;
    }
     
}

Давайте предположим, что мы привязываем наш JeepWrangler к контейнеру следующим образом.

1
2
3
4
5
6
7
$this->app->bindShared(‘fuel’, function ($app) {
    return new Petrol;
});
 
$this->app->bindShared(‘car’, function ($app) {
    return new JeepWrangler($app[‘fuel’]);
});

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

1
$this->app[‘car’]->setFuel(new PremiumPetrol);

В большинстве случаев это может быть все, что нужно; однако, что, если наш пакет становится более сложным, и связывание fuel впрыскивается в несколько других классов? Это приведет к тому, что другому разработчику придется устанавливать свой новый экземпляр целую кучу раз. Итак, чтобы решить это, мы можем использовать перепривязку:

1
2
3
4
5
$this->app->bindShared(‘car’, function ($app) {
    return new JeepWrangler($app->rebinding(‘fuel’, function ($app, $fuel) {
        $app[‘car’]->setFuel($fuel);
    }));
});

Метод rebinding немедленно вернет нам уже связанный экземпляр, поэтому мы можем использовать его в конструкторе нашего JeepWrangler . Закрытие, данное методу rebinding получает два параметра, первый из которых является контейнером IoC, а второй — новым связыванием. Затем мы можем сами использовать метод setFuel чтобы внедрить новую привязку в наш экземпляр JeepWrangler .

Осталось только другому разработчику просто перезаправить fuel в контейнере. Их поставщик услуг может выглядеть так:

1
2
3
$this->app->bindShared(‘fuel’, function () {
    return new PremiumPetrol;
});

Как только привязка будет восстановлена ​​в контейнере, Laravel автоматически вызовет связанные замыкания. В нашем случае новый экземпляр PremiumPetrol будет установлен на нашем экземпляре JeepWrangler .

Если вы хотите внедрить зависимость в одну из основных привязок или в привязку, созданную пакетом, то метод extension в контейнере является одним из самых простых способов сделать это.

Этот метод разрешит привязку из контейнера и выполнит закрытие с контейнером и разрешенным экземпляром в качестве параметров. Это позволяет вам легко разрешить и внедрить ваши собственные привязки или просто создать новый класс и внедрить его.

1
2
3
$this->app->extend(‘car’, function ($app, $car) {
    $car->setFuel(new PremiumPetrol);
});

В отличие от повторного связывания, это только устанавливает зависимость от единственного связывания.

Как и многие из компонентов Illuminate, составляющих основу Laravel, Контейнер можно использовать вне Laravel в автономном приложении. Для этого сначала необходимо указать его в качестве зависимости в файле composer.json .

1
2
3
4
5
{
    «require»: {
        «illuminate/container»: «4.2.*»
   }
}

Это установит последнюю версию 4.2 контейнера. Теперь осталось только создать новый контейнер.

1
2
3
4
5
6
7
require ‘vendor/autoload.php’;
 
$app = new Illuminate\Container\Container;
 
$app->bindShared(‘car’, function () {
    return new JeepWrangler;
});

Из всех компонентов это один из самых простых в использовании везде, где вам нужен гибкий и функциональный контейнер IoC.