El patrón MVC (Modelo-Vista-Controlador) es la base sobre la que están construidos frameworks como Laravel, Symfony o CodeIgniter, y también puede ser implementado en un proyecto PHP sin depender de ninguno de ellos.
En esta guía, un sistema MVC en PHP y MySQL es construido desde cero, utilizando PDO para la conexión a la base de datos y aplicando prácticas que son estándar en proyectos profesionales.
Te puede interesar: LegoBox PHP Micro-Framework MVC PHP MySQL
¿Qué es el patrón MVC en PHP?
MVC es un patrón de diseño mediante el cual el código de una aplicación PHP es dividido en tres capas con responsabilidades separadas:
- Modelo: gestiona el acceso a los datos, normalmente almacenados en MySQL.
- Vista: genera el HTML que finalmente es mostrado en el navegador.
- Controlador: recibe las solicitudes, coordina al Modelo y a la Vista, y decide qué respuesta debe ser devuelta.
El sistema MVC se trata de un patrón arquitectónico considerado un estándar de la industria, ya que esta separación en tres componentes independientes es lo que facilita el mantenimiento, la escalabilidad y el orden de un proyecto PHP a medida que crece.

Por qué usar MVC en un proyecto PHP con MySQL
Cuando la lógica de base de datos, las reglas de negocio y el HTML son mezclados en un mismo archivo, cualquier cambio pequeño obliga a revisar todo el código. Al aplicar MVC en un proyecto PHP con MySQL:
- Cada capa puede ser modificada sin afectar a las demás.
- Las consultas SQL quedan concentradas en un único lugar (el Modelo), lo que facilita su revisión.
- El HTML puede ser rediseñado sin tocar la lógica de negocio.
- Varios desarrolladores pueden trabajar sobre distintas capas al mismo tiempo.
Qué será construido en este tutorial
A lo largo de la guía se implementa un pequeño sistema de usuarios, capaz de:
- Mostrar un listado de usuarios almacenados en MySQL.
- Mostrar el perfil de un usuario específico.
La misma estructura puede ser ampliada más adelante para incluir registro, edición o eliminación de registros.
Requisitos previos
- PHP 8.0 o superior (por el tipado usado en los ejemplos).
- Un servidor MySQL o MariaDB.
- Un entorno local como XAMPP, Laragon o el servidor embebido de PHP (
php -S).
Estructura de carpetas de un proyecto MVC en PHP
mi-proyecto/
├── index.php
├── config/
│ └── Database.php
├── controllers/
│ └── UsuarioControlador.php
├── models/
│ └── UsuarioModelo.php
└── views/
├── usuario_lista.php
└── usuario_perfil.phpLa carpeta config/ es la que diferencia este ejemplo de un MVC básico: aquí queda aislada la lógica de conexión a MySQL, separada tanto del Modelo como del Controlador.
Paso 1: Crear la base de datos MySQL
El siguiente script SQL puede ejecutarse en phpMyAdmin, en la terminal de MySQL o en cualquier gestor equivalente:
CREATE DATABASE IF NOT EXISTS mvc_ejemplo
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mvc_ejemplo;
CREATE TABLE usuarios (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
correo VARCHAR(150) NOT NULL UNIQUE,
creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO usuarios (nombre, correo) VALUES
('Ana Torres', 'ana@example.com'),
('Luis Pérez', 'luis@example.com');Con este script, la tabla usuarios queda creada y dos registros de prueba quedan disponibles para las consultas que serán realizadas más adelante.
Paso 2: Conectar PHP con MySQL usando PDO
Para la conexión con MySQL, PDO (PHP Data Objects) es preferido frente a la extensión mysqli, ya que ofrece sentencias preparadas de forma más consistente y permite cambiar de motor de base de datos con modificaciones mínimas en el código.
<?php
// config/Database.php
class Database
{
private static ?PDO $conexion = null;
public static function obtenerConexion(): PDO
{
if (self::$conexion === null) {
$host = 'localhost';
$nombreBD = 'mvc_ejemplo';
$usuario = 'root';
$contrasena = '';
$dsn = "mysql:host={$host};dbname={$nombreBD};charset=utf8mb4";
try {
self::$conexion = new PDO($dsn, $usuario, $contrasena, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $error) {
die('Error de conexión: ' . $error->getMessage());
}
}
return self::$conexion;
}
}Tres decisiones son aplicadas en esta clase:
- Una sola conexión reutilizada. Gracias a la propiedad estática
$conexion, un patrón singleton simple es implementado, evitando que múltiples conexiones a MySQL sean abiertas durante la misma ejecución. - Errores convertidos en excepciones. Con
PDO::ERRMODE_EXCEPTION, cualquier fallo en una consulta puede ser capturado contry/catch, en lugar de fallar en silencio. - Sentencias preparadas reales. Al desactivar
PDO::ATTR_EMULATE_PREPARES, las sentencias preparadas del propio motor MySQL son utilizadas, lo que refuerza la protección contra inyección SQL.
En producción, las credenciales no deberían quedar escritas directamente en el código. Lo recomendado es que sean cargadas desde variables de entorno o desde un archivo
.env, con una librería comovlucas/phpdotenv.
Paso 3: El Modelo, encargado de los datos
El Modelo concentra toda la comunicación con MySQL. En cada consulta que recibe datos externos, sentencias preparadas son utilizadas en lugar de concatenar valores directamente en el SQL.
<?php
// models/UsuarioModelo.php
require_once __DIR__ . '/../config/Database.php';
class UsuarioModelo
{
private PDO $conexion;
public function __construct()
{
$this->conexion = Database::obtenerConexion();
}
public function obtenerTodos(): array
{
$consulta = $this->conexion->query(
'SELECT id, nombre, correo FROM usuarios ORDER BY nombre'
);
return $consulta->fetchAll();
}
public function obtenerPorId(int $id): ?array
{
$consulta = $this->conexion->prepare(
'SELECT id, nombre, correo FROM usuarios WHERE id = :id'
);
$consulta->execute(['id' => $id]);
$usuario = $consulta->fetch();
return $usuario ?: null;
}
public function crear(string $nombre, string $correo): bool
{
$consulta = $this->conexion->prepare(
'INSERT INTO usuarios (nombre, correo) VALUES (:nombre, :correo)'
);
return $consulta->execute([
'nombre' => $nombre,
'correo' => $correo,
]);
}
}En obtenerPorId() y en crear(), marcadores con nombre (:id, :nombre, :correo) son utilizados en lugar de valores insertados directamente en el texto de la consulta. Los datos reales son enviados por separado mediante execute(), con lo cual la inyección SQL queda neutralizada por PDO de forma automática.
Paso 4: Las Vistas, encargadas de la presentación
En las Vistas, solo código de presentación es incluido. Ninguna consulta a MySQL es realizada aquí: los datos ya llegan listos desde el Controlador.
<!-- views/usuario_lista.php -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Usuarios registrados</title>
</head>
<body>
<h1>Usuarios registrados</h1>
<ul>
<?php foreach ($usuarios as $usuario): ?>
<li>
<?= htmlspecialchars($usuario['nombre']) ?>
— <?= htmlspecialchars($usuario['correo']) ?>
</li>
<?php endforeach; ?>
</ul>
</body>
</html><!-- views/usuario_perfil.php -->
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Perfil de usuario</title>
</head>
<body>
<h1>Perfil de usuario</h1>
<p><strong>Nombre:</strong> <?= htmlspecialchars($usuario['nombre']) ?></p>
<p><strong>Correo:</strong> <?= htmlspecialchars($usuario['correo']) ?></p>
</body>
</html>En ambas Vistas, htmlspecialchars() es aplicado a cada dato impreso, previniendo así ataques XSS en caso de que algún registro contenga código HTML o JavaScript.
Paso 5: El Controlador, encargado de conectar todo
El Controlador solicita los datos al Modelo y, según el resultado, carga la Vista correspondiente.
<?php
// controllers/UsuarioControlador.php
require_once __DIR__ . '/../models/UsuarioModelo.php';
class UsuarioControlador
{
private UsuarioModelo $modelo;
public function __construct()
{
$this->modelo = new UsuarioModelo();
}
public function listar(): void
{
$usuarios = $this->modelo->obtenerTodos();
require __DIR__ . '/../views/usuario_lista.php';
}
public function mostrarPerfil(int $id): void
{
$usuario = $this->modelo->obtenerPorId($id);
if (!$usuario) {
http_response_code(404);
echo 'Usuario no encontrado.';
return;
}
require __DIR__ . '/../views/usuario_perfil.php';
}
}Paso 6: index.php como punto de entrada (front controller)
Un único archivo index.php recibe todas las solicitudes y las redirige hacia la acción del Controlador que corresponda:
<?php
// index.php
require_once __DIR__ . '/controllers/UsuarioControlador.php';
$accion = $_GET['accion'] ?? 'listar';
$controlador = new UsuarioControlador();
switch ($accion) {
case 'listar':
$controlador->listar();
break;
case 'perfil':
$id = (int) ($_GET['id'] ?? 0);
$controlador->mostrarPerfil($id);
break;
default:
http_response_code(404);
echo 'Página no encontrada.';
break;
}Con este enrutador, dos rutas quedan disponibles:
index.php?accion=listar— listado completo de usuarios.index.php?accion=perfil&id=1— perfil de un usuario específico.
Buenas prácticas para MVC en PHP y MySQL
- Sentencias preparadas con PDO en cada consulta que recibe datos externos, para evitar inyección SQL.
- Escape de salida con
htmlspecialchars()en toda Vista, para prevenir XSS. - Separación estricta de responsabilidades: conexión, datos, control de flujo y presentación en archivos independientes.
- Una sola conexión PDO reutilizada durante toda la ejecución, mediante el patrón singleton.
- Tipado explícito (
int,string,array,?array,void) para detectar errores antes de que lleguen a producción. - Manejo de errores con
try/catch, evitando exponer detalles técnicos al usuario final. - Credenciales fuera del código fuente en cualquier entorno de producción.
Preguntas frecuentes sobre MVC en PHP y MySQL
¿Qué significa MVC en PHP? MVC significa Modelo-Vista-Controlador, un patrón de diseño que divide el código PHP en tres capas: datos, presentación y control de flujo.
¿Es obligatorio usar un framework para aplicar MVC en PHP? No. Como se muestra en esta guía, MVC puede ser implementado con PHP puro. Frameworks como Laravel o Symfony añaden herramientas adicionales (enrutamiento avanzado, ORM, autenticación), pero el patrón puede aplicarse sin ellos.
¿Por qué usar PDO en lugar de mysqli en un proyecto MVC? PDO ofrece una API más consistente para sentencias preparadas y permite cambiar de motor de base de datos con menos cambios en el código, algo que mysqli no soporta de forma nativa.
¿Cuál es la diferencia entre el Modelo y el Controlador? El Modelo accede y manipula los datos en MySQL; el Controlador recibe la solicitud, decide qué necesita hacerse y coordina al Modelo y a la Vista para producir la respuesta.
¿MVC sirve también para proyectos PHP pequeños? En scripts muy simples, la estructura completa puede ser innecesaria. Sin embargo, incluso en proyectos pequeños, aplicar MVC facilita el crecimiento posterior del sistema.
Conclusión
Con MySQL conectado mediante PDO, sentencias preparadas en cada consulta y una separación clara entre Modelo, Vista y Controlador, el sistema construido en esta guía queda listo para crecer: nuevos controladores, nuevos modelos y nuevas vistas pueden ser agregados siguiendo la misma estructura, sin comprometer el orden ni la seguridad del proyecto.