Hasta ahora construimos formularios, consultas con PDO, CRUD, autenticación, permisos, seguridad y programación orientada a objetos. El siguiente paso es aprender a organizar mejor una aplicación PHP.
Una de las arquitecturas más conocidas para separar responsabilidades es MVC: Modelo, Vista y Controlador.
En esta guía vamos a entender qué hace cada parte y construiremos un ejemplo sencillo con productos y MySQL.
¿Qué problema resuelve MVC?
En proyectos pequeños es común comenzar con archivos que mezclan:
- HTML;
- consultas SQL;
- validaciones;
- lógica de negocio;
- redirecciones;
- procesamiento de formularios.
Por ejemplo, un único archivo podría consultar productos, procesar un POST, validar datos y generar todo el HTML.
Eso puede funcionar al principio, pero a medida que la aplicación crece se vuelve difícil de mantener.
MVC intenta separar esas responsabilidades.
¿Qué significa MVC?
MVC significa:
- Model: Modelo;
- View: Vista;
- Controller: Controlador.
Modelo
El Modelo se ocupa de trabajar con los datos y con la lógica relacionada con ellos.
Por ejemplo:
- consultar productos;
- buscar un producto por ID;
- crear registros;
- actualizarlos;
- eliminarlos.
Vista
La Vista se ocupa de presentar la información al usuario.
Normalmente contiene:
- HTML;
- tablas;
- formularios;
- mensajes;
- pequeñas expresiones PHP para mostrar datos.
La vista no debería contener consultas SQL.
Controlador
El Controlador coordina la aplicación.
Por ejemplo:
- recibe una petición;
- solicita información al modelo;
- procesa la lógica necesaria;
- envía datos a la vista.
Flujo básico de MVC
Usuario ↓ Controlador ↓ Modelo ↓ Base de datos ↓ Controlador ↓ Vista ↓ Usuario
No todas las aplicaciones MVC implementan exactamente el mismo flujo, pero esta idea ayuda a entender la separación de responsabilidades.
Estructura de carpetas
Podemos comenzar con una estructura simple:
proyecto/
│
├── app/
│ ├── Controllers/
│ │ └── ProductoController.php
│ │
│ ├── Models/
│ │ └── Producto.php
│ │
│ └── Views/
│ └── productos/
│ ├── index.php
│ └── create.php
│
├── config/
│ └── database.php
│
└── public/
└── index.php
Base de datos de ejemplo
Vamos a utilizar una tabla productos:
CREATE TABLE productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
Conexión a MySQL
En config/database.php podemos centralizar la conexión:
<?php $host = 'localhost'; $db = 'tienda'; $user = 'root'; $password = ''; $dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4"; $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ]); ?>
Crear el Modelo Producto
El modelo será responsable de consultar la tabla.
<?php class Producto { public function __construct( private PDO $pdo ) { } public function todos(): array { $stmt = $this->pdo->query( 'SELECT id, nombre, precio, stock FROM productos ORDER BY id DESC' ); return $stmt->fetchAll(); } } ?>
El modelo conoce PDO y la base de datos. La vista no necesita saber cómo se obtuvieron los productos.
Crear el Controlador
En app/Controllers/ProductoController.php:
<?php class ProductoController { public function __construct( private Producto $producto ) { } public function index(): void { $productos = $this->producto->todos(); require __DIR__ . '/../Views/productos/index.php'; } } ?>
El controlador solicita los datos al modelo y carga la vista.
Crear la Vista
En app/Views/productos/index.php:
<h1>Productos</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
<?php foreach ($productos as $producto): ?>
<tr>
<td>
<?= $producto['id'] ?>
</td>
<td>
<?= htmlspecialchars(
$producto['nombre'],
ENT_QUOTES,
'UTF-8'
) ?>
</td>
<td>
$<?= number_format(
$producto['precio'],
2,
',',
'.'
) ?>
</td>
<td>
<?= $producto['stock'] ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Crear un punto de entrada
Podemos utilizar public/index.php como punto de entrada:
<?php require __DIR__ . '/../config/database.php'; require __DIR__ . '/../app/Models/Producto.php'; require __DIR__ . '/../app/Controllers/ProductoController.php'; $modelo = new Producto($pdo); $controller = new ProductoController($modelo); $controller->index(); ?>
¿Qué logramos?
Ahora cada responsabilidad está separada:
Producto.phptrabaja con los datos;ProductoController.phpcoordina la petición;index.phpmuestra el HTML.
Agregar un formulario de creación
Podemos agregar un método al controlador:
public function create(): void { require __DIR__ . '/../Views/productos/create.php'; }
Vista para crear un producto
<h1>Nuevo producto</h1> <form method="POST" action="?accion=store"> <label>Nombre</label> <input type="text" name="nombre" required> <label>Precio</label> <input type="number" step="0.01" name="precio" required> <label>Stock</label> <input type="number" name="stock" required> <button type="submit"> Guardar </button> </form>
Agregar el método crear() al Modelo
public function crear( string $nombre, float $precio, int $stock ): bool { $sql = 'INSERT INTO productos (nombre, precio, stock) VALUES (:nombre, :precio, :stock)'; $stmt = $this->pdo->prepare($sql); return $stmt->execute([ 'nombre' => $nombre, 'precio' => $precio, 'stock' => $stock ]); }
Agregar store() al Controlador
public function store(): void { $nombre = trim($_POST['nombre'] ?? ''); $precio = filter_input( INPUT_POST, 'precio', FILTER_VALIDATE_FLOAT ); $stock = filter_input( INPUT_POST, 'stock', FILTER_VALIDATE_INT ); if ( $nombre === '' || $precio === false || $stock === false ) { exit('Datos inválidos'); } $this->producto->crear( $nombre, $precio, $stock ); header('Location: index.php'); exit; }
Agregar un router simple
Podemos decidir qué método ejecutar mediante un parámetro:
$accion = $_GET['accion'] ?? 'index'; switch ($accion) { case 'create': $controller->create(); break; case 'store': $controller->store(); break; default: $controller->index(); }
Esto todavía es un router muy básico, pero sirve para entender la idea.
Modelo no significa necesariamente tabla
Es común pensar que cada modelo debe representar una tabla exactamente, pero el concepto es más amplio.
El modelo representa los datos y reglas de negocio de la aplicación.
En proyectos simples puede coincidir bastante con una tabla, pero en sistemas mayores puede incluir lógica más compleja.
La Vista no debería consultar la base
Evitemos algo como esto dentro de una vista:
// No recomendado dentro de una vista $stmt = $pdo->query( 'SELECT * FROM productos' );
La vista debería recibir los datos ya preparados.
El Controlador tampoco debería hacer todo
Otro error frecuente es mover todo el código del archivo original hacia el controlador.
Un controlador gigantesco sigue siendo difícil de mantener.
La idea es que coordine responsabilidades, no que concentre toda la aplicación.
Separar la lógica de negocio
A medida que el proyecto crece, podemos tener otras capas o servicios.
Por ejemplo:
app/ ├── Controllers/ ├── Models/ ├── Services/ └── Views/
Un servicio podría encargarse de reglas específicas que no pertenecen directamente a una vista o a una consulta SQL.
MVC y seguridad
La arquitectura no reemplaza las buenas prácticas vistas anteriormente.
Seguimos necesitando:
- consultas preparadas;
- validación de datos;
- protección CSRF;
- escape de salida;
- autenticación;
- autorización.
MVC no es una regla rígida
Existen diferentes formas de implementar MVC.
Dos frameworks pueden utilizar el patrón y tener estructuras distintas.
Lo importante para comenzar es comprender la separación de responsabilidades.
¿Cómo se relaciona esto con Laravel?
Laravel utiliza muchos de estos conceptos.
Por ejemplo:
- los Models representan y trabajan con datos;
- los Controllers reciben peticiones y coordinan acciones;
- las vistas se construyen normalmente con Blade;
- las rutas determinan qué controlador debe ejecutarse.
Laravel agrega muchas herramientas alrededor de esta arquitectura, pero entender MVC primero hace que sea mucho más fácil comprender el framework.
Errores comunes al aprender MVC
- poner consultas SQL dentro de las vistas;
- crear controladores que hacen absolutamente todo;
- mezclar HTML dentro de los modelos;
- crear una clase por cada pequeño fragmento de código sin necesidad;
- pensar que MVC obliga a usar una estructura exacta;
- creer que usar carpetas llamadas Models, Views y Controllers ya convierte automáticamente un proyecto en MVC.
Ejercicio práctico
Extendé el ejemplo de productos para implementar un CRUD completo utilizando MVC.
Debería incluir:
- listar productos;
- crear productos;
- editar productos;
- eliminar productos;
- validar formularios;
- usar consultas preparadas;
- proteger la eliminación mediante POST;
- separar correctamente Modelo, Vista y Controlador.
¿Qué sigue?
En la próxima guía vamos a aprender Composer y autoloading en PHP.
Veremos:
- qué es Composer;
- qué es
composer.json; - cómo instalar paquetes;
- qué es
vendor; - autoloading;
- PSR-4;
- cómo dejar de utilizar múltiples
requiremanuales.
Ese paso nos acercará todavía más al funcionamiento interno de proyectos PHP modernos y frameworks como Laravel.
¿Te sirvió esta guía? ☕
Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.