Implementar patrón MVC en PHP

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


El patrón MVC (Modelo-Vista-Controlador) es uno de los enfoques más utilizados en el desarrollo web, y su popularidad en PHP es innegable. En este artículo veremos cómo implementar MVC en un proyecto de PHP puro sin el uso de frameworks como Laravel o Symphony. Vamos a desglosar cada componente de MVC y cómo se interrelacionan, explorando ejemplos y buenas prácticas.

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');
            $name = $_POST['name'];
            $age = $_POST['age'];

            if ($user->save($name, $age)) {
                header('Location: ' . PROJECT_ROOT . '/users');
                exit();
            } else {
                echo "Error while saving user.";
            }
        } 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.

php

Comparte el post y ayúdanos

También te puede interesar