Ir al contenido principal

PHP y la base de datos

Qué hace PHP

PHP es el lenguaje que corre en el servidor. En el recorrido de una operación ocupa el lugar del medio, y ese lugar lo explica todo: es el único de los cuatro protagonistas que puede hablar con los dos lados. El navegador no puede conectarse a la base de datos —no tiene las credenciales ni debería tenerlas— y la base de datos no arma páginas HTML. Los dos se comunican con PHP, y PHP traduce.

Sus responsabilidades en cada una de las cuatro operaciones son siempre las mismas seis:

  1. Recibir los datos que llegaron del formulario.
  2. Validar que sean aceptables, sin confiar en el navegador.
  3. Preparar la consulta, con los valores aparte.
  4. Ejecutarla contra la base de datos.
  5. Leer la respuesta: las filas que devolvió, o si la operación salió bien.
  6. Decidir qué mostrar: la página siguiente, o los errores que hay que corregir.

Los seis pasos aparecieron en las cuatro páginas anteriores, con el código ya escrito para cada caso. Esta página explica el mecanismo que está por debajo, que es común a todos: cómo se abre la conexión, cómo se manda una consulta y cómo se lee lo que devuelve. Es el tema que conviene entender de una vez, porque a partir de acá el resto es siempre lo mismo. Y al final de la unidad está el ejemplo completo que usa todo esto junto: Login y sesiones.

La conexión

PHP no habla con la base de datos directamente: usa una capa intermedia llamada PDO (PHP Data Objects). En toda la materia el gestor es MariaDB —el que viene con XAMPP— y PDO tiene una ventaja que conviene saber desde el principio: ofrece la misma forma de trabajar con cualquier gestor. Cambiar de MariaDB a SQLite, a PostgreSQL o a SQL Server no implica aprender otra biblioteca: es la misma clase PDO, con las mismas funciones, y lo único que cambia es el DSN.

<?php

// Con MariaDB, el de la materia. El driver de PDO se sigue llamando "mysql"
$bd = new PDO('mysql:host=localhost;dbname=biblioteca;charset=utf8mb4', 'usuario', 'clave');

// Con SQLite el DSN es más corto: la base entera es un archivo
$bd = new PDO('sqlite:biblioteca.db');

// Que los errores paren el programa en lugar de pasar en silencio
$bd->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

El texto entre comillas se llama DSN y es lo único que cambia entre un gestor y otro. En MySQL dice cuatro cosas, separadas por punto y coma:

Parte Qué indica Ejemplo
mysql:Con qué gestor se habla.mysql, sqlite
host=Dónde está el servidor. localhost es la misma máquina.localhost
dbname=Qué base se va a usar.biblioteca
charset=Cómo se codifica el texto.utf8mb4

El charset=utf8mb4 parece un detalle y no lo es: es lo que hace que las eñes y los acentos se guarden bien. Olvidarlo produce el error más desconcertante de todos, porque no aparece nunca: los datos funcionan bien durante semanas y de golpe un apellido aparece como MartÃnez. Es el mismo problema que resuelve el <meta charset="utf-8"> en HTML, pero del otro lado del camino.

La segunda línea, la de ERRMODE_EXCEPTION, merece una explicación porque cambia el comportamiento por defecto. PDO, si no se le dice otra cosa, es silencioso: cuando una consulta falla, no avisa. Devuelve un resultado vacío y el programa sigue como si nada. Eso es lo peor que puede pasar, porque el error se manifiesta mucho después y en otro lugar —una página en blanco, un dato que no se guardó— sin ninguna pista de dónde está. Con ERRMODE_EXCEPTION, el error detiene el programa y se puede atrapar para reaccionar. Es la primera línea que se escribe después de conectarse, siempre.

Dos puntos más sobre esta conexión, porque es la parte que se escribe una sola vez y conviene dejar bien. El primero: va en su propio archivo. Se escribe en conexion.php y todas las páginas lo traen con require "conexion.php";, así los datos del servidor están en un solo lugar en vez de repetidos en cada archivo. El nombre de la variable también es una elección: acá se usa $bd y en el ejemplo de Login y sesiones se llama $conexionbd.

El segundo: las credenciales no van en el código de un sitio publicado. Un usuario y una contraseña dentro de un archivo .php terminan publicados en el repositorio, y de ahí en la copia de alguien, y de ahí en cualquier lado. En un sistema real van en un archivo de configuración aparte que no se sube. Además, esa conexión es la de la aplicación, así que su usuario no debería ser root, como se vio en la página del gestor online. En los ejemplos de esta unidad se escriben directo y se usa root porque son para probar en la propia computadora: tener todo a la vista en un archivo ayuda a entender qué se está configurando, y es una comodidad que no puede salir de ahí.

Consultas preparadas

Las operaciones del CRUD usan prepare() y execute() para enviar valores a MariaDB. El motivo y las dos formas de escribir los marcadores se explican en Inyección SQL y consultas preparadas, en la Unidad 3.

Leer el resultado

Después de ejecutar, hay que leer lo que la base devolvió. Y lo que devuelve depende de qué se pidió, así que hay un método distinto para cada caso:

Método Devuelve Se usa cuando…
fetchAll() Todas las filas, en una lista. Se van a mostrar todos los resultados, como en un listado.
fetch() Una fila por vez. Se recorre el resultado dentro de un while.
fetchColumn() Un solo valor. La consulta devuelve un único dato, como un COUNT(*).

Cada fila que devuelven fetchAll() y fetch() es un conjunto de valores identificados por el nombre de su columna. Es decir: si la consulta pidió nombre y correo, en el programa se los alcanza con $socio['nombre'] y $socio['correo']. Y ahí se cierra el círculo con una idea del listado: al servidor le importa el nombre de la columna, no su rótulo. Si en la consulta se pidiera SELECT nombre AS apellido_y_nombre, el programa tendría que usar esa clave nueva, $socio['apellido_y_nombre'].

// Con fetchAll(): todo el resultado en una variable
$socios = $sentencia->fetchAll();

foreach ($socios as $socio) {
    echo $socio['nombre'];         // se accede por el nombre de la columna
}

// Con fetch(): de a una fila, sin cargar todo en memoria
while ($socio = $sentencia->fetch()) {
    echo $socio['nombre'];
}

Saber si funcionó

Cuando la consulta no devuelve filas —porque inserta, modifica o borra—, la información sobre lo que pasó se pide de otra manera. Los dos métodos ya aparecieron en las páginas anteriores; acá se juntan para tener el panorama completo:

Operación rowCount() devuelve… lastInsertId() devuelve…
Alta (INSERT)1 si se insertó la fila.El id que le asignó la base.
Listado (SELECT)No conviene usarlo: para contar filas está COUNT(*).—
Modificación (UPDATE)Las filas que cambiaron (puede ser 0 sin que haya error).—
Baja (DELETE)Las filas eliminadas.—

El caso que engaña es el del UPDATE con 0: significa «ningún valor cambió», y eso puede ser porque los datos ya eran esos o porque el id no existe. Como se vio en la modificación, ese número no sirve para distinguir los dos casos. Y en un SELECT, rowCount() no es de fiar: para saber cuántas filas cumplen una condición, lo correcto es un COUNT(*) aparte. La regla general es no usar rowCount() como única fuente de verdad, sino como una señal para comprobar mejor: si devuelve algo inesperado, hay que averiguar por qué.

Manejar los errores

Con ERRMODE_EXCEPTION, cualquier falla de la base de datos interrumpe el programa y lanza una excepción. Eso es bueno —permite enterarse— siempre que se la sepa atrapar:

try {

    $sentencia = $bd->prepare('INSERT INTO socios (nombre, documento) VALUES (:nombre, :documento)');
    $sentencia->execute([':nombre' => $nombre, ':documento' => $documento]);

    header('Location: listado.php?alta=ok');
    exit;

} catch (PDOException $e) {

    // Se guarda el detalle técnico para quien administra el sistema
    error_log('Error al insertar un socio: ' . $e->getMessage());

    // Al usuario se le muestra algo que entienda
    if ($e->getCode() === '23000') {
        $errores[] = 'Ya hay un socio cargado con ese documento.';
    } else {
        $errores[] = 'No se pudo guardar el socio. Intentá de nuevo en unos minutos.';
    }
}

El catch hace dos cosas distintas con la misma información, y ahí está toda la idea. El detalle técnico —el texto de la excepción, con el nombre de la tabla y de la restricción— se guarda en el registro del servidor, para quien administra el sistema. El usuario recibe un mensaje que puede entender y sobre el que puede actuar. Son dos públicos distintos y necesitan textos distintos.

Mostrar datos sin ejecutar HTML

Cuando PHP muestra en una página un valor que vino de la base, hay que impedir que el navegador lo interprete como código HTML:

  Cuando el dato sale a la página
El riesgo XSS: que el dato se lea como HTML.
La defensa htmlspecialchars(): el texto se muestra, no se interpreta.
Dónde se aplica Al imprimir, en todas las pantallas que muestran.

La función convierte los símbolos con significado en HTML en texto visible. Así el navegador muestra el dato en lugar de interpretarlo como una etiqueta o un script.

Resumen

  • PHP corre en el servidor y es el único que habla con los dos lados: recibe del navegador y consulta la base.
  • Sus seis responsabilidades: recibir, validar, preparar, ejecutar, leer y decidir.
  • PDO ofrece la misma forma de trabajar con cualquier gestor: cambia solo el DSN.
  • charset=utf8mb4 es lo que hace que las eñes y los acentos se guarden bien.
  • ERRMODE_EXCEPTION hace que los errores avisen en lugar de pasar en silencio. Se pone siempre.
  • Las credenciales no van en el código, y el usuario de la aplicación no debe ser root.
  • Para enviar valores en una consulta, aplicar la regla de consultas preparadas.
  • Para leer: fetchAll(), fetch() o fetchColumn(), según lo que devuelva la consulta.
  • Para saber si funcionó: rowCount() en las tres que escriben, y lastInsertId() en el alta.
  • El error se atrapa con try/catch: el detalle técnico al registro, un mensaje entendible al usuario.
  • htmlspecialchars() protege los datos al mostrarlos en HTML.
  • Las contraseñas no se guardan: se guarda el hash de password_hash() y se compara con password_verify().
  • La sesión es lo que recuerda quién entró: session_start() y una guardia al principio de cada página privada.

Ver el sistema de login Ver el proyecto completo