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

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

class Post extends Eloquent {

    protected $guarded = array();

    public static $rules = array();

}

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


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

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

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

}

Мой unit test будет выглядеть следующим образом:

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

}

Мой unit test будет выглядеть следующим образом:

<?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, но он не работает, поэтому я должен использовать Facocks Mocks. Любая оценка по этому вопросу будет принята с благодарностью.

Ответы

Ответ 1

Несмотря на то, что вы используете инъекцию зависимостей на 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 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', '[email protected]'); 

    $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');
  }

}