En esta clase vamos a entender qué es MVC, qué problema resuelve, qué son los controladores, qué son los modelos, qué son las vistas, qué es un endpoint y cómo todo esto se relaciona con lo que ya hicimos en PHP + MySQL.
Hasta ahora hemos trabajado de forma muy directa: un archivo PHP puede tener HTML, consultas SQL, validaciones, formularios y redirecciones en el mismo lugar. Eso funciona para aprender, pero conforme el proyecto crece se vuelve difícil de mantener.
Por ejemplo, un archivo como venta_save_post.php puede recibir POST, validar datos,
consultar productos, calcular total, insertar una venta y mostrar una respuesta. Todo eso en un solo archivo.
MVC propone separar responsabilidades:
Imagina un restaurante:
En MVC sería:
En un videojuego, cuando presionas un botón para atacar:
En MVC:
La vista es lo que el usuario ve: HTML, tablas, formularios, botones, mensajes.
En tu proyecto actual, ejemplos de vistas serían:
pages/usuarios_list.php cuando imprime una tabla.pages/venta_form_post.php cuando muestra el formulario de ventas.login.php cuando muestra el formulario de inicio de sesión.El modelo representa la parte que trabaja con datos. En una aplicación PHP + MySQL, normalmente el modelo hace consultas SQL.
En nuestro caso, el modelo puede usar las clases que ya conocemos:
Conexion y Functions.
Ejemplo conceptual:
// models/UsuarioModel.php
class UsuarioModel
{
public function listar()
{
$functions = new Functions();
return $functions->execute("
SELECT id, nombre, correo, rol, activo, created_at
FROM usuarios
ORDER BY id DESC
");
}
}
El controlador recibe una petición y decide qué hacer.
Puede recibir:
En tu proyecto ya vimos algo parecido con:
/controllers/AuthController.php?action=login
Ese controlador recibe datos, valida usuario y contraseña, crea sesión y redirige.
Un endpoint es una URL específica de tu sistema que recibe una petición y devuelve una respuesta.
Ejemplos:
/controllers/UsuarioController.php?action=listar
/controllers/UsuarioController.php?action=guardar
/controllers/VentaController.php?action=guardar
/controllers/AuthController.php?action=login
Cada endpoint debería tener una responsabilidad clara.
action=listar → obtener registros.action=guardar → recibir POST y guardar.action=login → validar credenciales e iniciar sesión.UsuarioController.php?action=listar.
Antes teníamos archivos sueltos:
pages/usuarios_list.php
pages/venta_form_post.php
pages/venta_save_post.php
pages/venta_save_async.php
En MVC podríamos empezar a organizar así:
/models
UsuarioModel.php
VentaModel.php
/controllers
UsuarioController.php
VentaController.php
/views
/usuarios
list.php
form.php
/ventas
list.php
form.php
Antes hacíamos esto:
<form method="post" action="venta_save_post.php">
Con controlador sería:
<form method="post" action="/controllers/VentaController.php?action=guardar">
Flujo:
Este ejemplo no lo vamos a implementar todavía en el proyecto completo. Sirve para entender cómo se empezaría a separar la lógica.
<?php
// models/UsuarioModel.php
require_once __DIR__ . '/../lib/Conexion.php';
require_once __DIR__ . '/../lib/Functions.php';
class UsuarioModel
{
private $functions;
public function __construct()
{
// Reutilizamos la clase Functions que ya conocemos.
// Functions contiene execute($sql), que ejecuta consultas SQL.
$this->functions = new Functions();
}
public function listar()
{
// El modelo se encarga de hablar con la base de datos.
// No imprime HTML, solo devuelve el resultado.
return $this->functions->execute("
SELECT id, nombre, correo, rol, activo, created_at
FROM usuarios
ORDER BY id DESC
");
}
public function buscarPorId($id)
{
$id = (int)$id;
return $this->functions->execute("
SELECT id, nombre, correo, rol, activo, created_at
FROM usuarios
WHERE id = $id
LIMIT 1
");
}
}
El controlador recibe acciones por URL. Por ejemplo:
/controllers/UsuarioController.php?action=listar
<?php
// controllers/UsuarioController.php
require_once __DIR__ . '/../models/UsuarioModel.php';
$action = $_GET['action'] ?? '';
$model = new UsuarioModel();
if ($action === 'listar') {
// 1) El controlador pide los datos al modelo
$getUsers = $model->listar();
// 2) Valida si hubo error
if ($getUsers['res'] !== 'ok') {
die($getUsers['res']);
}
// 3) El controlador carga una vista
// La vista usará $getUsers para pintar la tabla.
require_once __DIR__ . '/../views/usuarios/list.php';
exit;
}
if ($action === 'ver') {
$id = $_GET['id'] ?? 0;
$getUser = $model->buscarPorId($id);
if ($getUser['res'] !== 'ok') {
die($getUser['res']);
}
require_once __DIR__ . '/../views/usuarios/detail.php';
exit;
}
// Si la acción no existe
echo "Acción no válida.";
La vista recibe datos ya preparados por el controlador. Aquí sí se encarga de pintar HTML.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Usuarios</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<h1>Usuarios</h1>
<p class="text-muted">Vista renderizada desde MVC.</p>
<div class="card shadow-sm">
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Correo</th>
<th>Rol</th>
<th>Activo</th>
</tr>
</thead>
<tbody>
<?php while ($row = $getUsers['query']->fetch_assoc()) { ?>
<?php foreach ($row as $key => $value) { $$key = $value; } ?>
<tr>
<td><?php echo $id; ?></td>
<td><?php echo $nombre; ?></td>
<td><?php echo $correo; ?></td>
<td><?php echo $rol; ?></td>
<td>
<?php if ($activo == 1) { ?>
<span class="badge text-bg-success">Activo</span>
<?php } else { ?>
<span class="badge text-bg-danger">Inactivo</span>
<?php } ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
Un controlador no siempre carga una vista. También puede responder JSON para que JavaScript lo consuma.
<?php
// controllers/UsuarioApiController.php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../models/UsuarioModel.php';
$action = $_GET['action'] ?? '';
$model = new UsuarioModel();
if ($action === 'listar') {
$getUsers = $model->listar();
if ($getUsers['res'] !== 'ok') {
echo json_encode([
"ok" => false,
"msg" => $getUsers['res']
]);
exit;
}
$usuarios = [];
while ($row = $getUsers['query']->fetch_assoc()) {
$usuarios[] = $row;
}
echo json_encode([
"ok" => true,
"data" => $usuarios
]);
exit;
}
echo json_encode([
"ok" => false,
"msg" => "Acción no válida"
]);
Si un endpoint devuelve JSON, JavaScript puede leer esos datos y crear HTML dinámicamente.
Esto ya no es MVC clásico puro con PHP renderizando vistas. Esto se parece más a:
Frontend JS → Endpoint PHP → Modelo → BD → JSON → JS pinta HTML
// assets/js/usuarios_api.js
document.addEventListener("DOMContentLoaded", async () => {
const tbody = document.getElementById("tbodyUsuarios");
if (!tbody) return;
try {
const res = await fetch("/controllers/UsuarioApiController.php?action=listar");
const json = await res.json();
if (!json.ok) {
tbody.innerHTML = `<tr><td colspan="5">${json.msg}</td></tr>`;
return;
}
tbody.innerHTML = "";
json.data.forEach(usuario => {
tbody.innerHTML += `
<tr>
<td>${usuario.id}</td>
<td>${usuario.nombre}</td>
<td>${usuario.correo}</td>
<td>${usuario.rol}</td>
<td>${usuario.activo == 1 ? "Activo" : "Inactivo"}</td>
</tr>
`;
});
} catch (error) {
tbody.innerHTML = `<tr><td colspan="5">Error al cargar usuarios.</td></tr>`;
}
});