Curso de PHP

Uso de bases de datos con PHP: conexión con PDO paso a paso

Por Víctor Peña · Publicado el

Uso de bases de datos con PHP

Hola, ¿cómo están? Continuando con el curso de PHP, hoy veremos cómo conectar nuestras aplicaciones con una base de datos. Es el paso que convierte un conjunto de páginas en un sistema de verdad.

¡Empecemos!

PDO: la forma correcta de conectarse

PHP ofrece varias formas de trabajar con bases de datos, pero hoy solo hay una recomendable: PDO (PHP Data Objects).

Sus ventajas frente a las alternativas:

  • Funciona con muchos gestores: MySQL, PostgreSQL, SQLite, SQL Server. Cambiar de base de datos apenas requiere tocar la cadena de conexión.
  • Soporta consultas preparadas, que es lo que te protege de la inyección SQL.
  • Lanza excepciones, así que se integra con lo que vimos en la lección de excepciones.

Si encuentras tutoriales que usan mysql_query(), ignóralos: esa extensión se eliminó de PHP hace años.

La conexión

<?php
declare(strict_types=1);

$host = '127.0.0.1';
$baseDatos = 'tienda';
$usuario = 'root';
$clave = '';

$dsn = "mysql:host=$host;dbname=$baseDatos;charset=utf8mb4";

$opciones = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

try {
    $pdo = new PDO($dsn, $usuario, $clave, $opciones);
} catch (PDOException $e) {
    error_log("Error de conexión: " . $e->getMessage());
    exit("No se pudo conectar a la base de datos");
}

Esas tres opciones no son decorativas, así que vale la pena entenderlas:

  • ERRMODE_EXCEPTION hace que los errores lancen excepciones en vez de fallar en silencio. Sin esto, un error de SQL puede pasar desapercibido.
  • FETCH_ASSOC devuelve los resultados como arrays asociativos, no duplicados con índices numéricos.
  • EMULATE_PREPARES => false usa las consultas preparadas reales del motor, que es lo seguro.

Y el charset=utf8mb4 evita que las tildes y las eñes se conviertan en símbolos raros.

Nunca pongas las credenciales en el código

Como advertimos en la lección de constantes: esos datos no deben estar escritos en un archivo que va al repositorio.

Ponlos en un archivo aparte, incluido en el .gitignore:

<?php
// config/database.php  ← este archivo NO se sube
return [
    'host' => '127.0.0.1',
    'nombre' => 'tienda',
    'usuario' => 'root',
    'clave' => '',
];
<?php
$config = require __DIR__ . '/config/database.php';

Consultar datos

Una consulta simple

<?php
$sentencia = $pdo->query("SELECT id, nombre, precio FROM productos");
$productos = $sentencia->fetchAll();

foreach ($productos as $producto) {
    echo $producto['nombre'] . ": " . $producto['precio'] . "\n";
}

query() solo debe usarse cuando la consulta no tiene datos variables. En cuanto entra un valor de fuera, hay que preparar.

Consultas preparadas

Esta es la parte más importante de la lección.

<?php
$sentencia = $pdo->prepare(
    "SELECT id, nombre, precio FROM productos WHERE categoria = ? AND precio < ?"
);
$sentencia->execute(['accesorios', 500]);

$productos = $sentencia->fetchAll();

O con parámetros con nombre, que se leen mejor cuando son varios:

<?php
$sentencia = $pdo->prepare(
    "SELECT * FROM productos WHERE categoria = :categoria AND precio < :maximo"
);
$sentencia->execute([
    'categoria' => 'accesorios',
    'maximo' => 500,
]);

Por qué esto importa tanto

Mira lo que pasa si concatenas directamente:

<?php
// NUNCA HAGAS ESTO
$id = $_GET['id'];
$pdo->query("SELECT * FROM usuarios WHERE id = $id");

Si alguien visita tu página con ?id=1 OR 1=1, la consulta se convierte en:

SELECT * FROM usuarios WHERE id = 1 OR 1=1

Y devuelve todos los usuarios. Con un poco más de creatividad se pueden borrar tablas o extraer contraseñas. Se llama inyección SQL y sigue siendo una de las vulnerabilidades más explotadas.

Las consultas preparadas lo evitan porque el motor recibe la estructura y los datos por separado: lo que llega como valor nunca puede convertirse en instrucción.

La regla es absoluta: si un dato viene de fuera —formulario, URL, cookie, API—, va como parámetro. Sin excepciones.

Obtener resultados

<?php
$sentencia->fetchAll();     // todas las filas
$sentencia->fetch();        // una fila, o false si no hay más
$sentencia->fetchColumn();  // un solo valor
$sentencia->rowCount();     // filas afectadas por INSERT/UPDATE/DELETE

Para un solo registro:

<?php
$sentencia = $pdo->prepare("SELECT * FROM productos WHERE id = :id");
$sentencia->execute(['id' => 5]);
$producto = $sentencia->fetch();

if ($producto === false) {
    throw new RuntimeException("Producto no encontrado");
}

Con muchos resultados, recorrer en lugar de traer todo a memoria:

<?php
$sentencia = $pdo->query("SELECT * FROM productos");

while ($producto = $sentencia->fetch()) {
    echo $producto['nombre'];
}

Es el mismo principio de los generadores que vimos en iteración de objetos: no cargues un millón de filas si puedes procesarlas de una en una.

Insertar, actualizar y eliminar

<?php
// INSERT
$sentencia = $pdo->prepare(
    "INSERT INTO productos (nombre, precio, stock) VALUES (:nombre, :precio, :stock)"
);
$sentencia->execute([
    'nombre' => 'Teclado mecánico',
    'precio' => 450,
    'stock' => 10,
]);

$nuevoId = $pdo->lastInsertId();

// UPDATE
$sentencia = $pdo->prepare("UPDATE productos SET precio = :precio WHERE id = :id");
$sentencia->execute(['precio' => 399, 'id' => $nuevoId]);

echo $sentencia->rowCount() . " fila actualizada";

// DELETE
$sentencia = $pdo->prepare("DELETE FROM productos WHERE id = :id");
$sentencia->execute(['id' => $nuevoId]);

En el DELETE, fíjate siempre en que exista el WHERE. Sin él, borras la tabla entera.

Transacciones

Cuando varias operaciones deben completarse todas o ninguna:

<?php
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE cuentas SET saldo = saldo - :monto WHERE id = :id")
        ->execute(['monto' => 500, 'id' => 1]);

    $pdo->prepare("UPDATE cuentas SET saldo = saldo + :monto WHERE id = :id")
        ->execute(['monto' => 500, 'id' => 2]);

    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    error_log("Transferencia fallida: " . $e->getMessage());
    throw new RuntimeException("No se pudo completar la transferencia");
}

El caso clásico: una transferencia bancaria. Si el segundo UPDATE falla, el rollBack() deshace el primero y el dinero no desaparece.

Un ejemplo práctico

Juntemos todo en una clase, aplicando el encapsulamiento y las excepciones del curso:

<?php
declare(strict_types=1);

class RepositorioProductos
{
    public function __construct(
        private PDO $pdo,
    ) {}

    public function buscarPorId(int $id): array
    {
        $sentencia = $this->pdo->prepare("SELECT * FROM productos WHERE id = :id");
        $sentencia->execute(['id' => $id]);

        $producto = $sentencia->fetch();

        if ($producto === false) {
            throw new RuntimeException("No existe el producto $id");
        }

        return $producto;
    }

    public function buscarPorCategoria(string $categoria): array
    {
        $sentencia = $this->pdo->prepare(
            "SELECT * FROM productos WHERE categoria = :categoria ORDER BY precio"
        );
        $sentencia->execute(['categoria' => $categoria]);

        return $sentencia->fetchAll();
    }

    public function crear(string $nombre, float $precio, int $stock): int
    {
        $sentencia = $this->pdo->prepare(
            "INSERT INTO productos (nombre, precio, stock)
             VALUES (:nombre, :precio, :stock)"
        );

        $sentencia->execute([
            'nombre' => $nombre,
            'precio' => $precio,
            'stock' => $stock,
        ]);

        return (int) $this->pdo->lastInsertId();
    }
}

$repositorio = new RepositorioProductos($pdo);
$id = $repositorio->crear("Monitor 27\"", 1200, 5);
print_r($repositorio->buscarPorId($id));

Fíjate en que la conexión se recibe por constructor, tal como vimos en interfaces. La clase no sabe de dónde sale, y eso permite sustituirla en pruebas.

Este patrón se llama repositorio, y es la base de lo que hacen los ORM de los frameworks.

Errores comunes

  • Concatenar variables en el SQL. Es la puerta a la inyección SQL.
  • No activar ERRMODE_EXCEPTION y quedarte sin saber por qué una consulta no hizo nada.
  • Guardar credenciales en el código.
  • Olvidar el WHERE en un UPDATE o un DELETE.
  • Traer todo con fetchAll() cuando la tabla tiene millones de filas.
  • Conectarse dentro de un bucle. Una conexión por petición es suficiente.

Para cerrar

Con PDO y consultas preparadas ya puedes construir aplicaciones reales. Si te llevas una sola regla de esta lección, que sea esta: todo dato que venga de fuera va como parámetro, nunca concatenado.

En la siguiente lección veremos los frameworks, que es el paso natural cuando el proyecto crece.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp