Инъекция зависимостей Laravel: когда вам нужно? Когда вы можете издеваться над фасадами? Преимущества любого метода?

Я использую Laravel какое-то время, и я много читал об Injection Dependency – тестируемый код. Я пришел в замешательство, когда говорил о Фасадах и посмеянных предметах. Я вижу два шаблона:

class Post extends Eloquent { protected $guarded = array(); public static $rules = array(); } 

Это моя модель публикации. Я мог бы запустить Post::all(); чтобы получить все сообщения из моего блога. Теперь я хочу включить его в свой контроллер.


Вариант №1: Инъекция зависимостей

Моим первым инстинктом было бы ввести модель Post в качестве dependecy:

 class HomeController extends BaseController { public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->posts->all(); return View::make( 'posts' , compact( $posts ); } } 

Мой модульный тест будет выглядеть так:

 <?php use \Mockery; class HomeControllerTest extends TestCase { public function tearDown() { Mockery::close(); parent::tearDown(); } public function testIndex() { $post_collection = new StdClass(); $post = Mockery::mock('Eloquent', 'Post') ->shouldRecieve('all') ->once() ->andReturn($post_collection); $this->app->instance('Post',$post); $this->client->request('GET', 'posts'); $this->assertViewHas('posts'); } } 

Вариант № 2: Фасадные изгибы

 class HomeController extends BaseController { public function index() { $posts = Post::all(); return View::make( 'posts' , compact( $posts ); } } 

Мой модульный тест будет выглядеть так:

 <?php use \Mockery; class HomeControllerTest extends TestCase { public function testIndex() { $post_collection = new StdClass(); Post::shouldRecieve('all') ->once() ->andReturn($post_collection); $this->client->request('GET', 'posts'); $this->assertViewHas('posts'); } } 

Я понимаю оба метода, но я не понимаю, почему я должен или когда должен использовать один метод над другим. Например, я попытался использовать маршрут DI с классом Auth но он не работает, поэтому я должен использовать Facades Mocks. Любая оценка по этому вопросу будет принята с благодарностью.

Несмотря на то, что вы используете инъекцию зависимостей на Option # 1, ваш контроллер все еще соединен с Eloquent ORM. (Обратите внимание, что я не использую термин «Модель» здесь, потому что в MVC модель – это не просто класс или объект, а слой. Это ваша бизнес-логика.).

Инъекция зависимостей допускает инверсию зависимостей, но это не одно и то же. Согласно принципу инверсии зависимостей, код высокого и низкого уровня должен зависеть от абстракций. В вашем случае код высокого уровня является вашим контроллером, а низкоуровневый код – это Eloquent ORM, который извлекает данные из MySQL, но, как вы можете видеть, ни один из них не зависит от абстракций.

Как следствие, вы не можете изменить уровень доступа к данным, не влияя на ваш контроллер. Как бы вы перешли, например, из MySQL в MongoDB или в файловую систему? Для этого вам нужно использовать репозитории (или все, что вы хотите назвать).

Поэтому создайте интерфейс репозиториев, который должен реализовать все ваши конкретные реализации репозитория (MySQL, MongoDB, Файловая система и т. Д.).

 interface PostRepositoriesInterface { public function getAll(); } 

а затем создайте свою конкретную реализацию, например, для MySQL

 class DbPostRepository implements PostRepositoriesInterface { public function getAll() { return Post::all()->toArray(); /* Why toArray()? This is the L (Liskov Substitution) in SOLID. Any implementation of an abstraction (interface) should be substitutable in any place that the abstraction is accepted. But if you just return Post:all() how would you handle the situation where another concrete implementation would return another data type? Probably you would use an if statement in the controller to determine the data type but that's far from ideal. In PHP you cannot force the return data type so this is something that you have to keep in mind.*/ } } 

Теперь ваш контроллер должен ввести подсказку интерфейса, а не конкретную реализацию. Вот что такое «Код на интерфейсе, а не на реализацию». Это инверсия зависимостей.

 class HomeController extends BaseController { public function __construct(PostRepositoriesInterface $repo) { $this->repo= $repo; } public function index() { $posts = $this->repo->getAll(); return View::make( 'posts' , compact( $posts ) ); } } 

Таким образом, ваш контроллер отключается от вашего уровня данных. Он открыт для расширения, но закрыт для модификации. Вы можете переключиться на MongoDB или в файловую систему, создав новую конкретную реализацию PostRepositoriesInterface (например, MongoPostRepository) и изменив только привязку (обратите внимание, что я не использую здесь пространства имен):

 App:bind('PostRepositoriesInterface','DbPostRepository'); 

в

 App:bind('PostRepositoriesInterface','MongoPostRepository'); 

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

Это также облегчает тестирование. Теперь вы можете протестировать свой контроллер без фактического попадания в базу данных. Обратите внимание, что тест контроллера должен проверяться только в том случае, если контроллер функционирует правильно, что означает, что контроллер вызывает правильный метод, получает результаты и передает его в представление. На этом этапе вы не проверяете достоверность результатов. Это не ответственность диспетчера.

 public function testIndexActionBindsPostsFromRepository() { $repository = Mockery::mock('PostRepositoriesInterface'); $repository->shouldReceive('all')->once()->andReturn(array('foo')); App::instance('PostRepositoriesInterface', $repository); $response = $this->action('GET', 'HomeController@index'); $this->assertResponseOk(); $this->assertViewHas('posts', array('foo')); } 

РЕДАКТИРОВАТЬ

Если вы решите пойти с опцией №1, вы можете протестировать ее так

 class HomeControllerTest extends TestCase { public function __construct() { $this->mock = Mockery::mock('Eloquent', 'Post'); } public function tearDown() { Mockery::close(); } public function testIndex() { $this->mock ->shouldReceive('all') ->once() ->andReturn('foo'); $this->app->instance('Post', $this->mock); $this->call('GET', 'posts'); $this->assertViewHas('posts'); } }