CQRS EN PHP: EJEMPLO PRÁCTICO PASO A PASO

Hace un tiempo escribí sobre CQRS y revisando el post para ampliar el contenido. He visto que faltaba un ejemplo que voy a poner. A continuación, te presento un ejemplo práctico de cómo implementar el patrón CQRS (Command Query Responsibility Segregation) en una aplicación PHP simple, usando MySQL como base de datos y Apache como servidor web. El ejemplo será una aplicación básica de gestión de usuarios, donde se separan las operaciones de escritura (crear/actualizar usuarios) y lectura (consultar usuarios) en modelos distintos.

Estructura

  • CQRS:Modelo de escritura: Tabla users para comandos (crear/actualizar usuarios).
  • Modelo de lectura: Tabla users_view optimizada para consultas rápidas.
  • Sincronización mediante un manejador de eventos simple en PHP.

Estructura del Proyecto

/cqrs-example
├── config.php      # Configuración de la base de datos
├── command.php    # Lógica de comandos (escritura)
├── query.php      # Lógica de consultas (lectura)
├── event_handler.php   # Sincronización entre modelos
├── index.php      # Interfaz web simple
└── schema.sql     # Esquema de la base de datos

Base de datos

CREATE DATABASE cqrs_example;
USE cqrs_example;

-- Modelo de escritura (normalizado)
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Modelo de lectura (desnormalizado para consultas rápidas)
CREATE TABLE users_view (
    id INT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL
);

Lógica de comandos

Los comandos manejan las operaciones de escritura (crear un usuario) y disparan un evento para actualizar el modelo de lectura.


require_once 'config.php';
require_once 'event_handler.php';

class UserCommandHandler {
    private $db;
    private $eventHandler;

    public function __construct() {
   $this->db = (new Database())->getPdo();
   $this->eventHandler = new EventHandler();
    }

    public function createUser($email, $name) {
   // Validación básica
   if (empty($email) || empty($name)) {
  throw new Exception("Email y nombre son requeridos");
   }

   // Insertar en el modelo de escritura
   $stmt = $this->db->prepare("INSERT INTO users (email, name) VALUES (:email, :name)");
   $stmt->execute(['email' => $email, 'name' => $name]);
   $userId = $this->db->lastInsertId();

   // Disparar evento para sincronizar el modelo de lectura
   $this->eventHandler->handleUserCreated($userId, $email, $name);

   return $userId;
    }
}

Lógica de Consultas

Las consultas leen desde el modelo de lectura optimizado,

require_once 'config.php';

class UserQueryHandler {
    private $db;

    public function __construct() {
   $this->db = (new Database())->getPdo();
    }

    public function getAllUsers() {
   $stmt = $this->db->query("SELECT * FROM users_view");
   return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function getUserById($id) {
   $stmt = $this->db->prepare("SELECT * FROM users_view WHERE id = :id");
   $stmt->execute(['id' => $id]);
   return $stmt->fetch(PDO::FETCH_ASSOC);
    }
}

Manejador de Eventos

Sincroniza el modelo de escritura con el de lectura.

require_once 'config.php';

class EventHandler {
    private $db;

    public function __construct() {
   $this->db = (new Database())->getPdo();
    }

    public function handleUserCreated($userId, $email, $name) {
   // Sincronizar con el modelo de lectura
   $stmt = $this->db->prepare("INSERT INTO users_view (id, email, name) VALUES (:id, :email, :name)");
   $stmt->execute(['id' => $userId, 'email' => $email, 'name' => $name]);
    }
}

Explicación del CQRS

  • Modelo de escritura: La tabla users es usada por UserCommandHandler para insertar datos, asegurando validaciones (ej. unicidad del email).
  • Modelo de lectura: La tabla users_view es usada por UserQueryHandler para consultas rápidas, sin lógica de negocio compleja.
  • Sincronización: EventHandler actualiza users_view cuando se crea un usuario, simulando un sistema de eventos (en un sistema real, podrías usar una cola de mensajes como RabbitMQ).