Saltar al contenido principal
Implementar patrón MVC en PHP
Actualizado el

Guía Completa del Patrón MVC en PHP: Cómo Funciona y Cómo Implementarlo


El patrón MVC (Modelo-Vista-Controlador) sigue siendo uno de los enfoques más sólidos en el desarrollo web, y su popularidad en el ecosistema PHP es innegable.

En este artículo veremos cómo implementar MVC en un proyecto de PHP puro (compatible con PHP 8+) sin usar frameworks como Laravel o Symfony (que a menudo se basan en él). Desglosaremos cada componente de MVC y cómo se interrelacionan, con ejemplos y buenas prácticas actualizadas.

1. ¿Qué es el Patrón MVC y por qué es importante en PHP?

1.1. Definición de MVC: Modelo, Vista, Controlador

Diagrama patrón MVC

El patrón MVC es una arquitectura de software que organiza tu código en tres componentes principales: Modelo, Vista y Controlador. Esta separación te permite mantener un código limpio, modular y fácil de mantener.

  1. Modelo: Se encarga de la lógica de negocio y la gestión de los datos. Es quien interactúa con la base de datos y contiene las reglas de la aplicación.
  2. Vista: Es la parte encargada de mostrar los datos al usuario. Puede ser un archivo HTML, plantillas o incluso JSON, dependiendo del tipo de aplicación.
  3. Controlador: Es el mediador entre la vista y el modelo. Recibe las solicitudes del usuario, procesa la lógica en el modelo y luego presenta la salida adecuada a través de la vista.

1.2. Beneficios de usar MVC en proyectos PHP

El patrón MVC en PHP ofrece varios beneficios importantes:

  • Separación de responsabilidades: Al dividir la aplicación en tres componentes, facilitas el mantenimiento y la escalabilidad.
  • Reutilización de código: Puedes reutilizar el mismo modelo o vista en múltiples controladores o acciones, lo que ahorra tiempo y esfuerzo.
  • Modularidad: Facilita la adición de nuevas funcionalidades sin afectar otras partes del sistema.

Estas características son especialmente útiles en aplicaciones grandes o proyectos que requieren crecimiento a largo plazo.

2. Estructura básica de un proyecto MVC

Un proyecto MVC bien estructurado en PHP debe tener una clara separación entre las carpetas para modelos, vistas y controladores. Una estructura común podría verse así:

/php-mvc-example
|
|-- .htaccess # Configuración URL para servidores Apache
|
|-- /public # Archivos públicos accesibles por el navegador
| |-- /css
| | |-- global.css
| |-- /js
| | |-- script.js
| |-- /images
| |-- index.php # Punto de entrada. Implementa el Router
|
|-- /src
| |-- /Controllers
| | |-- UserController.php
| |-- /Core
| | |-- config.php
| | |-- Controller.php
| | |-- Database.php
| |-- /Models
| | |-- User.php
| |-- /Routes
| | |-- Router.php
| | |-- routes.php
| |-- /Templates
| | |-- header.php
| | |-- footer.php
| |-- /Views
| |-- /users
| |-- create.php
| |-- edit.php
| |-- list.php
|
|-- /vendor # Generado por Composer
|-- composer.json # Dependencias y configuración de Composer
  1. public: aquí se encuentra todo lo que debe ser accesible por el navegador: hojas de estilo CSS, scripts de javascript, imágenes, etc. Además, contiene index.php que implementa el Router y es el punto de entrada a nuestra aplicación.
  2. public/index.php: Este es el punto de entrada principal de la aplicación. Aquí es donde todas las solicitudes son redirigidas y el enrutador determina qué controlador y método ejecutar en función de la solicitud del usuario.
  3. src: aquí tendremos todo el código fuente de la aplicación. Lo descomponemos en directorios para seguir el patrón MVC y tener un código mucho más limpio y escalable.
  4. src/Core/: Clases y configuración esencial para todo el proyecto. Tiene clases padre como Controller y la clase Database que contiene la configuración de nuestra base de datos.
  5. src/Controllers/: Esta carpeta contiene todos los controladores de la aplicación. Los controladores son responsables de manejar las solicitudes del usuario, interactuar con el modelo correspondiente, y luego devolver la vista adecuada.
  6. src/Routes/: Contiene al Router y todas las rutas de nuestra aplicación en routes.php.
  7. src/Models/: Aquí se encuentran los modelos. Los modelos son responsables de manejar la lógica de negocio y la interacción con la base de datos.
  8. src/Views/: Las vistas contienen archivos PHP que representan la capa de presentación. Son responsables de mostrar los datos al usuario, generalmente generados dinámicamente a partir de los datos proporcionados por los controladores.
  9. src/Templates/: Trozos de PHP o HTML que se repiten y van a ser usados en más de una vista.

3. El punto de entrada index.php

Como hemos mencionado el public/index.php implementa el Router. Por tanto, cada petición pasará por aquí y el Router se comunicará con los controladores.

public/index.php
<?php
require_once '../vendor/autoload.php';
require_once '../src/Core/config.php';
require_once '../src/Routes/routes.php';

Es un archivo muy sencillo. Simplemente incluimos lo siguiente:

  • autoload.php: para el correcto funcionamiento de Composer.
  • config.php: configuración nuestra como variables globales.
  • routes.php: aquí definimos las rutas como veremos más adelante.

Es importante que el servidor sepa que index.php es el punto de entrada. En nuestro caso, estamos usando el servidor web Apache de XAMPP. Por tanto, hemos tenido que colocar en la raíz del proyecto el siguiente archivo de configuración .htaccess:

.htaccess
RewriteEngine On
# Modificar con nombre de tu proyecto en htdocs
# En mi caso esta en C:/xampp/htdocs/php-mvc-example/
RewriteBase /php-mvc-example/
# Evitar el /public en la URL
RewriteCond %{THE_REQUEST} /public/([^\s?]*) [NC]
RewriteRule ^ %1 [L,NE,R=302]
# Si la ruta no exige redirigir a /public/index.php (punto de entrada)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ public/index.php [L]
RewriteRule ^((?!public/).*)$ public/$1 [L,NC]

4. Implementando el Router

El Router es responsable de analizar la URL de la solicitud entrante y determinar a qué controlador debe pasar la solicitud, así como qué acción o método dentro de ese controlador debe ejecutar.

En resumen, el Router conecta una URL con un controlador y una acción. Además, también puede capturar parámetros adicionales de la URL que serán pasados al controlador.

src/Routes/Router.php
<?php
namespace App\Routes;
class Router
{
protected $routes = [];
public function add($method, $url, $action)
{
$this->routes[$method][$url] = $action;
}
public function handleRequest()
{
$requestUrl = $_SERVER['REQUEST_URI'];
$requestMethod = $_SERVER['REQUEST_METHOD'];
// URL parse
$url = explode('/', $requestUrl);
$url = array_slice($url, 2);
$requestUrl = '/' . implode('/', $url);
$requestUrl = strtok($requestUrl, '?');
if (isset($this->routes[$requestMethod][$requestUrl])) {
list($controllerName, $methodName) = explode('@', $this->routes[$requestMethod][$requestUrl]);
$controllerName = "App\\Controllers\\" . $controllerName;
$controller = new $controllerName();
return $controller->$methodName();
}
echo "Page not found.";
}
}

Con el Router podremos usar dos métodos:

  • add: añadir rutas según tipo de petición (generalmente usamos GET o POST).
  • handleRequest: se ejecutará cada vez que haya una petición y pase a index.php. IMPORTANTE: al estar en Apache con XAMPP hemos tenido que parsear la URL como indicamos en el código. Esto puede variar según que servidor web uses. En nuestro caso, tenemos que quitar la primera parte que va entre / de la URL.
src/Router/routes.php
<?php
use App\Routes\Router;
$router = new Router();
$router->add('GET', '/users', 'UserController@list');
$router->handleRequest();

Ya en el archivo routes.php añadimos las rutas al Router indicando: tipo de petición (p. ej. GET), la ruta y el Controlador@método.

Por ejemplo, para la primera ruta añadida el Router sabe que si una petición es tipo GET y es a /users, tiene que pasar la petición al UserController y a su método list.

5. Elementos Core de la aplicación

5.1. La clase Controller

Para implementar cada controlador, debemos elaborar una clase padre Controller de la que se extiendan las demás.

src/core/Controller.php
<?php
namespace App\Core;
class Controller
{
public function view($view, $data = [])
{
extract($data);
require_once "../src/Views/$view.php";
}
public function model($model)
{
$modelClass = "App\\Models\\$model";
return new $modelClass();
}
}

Los dos métodos que vamos a necesitar para todos los controladores son:

  • view: dado un nombre de una vista existente en el directorio src/Views/ la renderiza. También le podemos pasar datos como array asociativo para recuperarlos en la vista (p. ej. $data = ['users' => $user->getAll()])
  • model: igual pero para los modelos que estén src/Models/.

Básicamente son métodos para facilitar la comunicación con las vistas y los modelos. Posteriormente, veremos un ejemplo completo. De momento solo estamos viendo el esqueleto del MVC en PHP.

5.2. La clase Database

En el modelo, vamos a tener que realizar distintos tipos de operaciones a la base de datos, pero realmente no es en el modelo donde nos conectamos a ella ni configuramos las credenciales. Para ello, lo centralizamos todo en la clase Database:

src/Core/Database.php
<?php
namespace App\Core;
use PDO;
use PDOException;
class Database
{
private $host = 'localhost';
private $db_name = 'my_db';
private $username = 'root';
private $password = '';
public $conn;
public function connect()
{
$this->conn = null;
try {
$this->conn = new PDO('mysql:host=' . $this->host . ';dbname=' . $this->db_name, $this->username, $this->password);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo 'Connection error: ' . $e->getMessage();
}
return $this->conn;
}
}

Después, en cada modelo, haremos que previo a cada consulta se ejecute el método connect.

Recuerda que estamos estructurando un MVC muy básico para poder empezar nuestro proyecto. En este caso, $host, $db_name, $username y $password deberían definirse con variables de entorno para aumentar la seguridad, pero para desarrollo no necesitamos complicarnos en exceso.

En el caso de que no uses una base de datos MySQL tendrías que cambiar la URL de conexión en $this->conn = new PDO(URL_BASE_DE_DATOS);. Para más información puedes leer la documentación de PDO.

5.3. El archivo de configuración config.php

Tarde o temprano es útil para el desarrollo que usemos variables globales para facilitar el trabajo. Este archivo lo creamos con ese propósito.

src/Core/config.php
<?php
define('TEMPLATE_DIR', __DIR__ . '/../Templates/');
define('PROJECT_ROOT', '/php-mvc-example');

En nuestro caso, definimos dos variables globales para importar cómodamente las Templates (partes de la web que se repiten, como el header o el footer) y otra para facilitar la gestión de las URL al estar usando XAMPP.

6. Ejemplo completo de un patrón MVC en PHP

Ahora os vamos a mostrar un ejemplo muy simple de como usar el patrón MVC que hemos explicado.

6.1. Creando el Modelo User

No hemos implementado en el código la creación de las tablas en la base de datos, así que esta parte toca hacerla manualmente mediante queries desde consola (o desde phpMyAdmin como es nuestro caso).

src/sql/create_users_table.sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL
);

En el modelo implementamos operaciones básicas tipo CRUD. Las queries se escriben en SQL y se pueden parametrizar con el método bindParam.

src/Models/User.php
<?php
namespace App\Models;
use App\Core\Database;
use PDO;
class User
{
private $db;
public function __construct()
{
$this->db = (new Database())->connect();
}
public function getById($id)
{
$query = "SELECT * FROM users WHERE id = :id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
public function save($name, $age)
{
$query = "INSERT INTO users (name, age) VALUES (:name, :age)";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':age', $age);
return $stmt->execute();
}
public function getAll()
{
$query = "SELECT * FROM users";
$stmt = $this->db->prepare($query);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function delete($id)
{
$query = "DELETE FROM users WHERE id = :id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
return $stmt->execute();
}
public function update($id, $name, $age)
{
$query = "UPDATE users SET name = :name, age = :age WHERE id = :id";
$stmt = $this->db->prepare($query);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':age', $age);
return $stmt->execute();
}
}

6.2. Creando el Controlador UserController

Para poder interactuar con el modelo y renderizar las vistas, necesitamos de un controlador que implemente al Controller.

src/Controllers/UserController.php
<?php
namespace App\Controllers;
use App\Core\Controller;
class UserController extends Controller
{
public function list()
{
$user = $this->model('User');
$data = ['users' => $user->getAll()]; // Empaquetamos los datos
$this->view('users/list', $data); // Pasamos los datos a la vista
}
public function create()
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $this->model('User');
// Es vital sanear y validar las entradas del usuario
$name = htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES, 'UTF-8');
$age = (int)($_POST['age'] ?? 0);
if (!empty($name) && $age > 0 && $user->save($name, $age)) {
header('Location: ' . PROJECT_ROOT . '/users');
exit();
} else {
// Idealmente, aquí se manejaría un mensaje de error en la vista
echo "Error al guardar el usuario o datos inválidos.";
}
} else {
$this->view('users/create');
}
}
public function delete()
{
$user = $this->model('User');
$id = $_POST['id'];
if ($user->delete($id)) {
header('Location: ' . PROJECT_ROOT . '/users');
exit();
} else {
echo "Error while deleting user.";
}
}
public function edit()
{
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = $this->model('User');
$id = $_POST['id'];
$name = $_POST['name'];
$age = $_POST['age'];
if ($user->update($id, $name, $age)) {
header('Location: ' . PROJECT_ROOT . '/users');
exit();
} else {
echo "Error while updating user.";
}
} else {
$id = $_GET['id'];
$user = $this->model('User');
$data = ['user' => $user->getById($id)];
$this->view('users/edit', $data);
}
}
}

Aquí tienes que pensar que cada método del controlador va emparejado a una ruta que definiremos más tarde. A su vez, cada método puede tener más de una “versión”. Por ejemplo, el método edit si la petición es tipo GET renderiza la vista (un formulario de edición) y si es tipo POST recupera los datos del formulario y llama al modelo para ejecutar la operación update.

6.3. Creación de las Vistas

Ahora tenemos que implementar la parte más importante para el usuario, ya que es lo que puede apreciar a simple vista.

src/Views/users/edit.php
<?php
$pageTitle = "Editar Usuario";
include TEMPLATE_DIR . 'header.php';
?>
<h2>Editar Usuario</h2>
<form action="<?= PROJECT_ROOT . '/users/edit' ?>" method="POST">
<input type="hidden" name="id" value="<?= htmlspecialchars($user['id']); ?>">
<label for="name">Nombre:</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($user['name']); ?>" required>
<br>
<label for="age">Edad:</label>
<input type="number" id="age" name="age" value="<?= htmlspecialchars($user['age']); ?>" required>
<br>
<button type="submit">Actualizar</button>
</form>
<?php include TEMPLATE_DIR . 'footer.php'; ?>

Esta sería la vista para la edición de usuarios. Contiene un formulario con acción POST que realiza la petición sobre la misma URL. No enseñamos todas las vistas ya que se haría el post muy largo.

Como se puede ver, recuperamos el $user gracias al haber pasado los datos en el controlador ($data = ['user' => $user->getById($id)]).

Otra cosa a destacar es que usamos lo que llamamos Templates: trozos de HTML o PHP que se repiten en varias páginas, como el header o el footer. Las guardamos en src/Templates.

6.4. Añadiendo las rutas

Ya hemos creado lo necesario para que nuestro sistema trabaje con los Users. Sin embargo, si vamos a la URL (en mi caso localhost/php-mvc-example/users) nos mostrará un error HTTP 404. Esto es porque no le hemos dicho al Router qué rutas se corresponden con qué métodos del UserController.

src/Routes/routes.php
<?php
use App\Routes\Router;
$router = new Router();
$router->add('GET', '/users', 'UserController@list');
$router->add('GET', '/users/create', 'UserController@create');
$router->add('POST', '/users/create', 'UserController@create');
$router->add('POST', '/users/delete', 'UserController@delete');
$router->add('GET', '/users/edit', 'UserController@edit');
$router->add('POST', '/users/edit', 'UserController@edit');
$router->handleRequest();

Simplemente utilizamos la sintáxis que ya mencionamos anteriormente para asociar una ruta y un método HTTP a un método de un controlador.

7. Conclusión

El patrón MVC en PHP es una herramienta poderosa que facilita la organización del código y la escalabilidad de los proyectos. Aunque puede parecer complejo al principio, sus beneficios en términos de modularidad y mantenimiento son innegables. Implementar este patrón en tus proyectos PHP te permitirá crear aplicaciones más eficientes y fáciles de mantener a largo plazo. ¡No dudes en comenzar tu proyecto con MVC hoy mismo!

Este artículo cubre los fundamentos del modelo vista controlador en PHP, así como los beneficios de su implementación. Si estás buscando un php mvc tutorial para poner en práctica estos conceptos, esta guía es un excelente punto de partida.

Preguntas Frecuentes

¿Por qué aprender a implementar MVC en PHP puro en lugar de usar un framework como Laravel?

Implementar un MVC desde cero, como se muestra en este artículo, es fundamental para entender la arquitectura subyacente. Aunque frameworks como Laravel o Symfony (que usan este patrón) son más rápidos para producción, comprender cómo se conectan el Router, los Controladores y los Modelos te da un control total y te convierte en un mejor desarrollador.

¿Cuál es la función del `.htaccess` y el `public/index.php`?

Actúan como un "Front Controller". El archivo .htaccess redirige todas las solicitudes (URLs amigables) al único punto de entrada: public/index.php. Esto es crucial por seguridad (oculta la estructura de archivos /src) y para que nuestro Router pueda analizar la URL y decidir qué Controlador y método debe gestionar la petición.

¿Cómo se comunican el Controlador y la Vista? ¿Cómo se pasan los datos?

El Controlador actúa como mediador. Primero, solicita datos al Modelo (ej: $user->getAll()). Luego, pasa estos datos a la Vista a través del método $this->view('nombre-vista', $data). En la clase base Controller, se usa la función extract($data), que convierte un array asociativo (ej: ['users' => $arrayDeUsuarios]) en variables (ej: $users) que ya están disponibles para usarse dentro del archivo de la vista.


Posts Relacionados

Guía Práctica de WCAG 2.2 para Developers (2025)
Guía Práctica de Accesibilidad Web WCAG: Implementación y Testing para Desarrolladores en 2025

Domina la accesibilidad web en 2025. Te explicamos los 4 principios WCAG, los niveles (A, AA, AAA) y cómo auditar e implementar todo con ejemplos de código.

Tailwind Astro: Configuración Rápida y Fácil
Cómo Integrar y Personalizar Tailwind CSS en Proyectos con Astro Framework

Aprende a integrar Tailwind en Astro paso a paso. Cubrimos la instalación, configuración de "tailwind.config" y cómo solucionar errores comunes. Entra ya.

Consejos para la Optimización de Páginas Web
Mejores prácticas para mejorar la Optimización de tu Página Web

Descubre cómo se realiza una optimización de páginas web, analizar el rendimiento del sitio y mejorar el SEO y la experiencia de los usuarios.

Sitemap Astro: Integración Fácil en 5 Minutos
Cómo Crear y Configurar un Sitemap en Astro para Mejorar tu SEO

¿Problemas con tu sitemap de Astro? Te enseñamos a configurarlo correctamente, filtrar páginas y evitar errores comunes. ¡Optimiza tu SEO ahora!

🧩 Desarrolla tu Extensión de Chrome en pocos pasos
Desarrolla tu primera extensión de Chrome en 5 minutos

️Descubre cómo desarrollar una extensión de Chrome desde cero y amplía las funciones del navegador fácilmente.

☑️ ¿Qué es un JWT? Descubre Cómo Usarlo con Ejemplo ▶️
JSON Web Tokens (JWT): Guía Esencial y Buenas Prácticas

¿Qué es un JSON Web Token? ️ Guía completa con un ejemplo práctico en Node.js para crear tokens seguros y proteger tu app. ️