Patrón Repository en PHP: separar la lógica de acceso a datos

El patrón Repository separa la lógica de negocio del mecanismo de persistencia. En lugar de que una clase de servicio ejecute consultas SQL directamente, delega esa responsabilidad en un repositorio: una clase cuya única función es almacenar y recuperar entidades. Esto facilita los tests, permite cambiar la fuente de datos sin modificar la lógica y hace el código más fácil de mantener.

La interfaz del repositorio

Lo primero es definir una interfaz que describe qué operaciones ofrece el repositorio, sin comprometerse con ningún mecanismo de persistencia concreto.

<?php
declare(strict_types=1);

namespace AppRepositorios;

use AppModelosUsuario;

interface UsuarioRepositorioInterface
{
    public function buscarPorId(int $id): ?Usuario;
    public function buscarPorEmail(string $email): ?Usuario;

    /** @return Usuario[] */
    public function listarActivos(): array;

    public function guardar(Usuario $usuario): void;
    public function eliminar(int $id): void;
}
?>

Implementación con PDO

<?php
declare(strict_types=1);

namespace AppRepositorios;

use AppModelosUsuario;
use PDO;

class UsuarioRepositorioPDO implements UsuarioRepositorioInterface
{
    public function __construct(private PDO $pdo) {}

    public function buscarPorId(int $id): ?Usuario
    {
        $stmt = $this->pdo->prepare('SELECT * FROM usuarios WHERE id = ? LIMIT 1');
        $stmt->execute([$id]);
        $fila = $stmt->fetch(PDO::FETCH_ASSOC);

        return $fila ? $this->mapear($fila) : null;
    }

    public function buscarPorEmail(string $email): ?Usuario
    {
        $stmt = $this->pdo->prepare('SELECT * FROM usuarios WHERE email = ? LIMIT 1');
        $stmt->execute([$email]);
        $fila = $stmt->fetch(PDO::FETCH_ASSOC);

        return $fila ? $this->mapear($fila) : null;
    }

    public function listarActivos(): array
    {
        $stmt = $this->pdo->query('SELECT * FROM usuarios WHERE activo = 1 ORDER BY nombre');
        return array_map([$this, 'mapear'], $stmt->fetchAll(PDO::FETCH_ASSOC));
    }

    public function guardar(Usuario $usuario): void
    {
        if ($usuario->id === null) {
            // INSERT
            $stmt = $this->pdo->prepare(
                'INSERT INTO usuarios (nombre, email, activo) VALUES (?, ?, ?)'
            );
            $stmt->execute([$usuario->nombre, $usuario->email, (int) $usuario->activo]);
        } else {
            // UPDATE
            $stmt = $this->pdo->prepare(
                'UPDATE usuarios SET nombre = ?, email = ?, activo = ? WHERE id = ?'
            );
            $stmt->execute([$usuario->nombre, $usuario->email, (int) $usuario->activo, $usuario->id]);
        }
    }

    public function eliminar(int $id): void
    {
        $stmt = $this->pdo->prepare('DELETE FROM usuarios WHERE id = ?');
        $stmt->execute([$id]);
    }

    private function mapear(array $fila): Usuario
    {
        return new Usuario(
            id:     (int) $fila['id'],
            nombre: $fila['nombre'],
            email:  $fila['email'],
            activo: (bool) $fila['activo'],
        );
    }
}
?>

Repositorio en memoria para tests

Una de las grandes ventajas del patrón Repository es que los tests pueden usar una implementación en memoria que no necesita base de datos, lo que los hace rápidos y sin efectos secundarios.

<?php
namespace AppRepositorios;

use AppModelosUsuario;

class UsuarioRepositorioEnMemoria implements UsuarioRepositorioInterface
{
    private array $usuarios = [];
    private int   $nextId   = 1;

    public function buscarPorId(int $id): ?Usuario
    {
        return $this->usuarios[$id] ?? null;
    }

    public function buscarPorEmail(string $email): ?Usuario
    {
        foreach ($this->usuarios as $usuario) {
            if ($usuario->email === $email) {
                return $usuario;
            }
        }
        return null;
    }

    public function listarActivos(): array
    {
        return array_values(array_filter(
            $this->usuarios,
            fn(Usuario $u): bool => $u->activo
        ));
    }

    public function guardar(Usuario $usuario): void
    {
        if ($usuario->id === null) {
            $usuario = new Usuario($this->nextId++, $usuario->nombre, $usuario->email, $usuario->activo);
        }
        $this->usuarios[$usuario->id] = $usuario;
    }

    public function eliminar(int $id): void
    {
        unset($this->usuarios[$id]);
    }
}

// En el test:
// $repositorio = new UsuarioRepositorioEnMemoria();
// $servicio    = new RegistroServicio($repositorio);
// $servicio->registrar('Ana', '[email protected]');
// $this->assertNotNull($repositorio->buscarPorEmail('[email protected]'));
?>

Repository vs Active Record

El patrón Active Record, que usan ORMs como Eloquent de Laravel, une los datos y la lógica de persistencia en la misma clase. Es más rápido de implementar pero acopla la entidad a la base de datos, dificultando los tests. Repository separa las responsabilidades a cambio de más código inicial.

<?php
// Active Record (Eloquent de Laravel): todo en la misma clase
// $usuario = Usuario::find(1);
// $usuario->nombre = 'Ana';
// $usuario->save();   ? la entidad sabe cómo guardarse

// Repository (DDD): entidad pura + repositorio separado
$usuario = $repositorio->buscarPorId(1);
$usuario = new Usuario($usuario->id, 'Ana', $usuario->email, $usuario->activo);
$repositorio->guardar($usuario); // el repositorio sabe cómo guardarse, la entidad no
?>

La documentación sobre PDO en PHP complementa este patrón con detalles sobre transacciones, modos de fetch y sentencias preparadas reutilizables para repositorios de alto rendimiento.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP