Laravel Dependency Injection: When do you have to? When can you mock Facades? Advantages of either method?
Asked Answered
F

1

16

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.

Frederiksen answered 26/2, 2014 at 4:45 Comment(0)
Y
36

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

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

}
Yarvis answered 26/2, 2014 at 4:45 Comment(6)
Thank you very much for this very detailed answer. So if I follow, if I wanted to use the Auth class provided by laravel, I would have to write an AuthManagerInterface and a LaravelAuthManager that would use the Auth class. Also if I'm writing a smaller application where I'm confident that I will only use Eloquent for the DB, is there any problem using Post::shouldReceive ?Frederiksen
The approach in the example refers to the database layer and not to the auth class. You can use the auth class as is provided by laravel. Now regarding your app, which as you state its a small one you should be good to go with option 1 in your question, where you inject eloquent as a dependency in the controller.Yarvis
Ok if I can use the Auth class as provided, and all my eloquent models extend from Eloquent, meaning they implement laravel's facade interface, can't I just use Post::shouldReceive?Frederiksen
I added the code block in the answer above for clarityYarvis
Thank you for adding that you still haven't told me the difference between using Post::shouldReceive and using the DI model by injection Post into the controller.Frederiksen
If the controller is coupled to only an abstract interface PostRepositoriesInterface, what it's gonna call it with a certain instance of this interface? Imagine you have a REST API, based on a micro framework. Controllers implement actions and map routes. What decides which controllers gets an instance of "PostReader" or "CommentsReader"? Above controllers you're above routes and don't know anything about the choice between DB-models to read.Heterogony

© 2022 - 2024 — McMap. All rights reserved.