Patrón Repository: por qué no es tan recomendable
- Publicado el 13 septiembre, 2024
- Palabras: 1583
Si has trabajado con Laravel durante suficiente tiempo, probablemente hayas visto que el patrón Repositorio se usaba mucho hace 5 o 10 años. Pero ahora no es tan popular. Déjame demostrarte. Esta artículo será un poco más de opinión, pero se basará en las opiniones firmes de algunos miembros más influyentes de la comunidad de Laravel.
#Ejemplo típico de repository no tan práctico
En algunos tutoriales de Laravel, especialmente los más antiguos, puedes encontrar ejemplos similares a este:
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use Illuminate\View\View;
class UserController extends Controller
{
public function __construct(
protected UserRepository $users,
) {}
public function show(string $id): View
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
Entonces, la clase Repository podría verse así:
app/Repositories/UserRepository.php:
class UserRepository
{
public function find($id)
{
return User::find($id);
}
}
Pero esto no parece muy útil. ¿Por qué un repositorio con $this->users->find() en lugar de solo User::find()? ¿Una clase separada para hacer lo mismo?
Normalmente, los autores de esos tutoriales enfatizan la necesidad de simular o intercambiar la clase del repositorio con alguna otra clase en el futuro, para una mejor capacidad de prueba y aislamiento.
Pero este típico ejemplo simplificado en exceso anterior no hace justicia a la idea del patrón Repository en general.
Déjame mostrarte mejores ejemplos, y luego seguiré debatiendo que NO los apruebo en Laravel.
#Ejemplo de repository más práctico
Para mostrar realmente la idea de "intercambio" y "simulación" del mismo ejemplo anterior, debería ser así:
- Crear una interfaz de repository
- Crear una clase de repository que implemente esa interfaz
- En el controlador, escriba la interfaz como referencia
- En el proveedor de servicios, vincule la interfaz a esta clase, con la posibilidad futura de vincularla con otra clase de repository diferente
A continuación, se incluye un ejemplo.
app/Interfaces/UserRepositoryInterface.php:
namespace App\Interfaces;
interface UserRepositoryInterface
{
public function all();
public function find(int $id);
public function store(array $userData);
public function update(int $id, array $userData);
public function delete(int $id);
}
Definimos entonces los mismos métodos típicos de Eloquent.
Luego, la implementación está en el repository.
app/Repositories/UserRepository.php:
namespace App\Repositories;
use App\Interfaces\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface
{
public function all()
{
return User::all();
}
public function find($id)
{
return User::find($id);
}
// ...
}
Luego, en el Controlador, inyectamos la interfaz:
namespace App\Http\Controllers;
use App\Interfaces\UserRepositoryInterface;
use Illuminate\View\View;
class UserController extends Controller
{
public function __construct(
protected UserRepositoryInterface $users,
) {}
public function show(int $id): View
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}
Finalmente, especificamos qué clase de Repository utilizar (sí, solo tenemos una, por ahora) en AppServiceProvider:
app/Providers/AppServiceProvider.php:
use App\Interfaces\UserRepositoryInterface;
use App\Repositories\UserRepository;
// ...
public function register()
{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
Entonces, nuestro objetivo se habrá logrado: más tarde podremos intercambiar ese UserRepository con cualquier otra clase de Repositorio que implemente la misma interfaz pero que tenga códigos diferentes para trabajar con los usuarios.
#Pequeña mejora: ¿El modelo como parámetro?
Otro ejemplo es tener el modelo Eloquent como parámetro dentro de la clase Repository.
namespace App\Repositories;
use App\Interfaces\UserRepositoryInterface;
class UserRepository implements UserRepositoryInterface
{
public function __construct(
protected User $model
) {}
public function all()
{
return $this->model->all();
}
public function find($id)
{
return $this->model->find($id);
}
// ...
}
Entonces, ¿potencialmente cambiarías ese modelo por otro?
#Yendo aún más hacia lo "Meta": BaseRepository
Otro ejemplo que he visto es la clase denominada BaseRepository o BaseEloquentRepository, que contiene todas las implementaciones predeterminadas de los métodos Eloquent. Luego, las clases UserRepository o OrderRepository independientes extienden esa clase base.
app/Interfaces/EloquentRepositoryInterface.php:
namespace App\Interfaces;
use Illuminate\Database\Eloquent\Model;
interface EloquentRepositoryInterface
{
public function create(array $attributes): Model;
public function find($id): ?Model;
// ...
}
app/Repositories/BaseRepository.php:
namespace App\Repositories;
use App\Interfaces\EloquentRepositoryInterface;
use Illuminate\Database\Eloquent\Model;
class BaseRepository implements EloquentRepositoryInterface
{
public function __construct(
protected Model $model
) {}
public function create(array $attributes): Model
{
return $this->model->create($attributes);
}
public function find($id): ?Model
{
return $this->model->find($id);
}
// ...
}
app/Repositories/UserRepository.php:
namespace App\Repositories\Eloquent;
use App\Models\User;
use Illuminate\Support\Collection;
class UserRepository extends BaseRepository
{
public function __construct(User $model)
{
parent::__construct($model);
}
// We don't need to implement find() method here
// Because it comes from the BaseRepository
// We only implement EXTRA methods in this class
// That are not in the BaseRepository
}
También hay otras variaciones, como el patrón Service-Repository de este artículo, que no quiero mostrar aquí. En cambio, quiero llegar al punto principal.
Esos ejemplos se ven muy bien: separados, abstractos, flexibles, al estilo de los patrones de diseño, ¿no?
Pero déjame preguntarte esto: ¿cuándo fue la última vez que tuviste que cambiar la implementación de Eloquent por algo diferente?
#Repository para Eloquent: ¿Inútil en Laravel?
La idea del patrón Repositorio en su totalidad es buena y no se aplica a Laravel ni a PHP.
Uno de los ejemplos típicos de patrones Repositorio que no son de Laravel es exactamente eso: implementación de una capa de base de datos separada, de modo que pueda intercambiar fácilmente los motores de base de datos y almacenamiento.
Pero en Laravel, Eloquent ORM ya está haciendo eso por nosotros.
Si desea cambiar entre MySQL, SQLite, PostgreSQL o SQL Server, no cambia Eloquent por otra cosa. Simplemente configura un controlador diferente en config/database.php y tal vez necesite algunos cambios más en consultas específicas.
Entonces, en cierto modo, Eloquent ya es una capa de repositorio. Llama a User::all() y luego, en segundo plano, Eloquent sabe qué controlador usar: MySQL o SQLite.
Si desea usar controladores de bases de datos distintos de los oficialmente admitidos por Laravel, probablemente necesite cambiar bastante todo su código de Laravel. Tener un patrón Repositorio para operaciones de bases de datos no resolvería eso.
Mi punto es el siguiente: ¿hay un punto en el patrón Repositorio con este ejemplo "clásico" de "¿Qué pasa si necesitamos reemplazar Eloquent?"?
Otro argumento es que crear esas interfaces/repositorios con código de controlador más extenso parece mucho trabajo sin ningún beneficio inmediato (o, lo más probable, sin ningún beneficio inmediato).
#¿Pero qué pasa si... el repository NO es para Eloquent?
Cuando hablamos de repositorios, a menudo asumimos que deben estar conectados a Eloquent. Pero ¿qué pasa si no lo están?
En este caso, tenemos un ejemplo de código simple de Gummibeer:
class AuthorRepository
{
public function all(): Collection
{
return collect([
[
'nickname' => 'Gummibeer',
'email' => 'dev@gummibeer.de',
'firstname' => 'Tom',
'lastname' => 'Witkowski',
'payment_pointer' => '$ilp.uphold.com/EagWEdJU64mN',
'twitter' => '@devgummibeer',
],
])
->mapInto(Author::class);
}
public function find(string $nickname): Author
{
return $this->all()
->first(fn (Author $author): bool => Str::slug($author->nickname) === Str::slug($nickname));
}
}
Como puede ver, aquí no se utiliza ningún modelo. De hecho, estamos trabajando directamente con colecciones/matrices. Y ahí es donde los repositorios pueden resultar útiles. Por ejemplo, cambiemos el repositorio de una colección a una llamada API real:
class AuthorRepository
{
public function all(): Collection
{
return Http::get('https://api.example.com/authors')
->json()
->mapInto(Author::class);
}
public function find(string $nickname): Author
{
return $this->all()
->first(fn (Author $author): bool => Str::slug($author->nickname) === Str::slug($nickname));
}
}
Como puede ver, solo tuvimos que cambiar un método (en este caso, la fuente) para aplicar una lógica diferente, y el controlador no cambiaría en absoluto, seguirá llamando a algo como $this->authorRepository->all(), sin siquiera saber de dónde provienen esos datos.
Sin los repositorios, es posible que hayamos tenido que cambiar muchos archivos. Aquí es donde los repositorios pueden ser útiles, si está trabajando con diferentes fuentes de datos que pueden cambiar, no solo Eloquent.
Antonio Jenaro
Web Developer
Fuente: Laravel Daily
Inicia la conversación
Hazte miembro de Antonio Jenaro para comenzar a comentar.
Regístrate ahora¿Ya estás registrado? Inicia sesión