17-09-2025

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).