Laravel依赖注射:你什么时候需要?你什么时候可以模拟门面?两种方法的优点? [英] Laravel Dependency Injection: When do you have to? When can you mock Facades? Advantages of either method?

查看:451
本文介绍了Laravel依赖注射:你什么时候需要?你什么时候可以模拟门面?两种方法的优点?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在使用Laravel一段时间,我一直在阅读很多关于Dependency Injection的可测试代码。在谈论立面和模仿物体时,我已经到了一个混乱的地步。我看到两种模式:

  class Post Enloquent {

protected $ guarded = array();

public static $ rules = array();

}

这是我的帖子模型。我可以运行 Post :: all(); 来获取我博客中的所有帖子。现在我想把它并入我的控制器。




选项#1:依赖注入



我的第一本能就是注入 Post model as a 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 

使用\Mockery;

class HomeControllerTest扩展TestCase {

public function tearDown()
{
Mockery :: close();

parent :: tearDown();
}
public function testIndex()
{
$ post_collection = new StdClass();

$ post = Mockery :: mock('Eloquent','Post')
- > shouldRecieve('all')
- > once()
- >和返回($ post_collection);

$ this-> app->实例('Post',$ post);

$ this->客户端>请求('GET','posts');

$ this-> assertViewHas('posts');
}
}



选项#2:Facade Mocks



 类HomeController扩展BaseController {


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

}

我的单元测试如下所示:

 <?php 

使用\Mockery;

class HomeControllerTest扩展TestCase {


public function testIndex()
{
$ post_collection = new StdClass();

发布:: shouldRecieve('all')
- >一次()
- > andReturn($ post_collection);

$ this-> client->请求('GET','posts');

$ this-> assertViewHas('posts');
}
}

我了解这两种方法,但我不明白为什么我应该当我应该使用另一种方法时,例如,我尝试使用DI路由与 Auth 类,但它不工作,所以我必须使用在这个问题上的任何钙化将是非常大的

解决方案

尽管在Option#1上使用依赖注入,您的控制器仍然与Enloquent ORM配合使用。 (注意,我避免使用这里的术语模型,因为在MVC中,模型不仅仅是一个类或一个对象,而是一个层,这是你的业务逻辑。)。



依赖注入允许依赖性反转,但它们不一样。根据依赖性反转原则,高级和低级代码都应该取决于抽象。在您的情况下,高级代码是您的控制器,低级代码是从MySQL获取数据的雄辩ORM,但您可以看到它们都不依赖于抽象。



因此,您无法在不影响控制器的情况下更改数据访问层。你将如何改变例如从MySQL到MongoDB或文件系统?为此,您必须使用存储库(或任何您想要的名称)。



因此,创建一个存储库界面,您的所有具体存储库实现(MySQL,MongoDB,文件系统等)都应实现。

 界面PostRepositoriesInterface {

public function getAll();
}

然后创建您的具体实现,例如对于MySQL

  class DbPostRepository实现PostRepositoriesInterface {

public function getAll()
{

return Post :: all() - > toArray();

/ *为什么要toArray()?这是SOLID中的L(Liskov替代)。
任何抽象(接口)的实现都应该在可以接受抽象的任何地方替代
。但是如果你刚刚返回
Post:all()你会如何处理另一个具体的
实现返回另一种数据类型的情况?可能您会在控制器中使用if
语句来确定数据类型,但远远超过
的理想值。在PHP中,您不能强制返回数据类型,因此您必须记住
。* /
}
}

现在您的控制器必须键入提示的界面,而不是具体的实现。这是关于接口上的代码,而不是实现的代码。这是Dependency Inversion。

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

}

这样你的控制器将被解耦你的数据层。它是开放扩展,但关闭修改。您可以通过创建PostRepositoriesInterface(例如MongoPostRepository)的新具体实现,并仅更改绑定(请注意,我在此处不使用任何命名空间),可以切换到MongoDB或File System:

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

 应用程序:绑定( '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扩展TestCase {

public function __construct()
{
$ this-> mock = Mockery :: mock('Enloquent','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');
}

}


I've been using Laravel for a while now and I have been reading a lot about Dependency Injection an testable code. I've come to a point of confusion when talking about Facades and Mocked Objects. I see two patterns:

class Post extends Eloquent {

    protected $guarded = array();

    public static $rules = array();

}

This is my Post Model. I could run Post::all(); to get all the posts from my blog. Now I want to incorporate it into my controller.


Option #1: Dependency Injection

My first instinct would be to inject the Post model as a 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 );
    }

}

My unit test would look like this:

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

Option #2: Facade Mocks

class HomeController extends BaseController {


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

}

My unit test would look like this:

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

I understand both methods but I don't understand why I should or when I should use one method over the other. For example, I've tried to use the DI route with the Auth class but it doesn't work so I have to use the Facade Mocks. Any calcification on this issue would be greatly appreciated.

解决方案

Although you use dependency injection on Option #1, your controller is still coupled with the Eloquent ORM. (Note that i avoid to use the term Model here because in MVC the Model is not just a class or an object but a layer. It's your business logic.).

Dependency Injection allows for Dependency Inversion but they are not the same thing. According to the Dependency Inversion principle both high and low level code should depend on abstractions. In your case the high level code is your controller and the low level code is the Eloquent ORM that fetches data from MySQL, but as you can see none of them depends on abstractions.

As a consequence, you are not able to change your data access layer without affecting your controller. How would you go about changing for example from MySQL to MongoDB or to the File System? To do this you have to use repositories (or whatever you want to call it).

So create a repositories interface that all your concrete repository implementations (MySQL, MongoDB , File System etc.) should implement.

interface PostRepositoriesInterface {

    public function getAll();
}

and then create your concrete implementation e.g. for 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.*/
    }
}

Now your controller must type hint the interface and not the concrete implementation. This is what "Code on an interface an not on implementation" is all about. This is Dependency Inversion.

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

}

This way your controller is decoupled from your data layer. It's open for extension but closed for modification. You can switch to MongoDB or to the File System by creating a new concrete implementation of PostRepositoriesInterface (e.g. MongoPostRepository) and change only the binding from (Note that i don't use any namespaces here):

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

to

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

In an ideal situation your controller should contain only application and not business logic. If you ever find yourself wanting to call a controller from another controller its a sign that you've done something wrong. In this case your controllers contain to much logic.

This also makes testing easier. Now you are able to test your controller without actually hitting the database. Note that a controller test must test only if the controller functions properly which means that the controller calls the right method, gets the results and pass it to the view. At this point you are not testing the validity of the results. This is not controller's responsibility.

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

EDIT

If you choose to go with option #1 you can test it like this

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

}

这篇关于Laravel依赖注射:你什么时候需要?你什么时候可以模拟门面?两种方法的优点?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆