1. El problema del código espagueti
Los primeros scripts de PHP suelen mezclarlo todo en un mismo fichero: la consulta a la base de datos, las comprobaciones, los cálculos y el HTML, intercalados. Funciona mientras la página es pequeña, pero en cuanto crece aparecen los problemas: la misma consulta copiada en diez sitios, un cambio de diseño que obliga a tocar la lógica, y un código que nadie se atreve a modificar. A esto se le llama cariñosamente código espagueti.
La solución es separar responsabilidades: que cada parte del código se ocupe de una sola cosa. Para eso PHP ofrece programación orientada a objetos, y sobre ella se construye el patrón más usado en aplicaciones web: Modelo-Vista-Controlador.
2. Programación orientada a objetos en PHP
Si ya conoces la POO de Java, la de PHP te resultará muy familiar: clases, visibilidad (public, protected, private), herencia, interfaces, clases abstractas y excepciones. PHP 8 añadió además varias comodidades que hacen las clases mucho más cortas:
1<?php
2declare(strict_types=1);
3
4namespace App\Modelo;
5
6use InvalidArgumentException;
7
8final class Producto
9{
10 public function __construct(
11 public readonly ?int $id,
12 public readonly string $nombre,
13 public readonly float $precio,
14 private int $stock = 0,
15 ) {
16 if ($precio < 0) {
17 throw new InvalidArgumentException('El precio no puede ser negativo');
18 }
19 }
20
21 public function hayStock(int $cantidad = 1): bool
22 {
23 return $this->stock >= $cantidad;
24 }
25
26 public function retirar(int $cantidad): void
27 {
28 if (!$this->hayStock($cantidad)) {
29 throw new StockInsuficiente($this->nombre);
30 }
31 $this->stock -= $cantidad;
32 }
33}| Característica | Qué aporta |
|---|---|
| Promoción de propiedades en el constructor | Declarar y asignar las propiedades en la propia lista de parámetros |
| readonly | La propiedad solo se asigna una vez: objetos inmutables sin escribir getters |
| Tipos en propiedades, parámetros y retorno | Errores detectados al momento en lugar de datos corruptos |
| enum | Un conjunto cerrado de valores: enum Estado: string { case Pendiente = 'pendiente'; … } |
| Interfaces y traits | Contratos que cumplen varias clases, y bloques de código reutilizables entre clases |
| Espacios de nombres (namespace) | Organizar las clases en carpetas sin choques de nombres |
Composer y el autoload
Composer es el gestor de dependencias de PHP, el equivalente a npm o Maven. Además de instalar librerías, genera un autoloader: siguiendo el estándar PSR-4, relaciona cada espacio de nombres con una carpeta y carga cada clase la primera vez que se usa. Así desaparecen las listas interminables de require.
1{
2 "require": { "php": ">=8.2", "twig/twig": "^3.0" },
3 "autoload": { "psr-4": { "App\\": "src/" } }
4}3. El patrón Modelo-Vista-Controlador
MVC divide la aplicación en tres papeles. Lo importante no es el nombre de las carpetas, sino la regla que hay detrás: cada parte solo conoce lo que necesita. La vista no sabe de bases de datos, el modelo no sabe de HTML y el controlador coordina sin hacer el trabajo de los otros dos.
MVC en una biblioteca
- Todas las peticiones llegan a un único fichero,
public/index.php, el controlador frontal. - El router mira el método y la ruta (
GET /productos/7) y elige qué controlador y qué método ejecutar. - El controlador lee los datos de la petición, pide al modelo lo que necesita y elige una vista.
- El modelo aplica las reglas de negocio y accede a los datos (normalmente a través de un repositorio).
- La vista recibe los datos ya preparados y genera el HTML (o el JSON).
1final class ProductoControlador
2{
3 public function __construct(
4 private ProductoRepositorio $productos,
5 private Vista $vista,
6 ) {}
7
8 public function mostrar(int $id): string
9 {
10 $producto = $this->productos->buscar($id);
11 if ($producto === null) {
12 http_response_code(404);
13 return $this->vista->render('errores/404');
14 }
15 return $this->vista->render('productos/ficha', ['producto' => $producto]);
16 }
17}1<h1><?= e($producto->nombre) ?></h1>
2<p class="precio"><?= number_format($producto->precio, 2, ',', '.') ?> €</p>
3<?php if ($producto->hayStock()): ?>
4 <form method="post" action="/carrito"><button>Añadir al carrito</button></form>
5<?php else: ?>
6 <p>Agotado</p>
7<?php endif ?>Fíjate en que la plantilla no hace consultas ni cálculos de negocio: solo muestra. La función e() es un atajo habitual para htmlspecialchars. Motores de plantillas como Twig (Symfony) o Blade (Laravel) van más allá: escapan todo automáticamente, permiten heredar un diseño común y prohíben meter lógica compleja en la vista.
4. Frameworks: MVC ya construido
Escribir tu propio router, tu sistema de plantillas y tu capa de datos es un ejercicio excelente para entender cómo funcionan las cosas. En un proyecto real, en cambio, se usa un framework que ya resuelve todo eso y además la validación, la autenticación, las migraciones de base de datos, las colas o los tests.
1Route::get('/productos/{producto}', [ProductoController::class, 'show']);
2
3// app/Http/Controllers/ProductoController.php
4public function show(Producto $producto)
5{
6 return view('productos.ficha', ['producto' => $producto]); // Laravel busca el producto por su id
7}| Framework | Filosofía | Ideal para |
|---|---|---|
| Laravel | Productividad: convenciones, Eloquent, Blade y un ecosistema enorme | La mayoría de aplicaciones; el más demandado en España |
| Symfony | Componentes desacoplados y configurables, Doctrine y Twig | Proyectos grandes y de larga duración |
| Slim / Lumen | Microframeworks: router y poco más | APIs pequeñas |
Aprende primero sin framework
5. Retos
El primer reto es un modelo que protege sus propias reglas; el segundo, el corazón de cualquier motor de plantillas: sustituir variables escapando su contenido.
Retos de PHP en el IDE
Escribe el código que falta y pulsa Ejecutar tests: se ejecuta con PHP 8.3 real y se prueba con varios casos, algunos ocultos.
Completa la clase Producto: el constructor usa promoción de propiedades (string $nombre, float $precio, int $stock = 0) y lanza InvalidArgumentException con «El nombre es obligatorio», «El precio no puede ser negativo» o «El stock no puede ser negativo». precioConIva() devuelve el precio con el 21 %. Al convertirlo en texto (__toString) debe dar «Teclado: 24,99 € (30,24 € con IVA) · stock 5». El programa ya lee líneas «nombre;precio;stock» y muestra el producto o «Error: mensaje».
Separar la presentación de la lógica es la base de MVC. Completa render($plantilla, $datos): sustituye cada {{ clave }} por el valor de $datos escapado con htmlspecialchars, y {{ clave|raw }} por el valor sin escapar. Las claves pueden tener puntos para entrar en arrays anidados (usuario.nombre). Si una clave no existe, se sustituye por una cadena vacía. La primera línea de la entrada son los datos en JSON y el resto la plantilla.