Laravel 依赖注入:啥时候需要?啥时候可以模拟 Facades?两种方法的优点?

Posted

技术标签:

【中文标题】Laravel 依赖注入:啥时候需要?啥时候可以模拟 Facades?两种方法的优点?【英文标题】:Laravel Dependency Injection: When do you have to? When can you mock Facades? Advantages of either method?Laravel 依赖注入:什么时候需要?什么时候可以模拟 Facades?两种方法的优点? 【发布时间】:2014-03-28 17:24:25 【问题描述】:

我使用 Laravel 已经有一段时间了,我阅读了很多关于依赖注入的可测试代码。在谈论 Facades 和 Mocked Objects 时,我遇到了一些困惑。我看到两种模式:

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


我的单元测试如下所示:

<?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 类一起使用,但它不起作用,所以我必须使用 Facade Mocks。对此问题的任何钙化将不胜感激。

【问题讨论】:

【参考方案1】:

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

依赖注入允许依赖倒置,但它们不是一回事。根据依赖倒置原则,高级和低级代码都应该依赖于抽象。在您的情况下,高级代码是您的控制器,低级代码是从 mysql 获取数据的 Eloquent ORM,但正如您所见,它们都不依赖于抽象。

因此,您无法在不影响控制器的情况下更改数据访问层。例如,您将如何从 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 ) );
    


通过这种方式,您的控制器与数据层分离。它对扩展开放,但对修改关闭。您可以通过创建 PostRepositoriesInterface 的新具体实现(例如 MongoPostRepository)来切换到 MongoDB 或文件系统,并仅更改绑定(请注意,我在这里不使用任何命名空间):

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


【讨论】:

非常感谢您提供如此详细的回答。因此,如果我跟随,如果我想使用 laravel 提供的 Auth 类,我将不得不编写一个 AuthManagerInterface 和一个 LaravelAuthManager 来使用 Auth 类。另外,如果我正在编写一个较小的应用程序,并且我确信我只会将 Eloquent 用于 DB,那么使用 Post::shouldReceive 有什么问题吗? 示例中的方法是指数据库层而不是auth类。您可以使用 laravel 提供的 auth 类。现在关于你的应用程序,当你声明它是一个小应用程序时,你应该在你的问题中选择选项 1,你在控制器中注入 eloquent 作为依赖项。 好吧,如果我可以使用提供的 Auth 类,并且我所有的 eloquent 模型都从 Eloquent 扩展,这意味着它们实现了 laravel 的外观接口,我不能只使用 Post::shouldReceive 吗? 为了清楚起见,我在上面的答案中添加了代码块 感谢您补充,您仍然没有告诉我使用Post::shouldReceive 和通过将 Post 注入控制器来使用 DI 模型之间的区别。

以上是关于Laravel 依赖注入:啥时候需要?啥时候可以模拟 Facades?两种方法的优点?的主要内容,如果未能解决你的问题,请参考以下文章

spring的注入和直接new一个对象有啥不同?

laravel依赖注入浅析

php+laravel依赖注入浅析

接口和抽象之间有啥区别以及依赖注入如何[重复]

我啥时候需要在 Gradle 依赖项中使用 Kapt?

依赖注入究竟有啥好处?