Laravel 依赖注入:什么时候需要?什么时候可以模拟 Facades?两种方法的优点?
我使用 Laravel 已经有一段时间了,我已经阅读了很多关于依赖注入的可测试代码.在谈论 Facades 和 Mocked Objects 时,我感到困惑.我看到两种模式:
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();
}
这是我的帖子模型.我可以运行 Post::all();
从我的博客中获取所有帖子.现在我想将它合并到我的控制器中.
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.
我的第一反应是将 Post
模型作为依赖注入:
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');
}
}
选项 #2:外观模拟
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');
}
}
我了解这两种方法,但我不明白为什么我应该或何时应该使用一种方法而不是另一种方法.例如,我尝试将 DI 路由与 Auth
类一起使用,但它不起作用,所以我必须使用 Facade Mocks.对此问题的任何钙化将不胜感激.
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.
推荐答案
虽然你在选项 #1 上使用了依赖注入,但你的控制器仍然与 Eloquent ORM 耦合.(请注意,我在这里避免使用术语模型,因为在 MVC 中,模型不仅仅是一个类或对象,而是一个层.这是您的业务逻辑.).
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.).
依赖注入允许依赖倒置,但它们不是一回事.根据依赖倒置原则,高级和低级代码都应该依赖于抽象.在您的情况下,高级代码是您的控制器,低级代码是从 MySQL 获取数据的 Eloquent ORM,但正如您所见,它们都不依赖于抽象.
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.
因此,您无法在不影响控制器的情况下更改数据访问层.例如,您将如何从 MySQL 更改为 MongoDB 或文件系统?为此,您必须使用存储库(或任何您想调用的名称).
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).
因此,创建一个所有具体的存储库实现(MySQL、MongoDB、文件系统等)都应该实现的存储库接口.
So create a repositories interface that all your concrete repository implementations (MySQL, MongoDB , File System etc.) should implement.
interface PostRepositoriesInterface {
public function getAll();
}
然后创建您的具体实现,例如MySQL
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 ) );
}
}
通过这种方式,您的控制器与数据层分离.它对扩展开放,但对修改关闭.您可以通过创建 PostRepositoriesInterface 的新具体实现(例如 MongoPostRepository)来切换到 MongoDB 或文件系统并仅更改绑定(请注意,我在这里不使用任何命名空间):
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');
到
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 too 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'));
}
编辑
如果您选择使用选项 #1,您可以像这样测试它
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');
}
}
相关文章