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