📜 ⬆️ ⬇️

Laravel: Dependency Injection in practice

In my two previous articles, I talked about the Dependency Injection and IoC container , and how they work specifically in Laravel. This post will be devoted to the practical application of DI and IoC on a real example. And also, what advantages do these two excellent tools and patterns in the application give us?



Introduction


')
Our task is to embed the ability to send SMS. We could write a class to work with a specific provider (gate) or take an already written class by the provider. But we are told that in the future a change of SMS provider is possible. It does not matter, the first thought is to write a component in which in a few hours we can then change the implementation of sending SMS. And now let's forget this thought and realize it more beautifully, without becoming attached to providers and with the ability to quickly switch from one provider to another.

To better understand this concept, I recommend considering the SMS provider as a driver for sending SMS. Switching should be as painless as turning off your old monitor and plugging in a new one or changing a keyboard. Consider this component of the system as a physical device. And in general, your application is a certain computer (device component) to which various components are connected, as in the Lego constructors. As it seems to me, considering your application in this way, you will be able to approach the design of architecture most effectively.

Implementation



I will place all classes for SMS in the folder `app \ Acme \ Sms` and register it under PSR-0 in composer.json:

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


First we need to describe the interface that will be used by all SMS drivers and list the methods we need.

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


We will need only 1 method `send`, which will send SMS. The `SmsRecipient` class stores data on the recipient:

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


Install the class to work with the SmsOnline provider in the composer:

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


Now we need to write the driver of this provider and implement the interface that we described above:

 <?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); } } 


But the DI class `SmsOnline \ Api` is not so easy to do so because The constructor of the class `SmsOnline \ Api` takes an array with a configuration. Create a configuration file for our SMS component (`app / config / sms.php`), and at the same time put the default driver` SmsOnlineGate`:

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


Now it's up to IoC. Create the file `app / bindings.php`, where we will configure IoC:

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


We get the default SMS driver and tell IoC that when the application wants to `SmsGateInterface` give it the` SmsOnlineGate`. By the way, if you are already PHP to version 5.5, then I recommend rewriting the code as follows:

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); 


This is convenient because when refactoring, we can easily change the names of classes, and the IDE, in turn, will replace these lines inclusively.
Next, we need to set the configuration for `SmsOnline \ Api` by adding 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); }); 


Now when the application requires an object of the class `SmsOnline \ Api`, it will receive a configured instance.

Using this design in your application, you can easily switch between providers - you only need to write a driver for it and change the configuration, such as during development, we do not want to send SMS through the provider, so we can write somewhere in the database or even in file. To do this, we will write the `DatabaseSmsGate` and` FileSmsGate` drivers on the same principle. It's time to go to the most "delicious part" - cover code tests.

Testing



Actually, this is the main plus in DI: convenient testing in complete isolation. Instead of sucking real objects with working methods - in tests you create Mock objects with methods of stubs and check that the method was called n times with expected arguments and in a certain order. Let's take a look at how to test our code written above.

First I need to install phpunit and mockery . I put the same through the composer:

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


During the testing of each class, I want my tests to be performed in complete isolation. For example, when you test the `SmsOnlineGate` class, the` send` method calls the `send` method from` SmsOnlineApi`, but it should not be called physically. That is, you only check that the `send` method from` SmsOnlineApi` was called, but not physically. For this we will use mock objects. Consider how our test will look like:

 <?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); } } 


The test is that we check that the send method in `SmsOnline \ Api` was actually called once with the required parameters. Actually, it was not called, instead a method was called from our Mock object, and Mockery helped us with this.
We need another test to make sure that when the application wants to get `SmsGateInterface`, IoC returns us to` SmsOnlineGate`, because it is registered in our config by default:

 <?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); } } 





That's all I wanted to say. Here I did not consider the injection of objects in IoC through ServiceProvider , which is a more correct solution.
I hope that I have described my vision in some detail in favor of DI and IoC using the example of the SMS sending component. Remember that development should be fun, and you should feel yourself as an artist who paints a mechanism with a beautiful internal device. If you still have questions - ask in the comments, I will answer them with pleasure.




List of useful literature:

Source: https://habr.com/ru/post/206442/


All Articles