Repository pattern en Laravel

Para los que no estéis familiarizados con este patrón, el Repository pattern es un patrón de tipo estructural que abstrae las operaciones sobre nuestros modelos-capa persistencia (operaciones CRUD) con la intención de ocultar la implementación de esas operaciones a las capas superiores. Concretamente en Laravel, el repositorio, se puede ver como una capa intermedia de comunicación entre la entrada de peticiones y la persistencia de datos. Las ventajas que le veo a este patrón pueden ser:

  • Dispones de una interfaz de comunicación predecible, ya que están muy bien definidas las entradas y salidas de tus funciones (definidas en el contrato).
  • Una capa de comunicación predefinida por el contrato, pero extendible. Otras implementaciones siguen ese contrato.
  • Siguiendo el beneficio de programar orientados a interfaces, la implementación es fácilmente reemplazable.
  • Las operaciones estarán encapsuladas en un único sitio, por lo que el código es reusable.
  • La implementación tiene un único propósito. No habrá lógica de negocio en nuestros repositorios, sólo operaciones sobre la capa de persistencia.

Pero no todo van a ser beneficios :D. Aplicar una capa intermedia de comunicación siempre trae como consecuencia aumentar la complejidad en el sistema, que no siempre tiene por que ser necesaria.

Uno de los artículos de referencia que he utilizado para conocer como otras personas entienden e implementan este patrón, dice lo siguiente: ¨….. abstract some calls into PHP class called Repositories. The idea es that we decouple models from controllers and assign readable names to complicated queries¨ (https://medium.com/employbl/use-the-repository-design-pattern-in-a-laravel-application-13f0b46a3dce). Personalmente no estoy de acuerdo cuando el autor dice que el repositorio asigna nombres fácilmente reconocibles a queries complicadas. Las operaciones no tienen por que ser complicadas siempre, en muchas ocasiones simplemente estaremos implementando un método all() para ocultar $this->model->all() en el caso que utilizamos Eloquent. La clave del repositorio para mí es la abstracción y encapsulación de esas operaciones, independientemente de su complejidad. Otra clave que ya he mencionado anteriormente es la reutilización, reusabilidad en caso de que existan cambios. Otro aspecto es que se divide la aplicación en capas estructuradas.

Casi en todos los proyectos que hago en Laravel siempre me aparece la misma pregunta: ¿Debería aplicar o no aplicar repository pattern? y ¿cómo debería aplicarlo?. Una vez lo tengo implementado siempre me aparece la duda: ¿Realmente me ha valido la pena? Después de aplicarlo en varios proyectos he llegado a la siguiente conclusión: depende, y al menos en mi opinión no aplicarlo de cualquier manera.

Vamos manos a la obra: implementemos un repository pattern.

Digamos que tenemos un blog y queremos crear una API en la que las aplicaciones que la consuman tengan una solo endpoint el cual devuelva la información de un post determinado. Nuestro endpoint será /api/posts/{post} . En este caso que voy a utilizar como ejemplo, obviamente no hace falta ese patrón, pero un ejemplo sencillo me ayudará a explicar la idea. Al final del artículo os hablaré de en que casos en mi opinión vale la pena aplicarlo. En nuestra aplicación Laravel tendremos un controlador llamado PostController que tendrá una único método que será el show, el cual tendrá la siguiente pinta:

<?php

namespace
App\Http\Controllers;

use App\Post;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class PostController extends Controller
{
/**
*
@param $id
*
*
@return mixed
*/
public function show($id)
{
if (null == $post = Post::find($id)) {
throw new ModelNotFoundException("Post not found");
}

return $post;
}
}

¿Por donde empezaríamos a aplicar en este caso el Repository Pattern? Pues como vamos a programar orientados a interfaces, empecemos escribiendo la interfaz principal RepositoryInterface que contendrá los métodos básicos:

<?php

namespace
App\Repositories;

interface RepositoryInterface
{
public function all();

public function create(array $data);

public function update(array $data, $id);

public function delete($id);

public function find($id);
}

En el caso de Laravel, como vamos a registrar la interfaz y su implementación, no queremos que sea esta interfaz principal la que registremos, así que aprovechándonos que podemos extender interfaces, crearemos una interfaz PostRepositoryInterface que extenderá de RepositoryInteface . La ventaja de hacerlo así, es que puedo aumentar PostRepositoryInterface si me hiciera falta en el futuro y luego puedo registrar únicamente RepositoryInterface en las clases que me hiciera falta.

<?php

namespace
App\Repositories;

interface PostRepositoryInterface extends RepositoryInterface
{
}

Ahora ya podemos implementar nuestra clase PostRepository que implementará PostRepositoryInterface

<?php

namespace
App\Repositories;

use App\Post;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class PostRepository implements PostRepositoryInterface
{
protected $model;

/**
* PostRepository constructor.
*
*
@param Post $post
*/
public function __construct(Post $post)
{
$this->model = $post;
}

public function all()
{
$this->model->all();
}

public function create(array $data)
{
return $this->model->create($data);
}

public function update(array $data, $id)
{
return $this->model->where('id', $id)
->update($data);
}

public function delete($id)
{
return $this->model->destroy($id);
}

public function find($id)
{
if (null == $post = $this->model->find($id)) {
throw new ModelNotFoundException("Post not found");
}

return $post;
}
}

Ya tenemos el repositorio preparado. Lo siguiente que tenemos que hacer es pasar al constructor del controlador una instancia del repositorio. Pero como queremos pasar una la interfaz, no la implementación, lo que haremos es que registramos la interfaz y la implementación a resolver en el AppServiceProvider y el Service Container de Laravel se encargará de resolver que implementación que se inyectará en el constructor.

// Código de AppServiceProvider que por brevedad sólo incluyo
// la parte que se registra la interfaz y su clase.
/**
* Register any application services.
*
*
@return void
*/
public function register()
{
$this->app->bind(
'App\Repositories\PostRepositoryInterface',
'App\Repositories\PostRepository'
);
}

Finalmente, refactorizamos nuestro controlador para que empiece a utilizar nuestro repositorio. En el caso que tengamos otras clases o controladores que también están utilizando el modelo Post, podremos refactorizar el código para que también empiecen a usar el PostRepository.

<?php

namespace
App\Http\Controllers;

use App\Repositories\PostRepositoryInterface;

class PostController extends Controller
{
/** @var PostRepositoryInterface */
private $repository;

public function __construct(PostRepositoryInterface $repository)
{
$this->repository = $repository;
}

/**
*
@param $id
*
*
@return mixed
*/
public function show($post)
{
return $this->repository->find($post);
}
}

Los beneficios que tenemos después de aplicar este patrón ahora son más visibles: todas las operaciones relacionadas con el modelo Postestán encapsuladas y todo el que consume el modelo Post lo hace através de la misma API. Pero ahora vendría la siguiente pregunta: ¿Realmente vale la pena utilizar este patrón si Postsólo se utiliza en un controlador? En ese caso obviamente diria que no. Por lo que, para mí un regla a seguir para utilizar este patrón, es que se aplique sobre aquellos modelos que veamos que pueden ser utilizados de forma frecuente en nuestra aplicación por más de una clase.

Otro caso que me parece interesante mencionar donde se puede aplicar este patrón es cuando queremos intercambiar la implementación de la capa de persistencia. Hay una lucha feroz entre los protectores de Eloquent y Doctrine. Yo soy un apasionado de Eloquent, pero también he usado Doctrine y puedo entender que a mucha gente no le guste Eloquent o que incluso lo intercambien depende del caso. Si este es vuestro caso, o incluso si aparece otra implementación de Active record que queremos utilizar, el Repository Pattern nos ayuda a intercambiar esas implementaciones únicamente modificando los repositorios, haciendo que el resto de la aplicación siga funcionando como hasta el momento. Que al final, es lo que se busca aplicando buenas prácticas, que modificaciones en capas interiores, causen los modificaciones mínimas o nulas a las capas superiores.

Referencias para el artículo:

https://mguimaraes.co/repository-pattern-on-laravel/

https://vegibit.com/laravel-repository-pattern/

https://bosnadev.com/2015/03/07/using-repository-pattern-in-laravel-5/

https://medium.com/employbl/use-the-repository-design-pattern-in-a-laravel-application-13f0b46a3dce