PDO (PHP Data Objects) es una extensión que permite acceder a bases de datos de forma uniforme. Ventajas frente a mysqli:
<?php
// config.php — Configuración de la base de datos
$host = "localhost";
$dbname = "tienda";
$username = "root";
$password = "";
$charset = "utf8mb4";
try {
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Lanzar excepciones
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // Array asociativo
PDO::ATTR_EMULATE_PREPARES => false, // Prepared statements reales
];
$pdo = new PDO($dsn, $username, $password, $options);
echo "Conectado a $dbname";
} catch (PDOException $e) {
// En producción, no mostrar detalles del error al usuario
error_log("Error de conexión: " . $e->getMessage());
die("Error al conectar con la base de datos");
}
?>
Los prepared statements separan la consulta SQL de los datos, eliminando el riesgo de inyección SQL:
<?php
// MAL: inyección SQL directa (¡NUNCA HAGAS ESTO!)
// $sql = "SELECT * FROM usuarios WHERE email = '" . $_POST["email"] . "'";
// BIEN: prepared statement con marcadores (?)
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE email = ?");
$stmt->execute([$_POST["email"]]);
$usuario = $stmt->fetch();
// BIEN: prepared statement con marcadores nombrados (:param)
$stmt = $pdo->prepare("SELECT * FROM productos WHERE precio > :min AND precio < :max");
$stmt->execute(["min" => 10, "max" => 100]);
$productos = $stmt->fetchAll();
?>
<?php
function insertar_producto(PDO $pdo, string $nombre, float $precio, int $stock): int {
$sql = "INSERT INTO productos (nombre, precio, stock, creado_en) VALUES (?, ?, ?, NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([$nombre, $precio, $stock]);
return (int) $pdo->lastInsertId(); // Devuelve el ID generado
}
// Uso:
$id = insertar_producto($pdo, "Teclado mecánico", 89.99, 15);
echo "Producto creado con ID: $id";
?>
<?php
// Obtener todos los productos
function obtener_productos(PDO $pdo): array {
$stmt = $pdo->query("SELECT * FROM productos ORDER BY creado_en DESC");
return $stmt->fetchAll();
}
// Obtener un producto por ID
function obtener_producto_por_id(PDO $pdo, int $id): ?array {
$sql = "SELECT * FROM productos WHERE id = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$id]);
$producto = $stmt->fetch();
return $producto ?: null; // Devuelve null si no existe
}
// Mostrar productos en una tabla HTML
$productos = obtener_productos($pdo);
?>
<table>
<thead>
<tr><th>ID</th><th>Nombre</th><th>Precio</th><th>Stock</th><th>Acciones</th></tr>
</thead>
<tbody>
<?php foreach ($productos as $p): ?>
<tr>
<td><?= $p["id"] ?></td>
<td><?= htmlspecialchars($p["nombre"]) ?></td>
<td><?= number_format($p["precio"], 2) ?>€</td>
<td><?= $p["stock"] ?></td>
<td>
<a href="editar.php?id=<?= $p["id"] ?>">Editar</a>
<a href="eliminar.php?id=<?= $p["id"] ?>" onclick="return confirm('¿Seguro?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
function actualizar_producto(PDO $pdo, int $id, string $nombre, float $precio, int $stock): bool {
$sql = "UPDATE productos SET nombre = ?, precio = ?, stock = ? WHERE id = ?";
$stmt = $pdo->prepare($sql);
return $stmt->execute([$nombre, $precio, $stock, $id]);
}
// Formulario de edición
$id = (int) ($_GET["id"] ?? 0);
$producto = obtener_producto_por_id($pdo, $id);
if (!$producto) {
die("Producto no encontrado");
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$nombre = trim($_POST["nombre"] ?? "");
$precio = (float) ($_POST["precio"] ?? 0);
$stock = (int) ($_POST["stock"] ?? 0);
if (actualizar_producto($pdo, $id, $nombre, $precio, $stock)) {
header("Location: listar.php?mensaje=Actualizado correctamente");
exit;
}
}
?>
<form method="POST">
<label>Nombre: <input type="text" name="nombre" value="<?= htmlspecialchars($producto["nombre"]) ?>" required></label><br>
<label>Precio: <input type="number" step="0.01" name="precio" value="<?= $producto["precio"] ?>" required></label><br>
<label>Stock: <input type="number" name="stock" value="<?= $producto["stock"] ?>" required></label><br>
<button type="submit">Guardar cambios</button>
</form>
<?php
function eliminar_producto(PDO $pdo, int $id): bool {
$sql = "DELETE FROM productos WHERE id = ?";
$stmt = $pdo->prepare($sql);
return $stmt->execute([$id]);
}
// eliminar.php
if (isset($_GET["id"])) {
$id = (int) $_GET["id"];
if (eliminar_producto($pdo, $id)) {
header("Location: listar.php?mensaje=Producto eliminado");
exit;
} else {
echo "Error al eliminar";
}
}
?>
<?php
function buscar_productos(PDO $pdo, string $termino): array {
$sql = "SELECT * FROM productos WHERE nombre LIKE ? OR descripcion LIKE ?";
$like = "%$termino%";
$stmt = $pdo->prepare($sql);
$stmt->execute([$like, $like]);
return $stmt->fetchAll();
}
// Búsqueda avanzada con filtros
function filtrar_productos(PDO $pdo, ?string $categoria, ?float $precio_min, ?float $precio_max): array {
$sql = "SELECT * FROM productos WHERE 1=1";
$params = [];
if ($categoria) {
$sql .= " AND categoria = ?";
$params[] = $categoria;
}
if ($precio_min !== null) {
$sql .= " AND precio >= ?";
$params[] = $precio_min;
}
if ($precio_max !== null) {
$sql .= " AND precio <= ?";
$params[] = $precio_max;
}
$sql .= " ORDER BY precio ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
}
?>
<!-- Formulario de búsqueda -->
<form method="GET">
<input type="text" name="q" placeholder="Buscar productos..." value="<?= htmlspecialchars($_GET["q"] ?? "") ?>">
<button type="submit">Buscar</button>
</form>
<?php
if (isset($_GET["q"]) && trim($_GET["q"]) !== "") {
$resultados = buscar_productos($pdo, $_GET["q"]);
echo "<p>Se encontraron " . count($resultados) . " resultados</p>";
}
?>
<?php
function obtener_productos_paginados(PDO $pdo, int $pagina = 1, int $por_pagina = 10): array {
$offset = ($pagina - 1) * $por_pagina;
// Total de registros
$total = (int) $pdo->query("SELECT COUNT(*) FROM productos")->fetchColumn();
$total_paginas = (int) ceil($total / $por_pagina);
// Registros de la página actual
$sql = "SELECT * FROM productos ORDER BY creado_en DESC LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$por_pagina, $offset]);
$productos = $stmt->fetchAll();
return [
"productos" => $productos,
"total" => $total,
"pagina_actual" => $pagina,
"total_paginas" => $total_paginas,
"por_pagina" => $por_pagina,
];
}
$pagina = max(1, (int) ($_GET["pagina"] ?? 1));
$datos = obtener_productos_paginados($pdo, $pagina, 5);
$productos = $datos["productos"];
?>
<!-- Mostrar productos (igual que antes) -->
<!-- Paginación -->
<nav class="paginacion">
<?php if ($datos["pagina_actual"] > 1): ?>
<a href="?pagina=<?= $datos["pagina_actual"] - 1 ?>">« Anterior</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $datos["total_paginas"]; $i++): ?>
<a href="?pagina=<?= $i ?>" class="<?= $i === $datos["pagina_actual"] ? 'activa' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
<?php if ($datos["pagina_actual"] < $datos["total_paginas"]): ?>
<a href="?pagina=<?= $datos["pagina_actual"] + 1 ?>">Siguiente »</a>
<?php endif; ?>
</nav>
<p>Página <?= $datos["pagina_actual"] ?> de <?= $datos["total_paginas"] ?>
(<?= $datos["total"] ?> productos en total)</p>
Para operaciones que deben ejecutarse juntas (todo o nada):
<?php
try {
// Iniciar transacción
$pdo->beginTransaction();
// Descontar stock
$stmt1 = $pdo->prepare("UPDATE productos SET stock = stock - ? WHERE id = ? AND stock >= ?");
$stmt1->execute([$cantidad, $producto_id, $cantidad]);
if ($stmt1->rowCount() === 0) {
throw new Exception("Stock insuficiente");
}
// Registrar pedido
$stmt2 = $pdo->prepare("INSERT INTO pedidos (producto_id, cantidad, total) VALUES (?, ?, ?)");
$stmt2->execute([$producto_id, $cantidad, $precio * $cantidad]);
// Confirmar cambios
$pdo->commit();
echo "Pedido realizado con éxito";
} catch (Exception $e) {
// Revertir todos los cambios si algo falla
$pdo->rollBack();
error_log("Error en pedido: " . $e->getMessage());
echo "Error al procesar el pedido";
}
?>
Separación en Modelo, Vista y Controlador para organizar el código:
// Estructura de carpetas
// proyecto/
// ├── config/
// │ └── database.php # Conexión PDO
// ├── modelos/
// │ └── Producto.php # Consultas a la BD
// ├── controladores/
// │ └── ProductoController.php # Lógica de negocio
// ├── vistas/
// │ ├── listar.php # HTML (solo mostrar)
// │ └── formulario.php # HTML (formularios)
// └── index.php # Punto de entrada
<?php
// modelos/Producto.php — Modelo (acceso a datos)
namespace App\Modelos;
class Producto {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function obtenerTodos(): array {
$stmt = $this->pdo->query("SELECT * FROM productos ORDER BY id DESC");
return $stmt->fetchAll();
}
public function obtenerPorId(int $id): ?array {
$stmt = $this->pdo->prepare("SELECT * FROM productos WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch() ?: null;
}
public function crear(string $nombre, float $precio, int $stock): int {
$stmt = $this->pdo->prepare("INSERT INTO productos (nombre, precio, stock, creado_en) VALUES (?, ?, ?, NOW())");
$stmt->execute([$nombre, $precio, $stock]);
return (int) $this->pdo->lastInsertId();
}
public function actualizar(int $id, string $nombre, float $precio, int $stock): bool {
$stmt = $this->pdo->prepare("UPDATE productos SET nombre = ?, precio = ?, stock = ? WHERE id = ?");
return $stmt->execute([$nombre, $precio, $stock, $id]);
}
public function eliminar(int $id): bool {
$stmt = $this->pdo->prepare("DELETE FROM productos WHERE id = ?");
return $stmt->execute([$id]);
}
public function buscar(string $termino): array {
$like = "%$termino%";
$stmt = $this->pdo->prepare("SELECT * FROM productos WHERE nombre LIKE ? OR descripcion LIKE ?");
$stmt->execute([$like, $like]);
return $stmt->fetchAll();
}
}
?>
<?php
// controladores/ProductoController.php — Controlador (lógica)
namespace App\Controladores;
use App\Modelos\Producto;
class ProductoController {
private Producto $modelo;
public function __construct(Producto $modelo) {
$this->modelo = $modelo;
}
public function index(): void {
$productos = $this->modelo->obtenerTodos();
require __DIR__ . "/../vistas/listar.php";
}
public function crear(): void {
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$id = $this->modelo->crear(
trim($_POST["nombre"]),
(float) $_POST["precio"],
(int) $_POST["stock"]
);
header("Location: ?accion=ver&id=$id");
exit;
}
require __DIR__ . "/../vistas/formulario.php";
}
public function editar(int $id): void {
$producto = $this->modelo->obtenerPorId($id);
if (!$producto) {
die("Producto no encontrado");
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->modelo->actualizar(
$id,
trim($_POST["nombre"]),
(float) $_POST["precio"],
(int) $_POST["stock"]
);
header("Location: ?accion=index&msg=Actualizado");
exit;
}
require __DIR__ . "/../vistas/formulario.php";
}
public function eliminar(int $id): void {
$this->modelo->eliminar($id);
header("Location: ?accion=index&msg=Eliminado");
exit;
}
}
// index.php (punto de entrada)
require_once __DIR__ . "/config/database.php";
require_once __DIR__ . "/vendor/autoload.php";
use App\Modelos\Producto;
use App\Controladores\ProductoController;
$pdo = obtenerConexion();
$modelo = new Producto($pdo);
$controlador = new ProductoController($modelo);
// Enrutamiento simple
$accion = $_GET["accion"] ?? "index";
$id = (int) ($_GET["id"] ?? 0);
switch ($accion) {
case "crear":
$controlador->crear();
break;
case "editar":
$controlador->editar($id);
break;
case "eliminar":
$controlador->eliminar($id);
break;
case "ver":
$producto = $modelo->obtenerPorId($id);
require "vistas/detalle.php";
break;
default:
$controlador->index();
break;
}
?>
CREATE DATABASE tienda CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE tienda;
CREATE TABLE productos (
id INT PRIMARY KEY AUTO_INCREMENT,
nombre VARCHAR(150) NOT NULL,
descripcion TEXT,
precio DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
categoria VARCHAR(50),
creado_en DATETIME DEFAULT CURRENT_TIMESTAMP,
actualizado_en DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Datos de ejemplo
INSERT INTO productos (nombre, precio, stock, categoria) VALUES
('Portátil HP', 799.99, 10, 'informática'),
('Teclado RGB', 59.99, 25, 'periféricos'),
('Monitor 27"', 299.99, 8, 'informática'),
('Ratón inalámbrico', 34.99, 30, 'periféricos'),
('Auriculares Bluetooth', 89.99, 15, 'audio');
<?php
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
// Loggear el error real
error_log("DB Error: " . $e->getMessage());
// Mensaje genérico al usuario
die("Error de conexión. Inténtelo más tarde.");
}
// Capturar errores específicos de SQL
try {
$stmt = $pdo->prepare("INSERT INTO productos (nombre) VALUES (?)");
$stmt->execute(["Teclado"]);
} catch (PDOException $e) {
if ($e->getCode() == 23000) { // Código para violación de constraint
echo "Ese registro ya existe.";
} else {
throw $e; // Re-lanzar si es otro error
}
}
?>
tienda con la tabla productos del esquema anterior.