Inyección de dependencia de Laravel: ¿Cuándo tienes que hacerlo?¿Cuándo puedes simularse fachadas?Ventajas de cualquiera de los métodos?

StackOverflow https://stackoverflow.com//questions/22031972

Pregunta

He estado usando Laravel por un tiempo y he estado leyendo mucho sobre la inyección de dependencia un código de prueba. He llegado a un punto de confusión al hablar de fachadas y objetos burlados. Veo dos patrones:

class Post extends Eloquent {

    protected $guarded = array();

    public static $rules = array();

}

Este es mi modelo postal. Podría ejecutar Post::all(); para obtener todos los mensajes de mi blog. Ahora quiero incorporarlo en mi controlador.


Opción # 1: Inyección de dependencia

Mi primer instinto sería inyectar el modelo de Post como dependepy:

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

}

La prueba de mi unidad se vería así:

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

Opción # 2: Fachada Mocks

class HomeController extends BaseController {


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

}

La prueba de mi unidad se vería así:

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

Entiendo ambos métodos, pero no entiendo por qué debería o cuando debería usar un método sobre el otro. Por ejemplo, he intentado usar la ruta DI con la clase de Auth, pero no funciona, así que tengo que usar las simulaciones de fachada. Cualquier calcificación sobre este tema sería muy apreciado.

¿Fue útil?

Solución

Aunque utiliza la inyección de dependencia en la opción # 1, su controlador todavía está acoplado con el ORM ELOQUENTE. (Tenga en cuenta que evito usar el modelo de término aquí porque en MVC, el modelo no es solo una clase o un objeto, sino una capa. Es la lógica de su negocio).

La inyección de dependencia permite la inversión de dependencia, pero no son lo mismo. De acuerdo con el principio de la inversión de dependencia, tanto el código alto como el bajo nivel de nivel debe depender de las abstracciones. En su caso, el código de alto nivel es su controlador y el código de bajo nivel es el ORM elocuente que obtiene datos de MySQL, pero como puede ver que ninguno de ellos depende de las abstracciones.

Como consecuencia, no puede cambiar la capa de acceso de datos sin afectar su controlador. ¿Cómo iría cambiar, por ejemplo, desde MySQL a MongoDB o al sistema de archivos? Para hacer esto, tiene que usar repositorios (o lo que sea que quiera llamarlo).

Por lo tanto, cree una interfaz de repositorios que todas sus implementaciones de repositorio concreto (MySQL, MongoDB, sistema de archivos, etc.) deben implementar.

interface PostRepositoriesInterface {

    public function getAll();
}

y luego cree su implementación concreta, por ejemplo. para 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.*/
    }
}

Ahora su controlador debe escribir sugerir la interfaz y no la implementación concreta. A esto se trata de lo que se trata el "código en una interfaz y una implementación de una implementación". Esta es la inversión de dependencia.

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

}

De esta manera, su controlador está desacoplado de su capa de datos. Está abierto para la extensión pero cerrado para la modificación. Puede cambiar a MongoDB o al sistema de archivos creando una nueva implementación concreta de postrepositoriosinterface (por ejemplo, mongopostrepositivo) y cambie solo la unión de (tenga en cuenta que no uso ningún espacio de nombres aquí):

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

a

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

En una situación ideal, su controlador debe contener solo aplicación y no lógica de negocios. Si alguna vez se encuentra queriendo llamar a un controlador de otro controlador, es una señal que ha hecho algo mal. En este caso, sus controladores contienen demasiada lógica.

Esto también hace que las pruebas sean más fáciles. Ahora puede probar su controlador sin obtener realmente la base de datos. Tenga en cuenta que una prueba de controlador debe probar solo si el controlador funciona correctamente, lo que significa que el controlador llama al método correcto, obtiene los resultados y lo pasa a la vista. En este punto, no está probando la validez de los resultados. Esto no es responsabilidad del controlador.

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

editar

Si elige ir con la opción # 1, puede probarlo así

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

}

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top