Laravel: Dependency Injection на практике

    В своих двух предыдущих статьях я рассказал о Dependency Injection и IoC контейнере, и о том, как они работают конкретно в Laravel. Данный пост будет посвящен практическому применению DI и IoC на реальном примере. А так же, какие все таки преимущества нам дают эти два прекрасных инструмента и паттерна в приложении.



    Введение



    Перед нами стоит задача встроить возможность отправки SMS. Мы могли бы написать класс для работы с конкретным провайдером (gate) или взять уже написанный класс самим провайдером. Но нам говорят, что в будущем возможна смена смс провайдера. Не беда, первая мысль — написать компонент, в котором за несколько часов мы потом сможем сменить реализацию отправки SMS. А теперь давайте забудем эту мысль и реализуем это более красиво, не привязываясь к провайдерам и с возможностью быстро переключаться с одного провайдера на другой.

    Чтобы лучше понимать данную концепцию, я рекомендую рассматривать SMS провайдера как драйвер для отправки SMS. Переключение должно происходить так же безболезненно, как отключить ваш старый монитор и подключить новый или поменять клавиатуру. Рассматривайте этот компонент системы как физическое устройство. Да и вообще, ваше приложение — это некий компьютер (устройство компонент), к которому подсоединяются различные компоненты, как в конструкторах Lego. Как мне кажется, рассматривая свое приложение таким образом, у вас получится наиболее эффективно подойти к дизайну архитектуры.

    Реализация



    Все классы для SMS я буду помещать в папке `app\Acme\Sms` и зарегистрирую под PSR-0 в composer.json:

    "psr-0": {
        "Acme": "app"
    }
    


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

    
    <?php namespace Acme\Sms;
    
    interface SmsGateInterface
    {
    
        /**
         * @param SmsRecipient $recipient
         * @param  string      $text
         */
        public function send(SmsRecipient $recipient, $text);
    
    }
    


    Нам потребуется пока только 1 метод `send`, который будет отправлять SMS. Класс `SmsRecipient` хранит в себе данные по получателю:

    
    <?php namespace Acme\Sms;
    
    class SmsRecipient
    {    
        public $phone;    
    }
    


    Установим класс для работы с провайдером SmsOnline в composer:

    "require": {
        "laravel/framework": "4.0.*",
        "kkamkou/sms-online-api": "dev-master"
    }
    


    Теперь нам нужно написать драйвер этого провайдера и реализовать интерфейс, который мы описали выше:

    
    <?php namespace Acme\Sms;
    
    use SmsOnline\Api as SmsOnlineApi;
    
    class SmsOnlineGate implements SmsGateInterface
    {
    
        private $api;
    
        public function __construct(SmsOnlineApi $api)
        {
            $this->api = $api;
        }
    
        /**
         * @param SmsRecipient $recipient
         * @param  string      $text
         */
        public function send(SmsRecipient $recipient, $text)
        {
            $this->api->send($recipient->phone, $text);
        }
    }
    


    Но DI класса `SmsOnline\Api` провести так просто у нас не получится, т.к. конструктор класса `SmsOnline\Api` принимает массив с конфигурацией. Создадим конфигурационный файл для нашего SMS компонента (`app/config/sms.php`), и заодно поставим драйвер по-умолчанию `SmsOnlineGate`:

    
    <?php
    return [
    
        'default' => 'Acme\Sms\SmsOnlineGate',
    
        'drivers' => [
    
            'Acme\Sms\SmsOnlineGate' => [
    
                'user'       => '',
                'secret_key' => '',
    
            ],
    
        ],
    
    ];
    


    Теперь дело за IoC. Создадим файл `app/bindings.php`, где мы будем настраивать IoC:

    
    <?php
    $smsConfig = Config::get('sms');
    
    $smsGate = $smsConfig['default'];
    
    App::bind('Acme\Sms\SmsGateInterface', $smsGate);    
    


    Мы получаем драйвер для SMS по-умолчанию и говорим IoC, что когда приложение хочет `SmsGateInterface` отдай ему `SmsOnlineGate`. Кстати, если вы уже PHP до версии 5.5, то рекомендую код переписать следующим образом:

    app/config/sms.php

    
    <?php
    use Acme\Sms\SmsOnlineGate;
    
    return [
    
        'default' => SmsOnlineGate::class,
    
        'drivers' => [
    
            SmsOnlineGate::class => [
    
                'user'       => '',
                'secret_key' => '',
    
            ],
    
        ],
    
    ];
    


    app/bindings.php

    
    <?php
    use Acme\Sms\SmsGateInterface;
    
    $smsConfig = Config::get('sms');
    
    $smsGate = $smsConfig['default'];
    
    App::bind(SmsGateInterface::class, $smsGate);
    


    Это удобно тем, что при рефакторинге мы сможем легко менять названия классов, а IDE, в свою очередь, заменит эти строки включительно.
    Далее нам нужно прописать конфигурацию для `SmsOnline\Api`, дополнив app/bindings.php

    
    <?php
    use Acme\Sms\SmsGateInterface;
    use Acme\Sms\SmsOnlineGate;
    
    // указываем текущий драйвер
    $smsConfig = Config::get('sms');
    
    $smsGate = $smsConfig['default'];
    
    App::bind(SmsGateInterface::class, $smsGate);
    
    // настраиваем класс "SmsOnline"
    App::bind(SmsOnline\Api::class, function ($app) {
        $gateConfig = Config::get('sms');
        $gateConfig = $gateConfig['drivers'][SmsOnlineGate::class];
    
        return new SmsOnline\Api($gateConfig);
    });
    


    Теперь когда приложение потребует объект класса `SmsOnline\Api` — оно получит сконфигурированный экземпляр.

    Используя данный дизайн в вашем приложении вы сможете легко переключаться между провайдерами — вам будет достаточно написать драйвер для него и поменять конфигурацию, как например, во время разработки мы не хотим отправлять SMS через провайдера, поэтому мы можем записывать куда-нибудь в БД или даже в файл. Для этого мы напишем драйверы `DatabaseSmsGate` и `FileSmsGate` по тому же принципу. Самое время перейти к самой «вкусной части» — покрытие кода тестами.

    Тестирование



    Собственно, это самый главный плюс в DI: удобное тестирование в полнейшей изоляции. Вместо того, чтобы прососывать настоящие объекты с рабочими методами — в тестах вы создаете Mock объекты с методами заглушек и проверяете, то что метод был вызван n раз с ожидаемыми аргументами и в определенном порядке. Давайте рассмотрим как протестировать наш код, написанный выше.

    Для начала мне нужно установить phpunit и mockery. Ставлю так же через composer:

    "require-dev": {
            "phpunit/phpunit": "3.8.*@dev",
            "mockery/mockery": "dev-master"
    }
    


    Во время тестирования каждого класса я хочу чтобы мои тесты выполнялись в полной изоляции. Например, когда вы тестируете класс `SmsOnlineGate`, в его методе `send` вызывается метод `send` из `SmsOnlineApi`, но он не должен вызываться физически. То есть вы проверяете только то, что метод `send` из `SmsOnlineApi` был вызван, но никак не физически. Для этого мы будем использовать Mock объекты. Рассмотрим как будет выглядеть наш тест:

    
    <?php
    
    use Acme\Sms\SmsOnlineGate;
    use Acme\Sms\SmsRecipient;
    
    class SmsOnlineGateTest extends TestCase {
    
        /**
         * @var SmsOnline\Api
         */
        private $api;
    
        /**
         * @var SmsOnlineGate
         */
        private $gate;
    
        /**
         * @var SmsRecipient
         */
        private $recipient;
    
        public function setUp() {
            parent::setUp();
    
            $this->api = Mockery::mock(SmsOnline\Api::class)->makePartial();
            $this->recipient = Mockery::mock(SmsRecipient::class);
    
            $this->gate = new SmsOnlineGate($this->api);
        }
    
        public function test_send()
        {
            $text = 'текст смс';
    
            $this->api->shouldReceive('send')
                ->withArgs([$this->recipient->phone, $text])
                ->once();
    
            $this->gate->send($this->recipient, $text);
        }
    
    }
    


    Тест заключается в том, что мы проверяем, что метод send в `SmsOnline\Api` был действительно вызван один раз с требуемыми параметрами. На самом деле он не был вызван, вместо этого был вызван метод из нашего Mock объекта, и в этом нам помог Mockery.
    Нам нужен еще один тест, чтобы убедиться, что когда приложение хочет получить `SmsGateInterface`, IoC возвращает нам `SmsOnlineGate`, т.к. он прописан у нас в конфиге по умолчанию:

    
    <?php
    use Acme\Sms\SmsGateInterface;
    use Acme\Sms\SmsOnlineGate;
    
    class SmsGateTest extends TestCase
    {
    
        public function test_instance()
        {
            $instance = App::make(SmsGateInterface::class);
    
            $this->assertInstanceOf(SmsOnlineGate::class, $instance);
        }
    
    } 
    





    На этом все, что я хотел рассказать. Здесь я не рассматривал инъекцию объектов в IoC через ServiceProvider, что является более правильным решением.
    Я надеюсь, что я достаточно подробно расписал мое видение в пользе DI и IoC на примере компонента отправки смс. Помните, что разработка должна приносить удовольствие, а вы себя должны чувствовать художником, который рисует механизм c прекрасным внутренним устройством. Если у вас все еще остались вопросы — спрашивайте в комментариях, я с удовольствием на них отвечу.




    Список полезной литературы:

    • +15
    • 15,5k
    • 4
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 4
    • +3
      Наконец-то что-то полезное о Laravel, а не статья типа «смотрите, как просто сделать блог, омайгад, тут есть ORM!»
      Спасибо.
      • 0
        Если сделать SmsRecipient интерфейсом, то его можно имплементировать пользователям, компаниям, администраторам системы, и красиво передавать сами модели в шлюз.
        • 0
          Согласен. Если стоит такая задача — то интерфейсить самое то
          • +1
            В введении написано «А теперь давайте забудем эту мысль и реализуем это более красиво».
            Думаю, с интерфейсом будет совсем красиво.

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