- PVSM.RU - https://www.pvsm.ru -
В своих двух предыдущих статьях я рассказал о Dependency Injection [1] и IoC контейнере [2], и о том, как они работают конкретно в Laravel. Данный пост будет посвящен практическому применению DI и IoC на реальном примере. А так же, какие все таки преимущества нам дают эти два прекрасных инструмента и паттерна в приложении.
Перед нами стоит задача встроить возможность отправки SMS. Мы могли бы написать класс для работы с конкретным провайдером (gate) или взять уже написанный класс самим провайдером. Но нам говорят, что в будущем возможна смена смс провайдера. Не беда, первая мысль — написать компонент, в котором за несколько часов мы потом сможем сменить реализацию отправки SMS. А теперь давайте забудем эту мысль и реализуем это более красиво, не привязываясь к провайдерам и с возможностью быстро переключаться с одного провайдера на другой.
Чтобы лучше понимать данную концепцию, я рекомендую рассматривать SMS провайдера как драйвер для отправки SMS. Переключение должно происходить так же безболезненно, как отключить ваш старый монитор и подключить новый или поменять клавиатуру. Рассматривайте этот компонент системы как физическое устройство. Да и вообще, ваше приложение — это некий компьютер (устройство компонент), к которому подсоединяются различные компоненты, как в конструкторах Lego. Как мне кажется, рассматривая свое приложение таким образом, у вас получится наиболее эффективно подойти к дизайну архитектуры.
Все классы для SMS я буду помещать в папке `appAcmeSms` и зарегистрирую под PSR-0 [3] в composer.json:
"psr-0": {
"Acme": "app"
}
Для начала нам нужно описать интерфейс, который будут использовать все смс драйвера и перечислить методы, которые нам нужны.
<?php namespace AcmeSms;
interface SmsGateInterface
{
/**
* @param SmsRecipient $recipient
* @param string $text
*/
public function send(SmsRecipient $recipient, $text);
}
Нам потребуется пока только 1 метод `send`, который будет отправлять SMS. Класс `SmsRecipient` хранит в себе данные по получателю:
<?php namespace AcmeSms;
class SmsRecipient
{
public $phone;
}
Установим класс для работы с провайдером SmsOnline в composer:
"require": {
"laravel/framework": "4.0.*",
"kkamkou/sms-online-api": "dev-master"
}
Теперь нам нужно написать драйвер этого провайдера и реализовать интерфейс, который мы описали выше:
<?php namespace AcmeSms;
use SmsOnlineApi 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 класса `SmsOnlineApi` провести так просто у нас не получится, т.к. конструктор класса `SmsOnlineApi` принимает массив с конфигурацией. Создадим конфигурационный файл для нашего SMS компонента (`app/config/sms.php`), и заодно поставим драйвер по-умолчанию `SmsOnlineGate`:
<?php
return [
'default' => 'AcmeSmsSmsOnlineGate',
'drivers' => [
'AcmeSmsSmsOnlineGate' => [
'user' => '',
'secret_key' => '',
],
],
];
Теперь дело за IoC. Создадим файл `app/bindings.php`, где мы будем настраивать IoC:
<?php
$smsConfig = Config::get('sms');
$smsGate = $smsConfig['default'];
App::bind('AcmeSmsSmsGateInterface', $smsGate);
Мы получаем драйвер для SMS по-умолчанию и говорим IoC, что когда приложение хочет `SmsGateInterface` отдай ему `SmsOnlineGate`. Кстати, если вы уже PHP до версии 5.5, то рекомендую код переписать следующим образом:
app/config/sms.php
<?php
use AcmeSmsSmsOnlineGate;
return [
'default' => SmsOnlineGate::class,
'drivers' => [
SmsOnlineGate::class => [
'user' => '',
'secret_key' => '',
],
],
];
app/bindings.php
<?php
use AcmeSmsSmsGateInterface;
$smsConfig = Config::get('sms');
$smsGate = $smsConfig['default'];
App::bind(SmsGateInterface::class, $smsGate);
Это удобно тем, что при рефакторинге мы сможем легко менять названия классов, а IDE, в свою очередь, заменит эти строки включительно.
Далее нам нужно прописать конфигурацию для `SmsOnlineApi`, дополнив app/bindings.php
<?php
use AcmeSmsSmsGateInterface;
use AcmeSmsSmsOnlineGate;
// указываем текущий драйвер
$smsConfig = Config::get('sms');
$smsGate = $smsConfig['default'];
App::bind(SmsGateInterface::class, $smsGate);
// настраиваем класс "SmsOnline"
App::bind(SmsOnlineApi::class, function ($app) {
$gateConfig = Config::get('sms');
$gateConfig = $gateConfig['drivers'][SmsOnlineGate::class];
return new SmsOnlineApi($gateConfig);
});
Теперь когда приложение потребует объект класса `SmsOnlineApi` — оно получит сконфигурированный экземпляр.
Используя данный дизайн в вашем приложении вы сможете легко переключаться между провайдерами — вам будет достаточно написать драйвер для него и поменять конфигурацию, как например, во время разработки мы не хотим отправлять SMS через провайдера, поэтому мы можем записывать куда-нибудь в БД или даже в файл. Для этого мы напишем драйверы `DatabaseSmsGate` и `FileSmsGate` по тому же принципу. Самое время перейти к самой «вкусной части» — покрытие кода тестами.
Собственно, это самый главный плюс в DI: удобное тестирование в полнейшей изоляции. Вместо того, чтобы прососывать настоящие объекты с рабочими методами — в тестах вы создаете Mock объекты с методами заглушек и проверяете, то что метод был вызван n раз с ожидаемыми аргументами и в определенном порядке. Давайте рассмотрим как протестировать наш код, написанный выше.
Для начала мне нужно установить phpunit [4] и mockery [5]. Ставлю так же через composer:
"require-dev": {
"phpunit/phpunit": "3.8.*@dev",
"mockery/mockery": "dev-master"
}
Во время тестирования каждого класса я хочу чтобы мои тесты выполнялись в полной изоляции. Например, когда вы тестируете класс `SmsOnlineGate`, в его методе `send` вызывается метод `send` из `SmsOnlineApi`, но он не должен вызываться физически. То есть вы проверяете только то, что метод `send` из `SmsOnlineApi` был вызван, но никак не физически. Для этого мы будем использовать Mock объекты. Рассмотрим как будет выглядеть наш тест:
<?php
use AcmeSmsSmsOnlineGate;
use AcmeSmsSmsRecipient;
class SmsOnlineGateTest extends TestCase {
/**
* @var SmsOnlineApi
*/
private $api;
/**
* @var SmsOnlineGate
*/
private $gate;
/**
* @var SmsRecipient
*/
private $recipient;
public function setUp() {
parent::setUp();
$this->api = Mockery::mock(SmsOnlineApi::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 в `SmsOnlineApi` был действительно вызван один раз с требуемыми параметрами. На самом деле он не был вызван, вместо этого был вызван метод из нашего Mock объекта, и в этом нам помог Mockery.
Нам нужен еще один тест, чтобы убедиться, что когда приложение хочет получить `SmsGateInterface`, IoC возвращает нам `SmsOnlineGate`, т.к. он прописан у нас в конфиге по умолчанию:
<?php
use AcmeSmsSmsGateInterface;
use AcmeSmsSmsOnlineGate;
class SmsGateTest extends TestCase
{
public function test_instance()
{
$instance = App::make(SmsGateInterface::class);
$this->assertInstanceOf(SmsOnlineGate::class, $instance);
}
}
На этом все, что я хотел рассказать. Здесь я не рассматривал инъекцию объектов в IoC через ServiceProvider [6], что является более правильным решением.
Я надеюсь, что я достаточно подробно расписал мое видение в пользе DI и IoC на примере компонента отправки смс. Помните, что разработка должна приносить удовольствие, а вы себя должны чувствовать художником, который рисует механизм c прекрасным внутренним устройством. Если у вас все еще остались вопросы — спрашивайте в комментариях, я с удовольствием на них отвечу.
Список полезной литературы:
Автор: misterio
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/50977
Ссылки в тексте:
[1] Dependency Injection: http://vladimirsblog.com/laravel-dependency-injection-for-beginners/
[2] IoC контейнере: http://vladimirsblog.com/laravel-inversion-of-control/
[3] PSR-0: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
[4] phpunit: https://github.com/sebastianbergmann/phpunit/
[5] mockery: https://github.com/padraic/mockery
[6] ServiceProvider: http://laravel.com/docs/ioc#service-providers
[7] What is Dependency Injection?: http://fabien.potencier.org/article/11/what-is-dependency-injection
[8] Документация по Laravel IoC контейнеру: http://laravel.com/docs/ioc
[9] Mockery: A Better Way: http://net.tutsplus.com/tutorials/php/mockery-a-better-way/
[10] Laravel Testing Decoded: https://leanpub.com/laravel-testing-decoded
[11] Laravel: From Apprentice To Artisan: https://leanpub.com/laravel
[12] Laracasts: https://laracasts.com
[13] Источник: http://habrahabr.ru/post/206442/
Нажмите здесь для печати.