Clases y objetos
La Programación Orientada a Objetos organiza el código en torno a objetos: estructuras que combinan datos (propiedades) y comportamiento (métodos). Una clase es el «plano»; cada objeto es una «instancia» concreta construida a partir de ese plano.
1.1 · Tu primera clase
class Usuario {
// propiedades con nivel de acceso
public $nombre;
private $clave;
// constructor: se ejecuta al crear el objeto
public function __construct($nombre, $clave) {
$this->nombre = $nombre;
$this->clave = $clave;
}
// método público
public function saludar() {
return "Hola, soy " . $this->nombre;
}
}
// crear una instancia (objeto)
$u = new Usuario("Raúl", "secreto");
echo $u->saludar(); // Hola, soy Raúl
echo $u->nombre; // público: accesible
// echo $u->clave; ❌ privado: fuera de la clase no se puede
1.2 · Modificadores de acceso y getters
public (accesible desde fuera), private (solo dentro de la clase) y
protected (dentro de la clase y sus herederas). Para leer propiedades privadas sin exponerlas
directamente, se usan getters/setters:
class CuentaBancaria {
private $saldo = 0;
public function getSaldo() {
return $this->saldo;
}
public function ingresar($cantidad) {
if ($cantidad <= 0) {
throw new InvalidArgumentException("Cantidad inválida");
}
$this->saldo += $cantidad;
}
}
private + métodos public = encapsulación:
el objeto controla cómo cambia su propio estado y evitas que se corrompa desde fuera.Herencia, interfaces y traits
2.1 · Herencia con extends
Una clase puede heredar de otra con extends, reutilizando sus métodos y
añadiendo o sobrescribiendo (override) lo suyo:
class Animal {
protected $nombre;
public function sonido() {
return "...";
}
}
class Perro extends Animal {
public function sonido() {
return "Guau";
}
}
$p = new Perro();
echo $p->sonido(); // Guau
2.2 · Interfaces: contratos
Una interfaz declara métodos que la clase debe implementar. PHP no tiene herencia múltiple, pero una clase sí puede implementar varias interfaces:
interface Loggeable {
public function loguear($mensaje): void;
}
class Pedido implements Loggeable {
public function loguear($mensaje): void {
error_log($mensaje);
}
}
2.3 · Traits: reutilizar sin herencia
Un trait es un bloque de métodos reutilizable que se «inyecta» en varias clases, ideal para compartir lógica común que no encaja en una jerarquía de herencia:
trait Formateable {
public function precioMostrable($precio) {
return number_format($precio, 2) . " €";
}
}
class Producto {
use Formateable;
public $precio = 19.9;
}
$prod = new Producto();
echo $prod->precioMostrable($prod->precio); // "19.90 €"
PDO y conexión segura a MySQL
PDO (PHP Data Objects) es la capa moderna para hablar con bases de datos. A diferencia
de la vieja mysqli, PDO usa sentencias preparadas que te protegen de la
inyección SQL de forma natural. Para este bloque activa MySQL en XAMPP.
3.1 · Conectar y crear la base de datos
$dsn = "mysql:host=localhost;dbname=tienda;charset=utf8mb4";
$usuario = "root";
$clave = ""; // XAMPP por defecto no tiene contraseña
try {
$pdo = new PDO($dsn, $usuario, $clave, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
echo "Conectado\n";
} catch (PDOException $e) {
echo "Error de conexión: " . $e->getMessage() . "\n";
exit;
}
ATTR_ERRMODE => ERRMODE_EXCEPTION hace que los errores de SQL lancen
excepciones que capturas con try/catch, en lugar de fallos silenciosos.
FETCH_ASSOC devuelve cada fila como un array asociativo.3.2 · Sentencias preparadas (preventions de inyección)
Con un placeholder ? o :nombre y execute(),
PDO separa la consulta de los datos: el valor jamás se interpreta como SQL.
$sql = "SELECT * FROM productos WHERE precio > :minimo";
$stmt = $pdo->prepare($sql);
$stmt->execute(["minimo" => 20]);
$productos = $stmt->fetchAll();
CRUD completo con PDO
CRUD = Create, Read, Update, Delete: las cuatro operaciones básicas sobre cualquier tabla.
4.1 · Create (INSERT)
$stmt = $pdo->prepare(
"INSERT INTO productos (nombre, precio) VALUES (:nombre, :precio)"
);
$stmt->execute(["nombre" => "Ratón", "precio" => 9.99]);
// id del último insertado
$id = $pdo->lastInsertId();
4.2 · Read (SELECT)
// todas las filas
$filas = $pdo->query("SELECT * FROM productos")->fetchAll();
// una fila por id (con placeholder)
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id = ?");
$stmt->execute([1]);
$uno = $stmt->fetch();
foreach ($filas as $f) {
echo $f["nombre"] . ": " . $f["precio"] . "\n";
}
4.3 · Update y Delete
// UPDATE
$stmt = $pdo->prepare(
"UPDATE productos SET precio = :precio WHERE id = :id"
);
$stmt->execute(["precio" => 11.50, "id" => 1]);
// DELETE (siempre con WHERE: ¡cuidado con borrarlo todo!)
$stmt = $pdo->prepare("DELETE FROM productos WHERE id = :id");
$stmt->execute(["id" => 4]);
echo $stmt->rowCount() . " fila(s) afectadas\n";
UPDATE y DELETE valida que el usuario tenga permiso y usa
siempre condición WHERE. Nunca hagas DELETE FROM tabla sin filtro (borrarías todo).Seguridad web
Esta es una de las lecciones más importantes de todo el curso. Las cuatro amenazas que debes conocer y bloquear:
5.1 · Inyección SQL
Ocurre cuando un dato del usuario se mete literalmente en una consulta. La solución es siempre usar sentencias preparadas (ya lo viste en la lección 3).
// $id viene del usuario: ' OR '1'='1 ¡peligro!
$pdo->query("SELECT * FROM usuarios WHERE id = " . $_GET["id"]);
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE id = :id");
$stmt->execute(["id" => $_GET["id"]]);
5.2 · XSS (Cross-Site Scripting)
Si imprimes datos del usuario sin escapar, un atacante puede inyectar <script> que se
ejecute en el navegador de otras víctimas. Escapa SIEMPRE la salida con htmlspecialchars():
// $nombre viene del usuario y se va a pintar en HTML
echo htmlspecialchars($nombre, ENT_QUOTES, "UTF-8");
5.3 · CSRF (Cross-Site Request Forgery)
Un atacante puede hacer que un usuario ya logueado envíe formularios sin saberlo. La defensa clásica es un token CSRF único por sesión que va en cada formulario:
session_start();
if (empty($_SESSION["csrf"])) {
$_SESSION["csrf"] = bin2hex(random_bytes(32));
}
// en el formulario
echo '<input type="hidden" name="csrf" value="' . htmlspecialchars($_SESSION["csrf"]) . '">';
// al recibir el POST, validar
if (hash_equals($_SESSION["csrf"], $_POST["csrf"] ?? "")) {
echo "Petición válida\n";
} else {
echo "Token CSRF inválido\n";
}
5.4 · Hashing de contraseñas
NUNCA guardes contraseñas en claro. Usa password_hash() para almacenar y
password_verify() para comprobar. El hash incluye un salt y es resistente a fuerza bruta:
// al registrar
$hash = password_hash("secreto123", PASSWORD_DEFAULT);
// guarda $hash en la base de datos
// al iniciar sesión
if (password_verify("secreto123", $hash)) {
echo "Contraseña correcta\n";
} else {
echo "Contraseña incorrecta\n";
}
Composer y carga automática
Composer es el gestor de dependencias de PHP (el equivalente a npm en Node). Con él instalas librerías y generas una carga automática de tus propias clases.
6.1 · composer.json y PSR-4
{
"require": {
"php": ">=8.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Con PSR-4, la clase App\Producto se carga desde src/Producto.php
automáticamente. Tras crear el archivo ejecutas composer dump-autoload y ya puedes usarla
sin require manual:
6.2 · Usar el autoload
// único require que necesitas
require __DIR__ . "/vendor/autoload.php";
// ahora las clases App\... se cargan solas
$p = new App\Producto("Ratón", 9.99);
composer install dentro de tu
carpeta del proyecto (donde está composer.json). No lo confundas con el servidor: Composer no es
obligatorio para una web simple, pero sí para proyectos reales y para usar el ecosistema de librerías.✍️ Los 16 ejercicios del bloque
Crea la carpeta php-b3/ dentro de htdocs. Los ejercicios de POO se pueden
probar por línea de comandos (php archivo.php) o desde el navegador; los de base de datos
necesitan MySQL activado y una base tienda creada (p. ej. desde phpMyAdmin).
Sección A · POO
1 Clase Producto
Crea la clase Producto con constructor y propiedades públicas $nombre y
$precio. Crea dos instancias, llama a un método formatear() que devuelva
"nombre — X,XX €" (con number_format) y muéstralos.
Ver solución
class Producto {
public $nombre;
public $precio;
public function __construct($nombre, $precio) {
$this->nombre = $nombre;
$this->precio = $precio;
}
public function formatear() {
return $this->nombre . " — " .
number_format($this->precio, 2) . " €";
}
}
$p1 = new Producto("Ratón", 9.99);
$p2 = new Producto("Monitor", 129);
echo $p1->formatear() . "\n";
echo $p2->formatear() . "\n";
2 Encapsulación con CuentaBancaria
Crea CuentaBancaria con private $saldo, un constructor que acepte el saldo
inicial, y métodos getSaldo(), ingresar($n) y retirar($n). En
retirar, lanza una excepción si $n > $saldo o es negativo. Prueba el flujo y muestra el
saldo final.
Ver solución
class CuentaBancaria {
private $saldo;
public function __construct($saldoInicial = 0) {
$this->saldo = $saldoInicial;
}
public function getSaldo() {
return $this->saldo;
}
public function ingresar($n) {
if ($n <= 0) {
throw new InvalidArgumentException("Cantidad inválida");
}
$this->saldo += $n;
}
public function retirar($n) {
if ($n <= 0 || $n > $this->saldo) {
throw new InvalidArgumentException("Fondos insuficientes");
}
$this->saldo -= $n;
}
}
$c = new CuentaBancaria(100);
$c->ingresar(50);
$c->retirar(30);
echo "Saldo: " . $c->getSaldo() . "\n"; // 120
3 Herencia Animal
Crea la clase base Animal con un método sonido() que devuelva
"...", y las clases Perro y Gato que lo sobrescriban con
«Guau» y «Miau». Instancia cada una y muestra sus sonidos.
Ver solución
class Animal {
public function sonido() {
return "...";
}
}
class Perro extends Animal {
public function sonido() {
return "Guau";
}
}
class Gato extends Animal {
public function sonido() {
return "Miau";
}
}
echo (new Perro())->sonido() . "\n";
echo (new Gato())->sonido() . "\n";
4 Interfaz Pagable
Define la interfaz Pagable con un método importe(): float. Crea las clases
LineaDePedido (con precio y cantidad) y TarifaPlana que la implementen
devolviendo su importe. Instancia ambas, invoca importe() en cada una y muéstrala.
Ver solución
interface Pagable {
public function importe(): float;
}
class LineaDePedido implements Pagable {
public function __construct(private float $precio, private int $cantidad) {}
public function importe(): float {
return $this->precio * $this->cantidad;
}
}
class TarifaPlana implements Pagable {
public function __construct(private float $precio) {}
public function importe(): float {
return $this->precio;
}
}
echo (new LineaDePedido(9.99, 2))->importe() . "\n"; // 19.98
echo (new TarifaPlana(30))->importe() . "\n"; // 30
5 Trait Formateable
Crea un trait Formateable con un método precioMostrable() y úsalo en dos
clases distintas (Producto y Servicio) que tengan una propiedad $precio.
Muestra el precio formateado de ambas.
Ver solución
trait Formateable {
public function precioMostrable() {
return number_format($this->precio, 2) . " €";
}
}
class Producto {
use Formateable;
public $precio = 19.9;
}
class Servicio {
use Formateable;
public $precio = 50;
}
echo (new Producto())->precioMostrable() . "\n";
echo (new Servicio())->precioMostrable() . "\n";
Sección B · PDO y MySQL
6 Conectar con PDO
Escribe un conexion.php que conecte a mysql:host=localhost;dbname=tienda
con try/catch, mostrando «Conectado» en éxito y el mensaje de error en fallo. Crea antes la
base tienda desde phpMyAdmin o con CREATE DATABASE tienda.
Ver solución
$dsn = "mysql:host=localhost;dbname=tienda;charset=utf8mb4";
try {
$pdo = new PDO($dsn, "root", "", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
echo "Conectado\n";
} catch (PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
7 Crear la tabla productos
Mediante CREATE TABLE IF NOT EXISTS (ejecutado con $pdo->exec()) crea una
tabla productos con id (INT AUTO_INCREMENT PRIMARY KEY), nombre
(VARCHAR) y precio (DECIMAL). Muestra «Tabla creada».
Ver solución
$pdo->exec("CREATE TABLE IF NOT EXISTS productos (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
echo "Tabla creada\n";
8 INSERT y lastInsertId
Inserta tres productos con prepare() + execute(). Al insertar cada uno,
muestra su id con lastInsertId(). Usa placeholders con nombre (:nombre,
:precio).
Ver solución
$stmt = $pdo->prepare(
"INSERT INTO productos (nombre, precio) VALUES (:nombre, :precio)"
);
foreach ([
["Ratón", 9.99], ["Teclado", 24.5], ["Monitor", 129],
] as $p) {
$stmt->execute(["nombre" => $p[0], "precio" => $p[1]]);
echo "Insertado id " . $pdo->lastInsertId() . "\n";
}
9 SELECT y fetchAll
Consulta SELECT * FROM productos y recorre las filas mostrando
«id: nombre — precio €». Después haz una consulta con placeholder que traiga solo los productos con
precio > :min (p. ej. 20) y muéstralos.
Ver solución
$filas = $pdo->query("SELECT * FROM productos")->fetchAll();
foreach ($filas as $f) {
echo $f["id"] . ": " . $f["nombre"] . " — " . $f["precio"] . " €\n";
}
$stmt = $pdo->prepare("SELECT * FROM productos WHERE precio > :min");
$stmt->execute(["min" => 20]);
$caros = $stmt->fetchAll();
echo "Caros: " . count($caros) . "\n";
10 UPDATE y DELETE
Actualiza el precio del producto con id 1 a 11.50 con UPDATE ... WHERE id = :id
y muestra rowCount(). Después borra el producto con id 3 con DELETE ... WHERE id = :id
y muestra las filas afectadas.
Ver solución
$stmt = $pdo->prepare("UPDATE productos SET precio = :precio WHERE id = :id");
$stmt->execute(["precio" => 11.5, "id" => 1]);
echo "Update: " . $stmt->rowCount() . " fila(s)\n";
$stmt = $pdo->prepare("DELETE FROM productos WHERE id = :id");
$stmt->execute(["id" => 3]);
echo "Delete: " . $stmt->rowCount() . " fila(s)\n";
Sección C · Seguridad
11 Demonstrar inyección y arreglarla
Escribe una versión VULNERABLE que concatene $_GET["id"] en una consulta y una comentada
que demuestre que ' OR '1'='1 rompería la lógica. Después escribe la versión segura con
prepared statement. Comenta la diferencia.
Ver solución
// ❌ VULNERABLE: el usuario podría enviar: id=1 OR 1=1
// $pdo->query("SELECT * FROM productos WHERE id = " . $_GET["id"]);
// ✅ SEGURO: prepared statement con placeholder
$stmt = $pdo->prepare("SELECT * FROM productos WHERE id = :id");
$stmt->execute(["id" => $_GET["id"]]);
$fila = $stmt->fetch();
12 Escapar con htmlspecialchars
Ten un $nombre con un valor aparentemente inofensivo pero que contenga
<script>alert('xss')</script>. Muestra la versión «cruda» y la escapada con
htmlspecialchars($nombre, ENT_QUOTES, "UTF-8") para ver la diferencia. Comenta cuál se
mostraría de forma segura en el navegador.
Ver solución
$nombre = "<script>alert('xss')</script>";
echo "Crudo: " . $nombre . "\n";
echo "Escapado: " . htmlspecialchars($nombre, ENT_QUOTES, "UTF-8") . "\n";
// El escapado convierte < > y ' en entidades, por lo que el navegador
// lo muestra como texto y NO lo ejecuta. Siempre escapa la salida.
13 Token CSRF
Crea un script con session_start(), genera (si no existe) un token en
$_SESSION["csrf"] con random_bytes() + bin2hex(), muéstralo en un
campo oculto de un formulario, y al recibir el POST valida con hash_equals() mostrando
«Válido» o «Inválido».
Ver solución
session_start();
if (empty($_SESSION["csrf"])) {
$_SESSION["csrf"] = bin2hex(random_bytes(32));
}
$enviado = $_SERVER["REQUEST_METHOD"] === "POST";
$valido = hash_equals($_SESSION["csrf"], $_POST["csrf"] ?? "");
?>
<form method="post">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION["csrf"]) ?>">
<button>Enviar</button>
</form>
<p><?= $enviado ? ($valido ? "Válido" : "Inválido") : "" ?></p>
14 Hash de contraseña
Usa password_hash("secreto123", PASSWORD_DEFAULT), muestra el hash, y comprueba con
password_verify() la contraseña correcta y una incorrecta. Comenta por qué NO debería
guardarse nunca en claro.
Ver solución
$clave = "secreto123";
$hash = password_hash($clave, PASSWORD_DEFAULT);
echo "Hash: $hash\n";
var_dump(password_verify("secreto123", $hash)); // true
var_dump(password_verify("incorrecta", $hash)); // false
// El hash con salt hace cada resultado único y difícil de revertir:
// aunque la BD se filtre, la contraseña en claro no queda expuesta.
Sección D · Composer
15 composer.json con PSR-4
Crea un composer.json con php >= 8.0 y autoload
"App\\": "src/" (PSR-4). Crea src/Producto.php con la clase
App\Producto y un uso.php que cargue vendor/autoload.php e
instancie la clase. Pista: recuerda el namespace App; en la clase.
Ver solución
// composer.json
{
"require": { "php": ">=8.0" },
"autoload": {
"psr-4": { "App\\": "src/" }
}
}
// src/Producto.php
<?php
namespace App;
class Producto {
public function __construct(public string $nombre) {}
}
?>
// uso.php — ejecutar: composer dump-autoload y luego php uso.php
<?php
require __DIR__ . "/vendor/autoload.php";
$p = new App\Producto("Ratón");
echo $p->nombre . "\n";
16 🏆 Proyecto integrador: «crud-seguro.php»
Une TODO el bloque en una aplicación que genere/lea productos en la tabla productos de
forma segura:
- Conecta con PDO (con
try/catchyERRMODE_EXCEPTION). - Define al menos una clase con constructor y algún método (p. ej.
Producto). - Usa sentencias preparadas (
prepare()+execute()) con placeholders. - Implementa al menos INSERT y SELECT (CRUD básico).
- Recorre los resultados con foreach.
- Escapa la salida con htmlspecialchars().
- Usa password_hash() o password_verify().
- NO concatenes variables directamente en SQL (nada de
query("... " . $dato)).
Este ejercicio es el reto del laboratorio del examen: la validación automática comprobará estos
criterios sobre el contenido de tus etiquetas <?php ... ?>.
📝 Examen del Bloque 3
Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con checklist.
Parte B · Reto práctico final
Construye el ejercicio 16 («crud-seguro.php») sin mirar soluciones y valida tú mismo/a estos criterios. Mínimo para aprobar: 6 de 7 casillas.
Cumplidos: 0 / 7