Ir al contenido principal

Listado

Qué es un listado

Un listado muestra lo que hay guardado. Es la R del CRUD y la única de las cuatro operaciones que no cambia nada: lee, muestra y listo. La tabla queda exactamente igual después de pasar por ella.

Es importante no dejarse engañar por esa modestia, porque el listado es la pantalla principal del sistema. Es la que se abre primero, la que da sentido a las otras tres y la que se usa más veces por día. Y casi siempre es también la puerta de entrada a las demás: en la fila de cada dato están los botones para editarlo y para darlo de baja. Como decía la sigla ABM, ahí está el motivo por el que el listado no aparece en su nombre: se da por supuesto.

Hay dos formas de listado, y las dos hacen falta. El listado completo, que muestra todo lo que hay, y el listado con filtros, que muestra solo lo que cumple una condición —buscar un socio por documento, ver los préstamos sin devolver—. El primero sirve cuando la tabla es chica; el segundo es obligatorio en cuanto hay unos cientos de filas, porque nadie encuentra nada en una lista de tres mil elementos.

La sentencia SELECT

La sentencia que devuelve datos es SELECT, y el tema completo está en la Unidad 3: las cláusulas y las columnas en Consultas básicas, y los filtros, los operadores y el ordenamiento en Filtros y ordenamiento.

SELECT  id, nombre, documento, correo
FROM    socios
WHERE   activo = 1
ORDER BY nombre;

Lo que es propio de un listado hecho desde un programa son los dos detalles de abajo.

Mostrar el resultado

Acá está la idea que cuesta al principio: la base de datos no devuelve una tabla HTML. Devuelve una lista de filas, y cada fila es un conjunto de valores con el nombre de su columna. Armar la tabla es trabajo de PHP, que tiene que recorrer esa lista y escribir el HTML.

<?php

$bd = new PDO('mysql:host=localhost;dbname=biblioteca;charset=utf8mb4', 'usuario', 'clave');
$bd->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Lo que el usuario escribió en el buscador (puede no venir)
$buscar = trim($_GET['buscar'] ?? '');

$sentencia = $bd->prepare(
    'SELECT id, nombre, documento, correo
     FROM   socios
     WHERE  nombre LIKE :buscar
     ORDER  BY nombre'
);

// El comodín se agrega acá, no en la consulta
$sentencia->execute([':buscar' => '%' . $buscar . '%']);

$socios = $sentencia->fetchAll();
?>

Y después, el HTML que recorre el resultado:

<table class="table">
    <thead>
        <tr>
            <th>Nombre</th>
            <th>Documento</th>
            <th>Correo</th>
            <th>Acciones</th>
        </tr>
    </thead>
    <tbody>
    <?php foreach ($socios as $socio): ?>
        <tr>
            <td><?= htmlspecialchars($socio['nombre']) ?></td>
            <td><?= htmlspecialchars($socio['documento']) ?></td>
            <td><?= htmlspecialchars($socio['correo']) ?></td>
            <td class="acciones">
                <a href="editar.php?id=<?= (int) $socio['id'] ?>">Editar</a>

                <form action="baja.php" method="post" class="en-linea">
                    <input type="hidden" name="id" value="<?= (int) $socio['id'] ?>">
                    <button type="submit">Eliminar</button>
                </form>
            </td>
        </tr>
    <?php endforeach; ?>
    </tbody>
</table>

Vale la pena mirar con atención la celda de acciones, porque ahí está resumida toda la unidad. El botón Editar es un enlace y el de Eliminar es un formulario. No es una inconsistencia: es exactamente la regla de los métodos de envío. Editar muestra un formulario con los datos, así que no cambia nada y va con GET; eliminar destruye una fila, así que va con POST y nunca podría ser un enlace.

Ese código arma un listado de socios. Así queda, en cambio, el listado del CRUD de la tienda, con los encabezados, los datos y los dos botones en la columna de acciones:

El listado del CRUD de la tienda: la tabla Lista de Productos con las columnas ID, Artículo, Descripción, Unidades Disponibles y Acciones, con una sola fila cargada (1, Aceite, Producto de girasol, 20) y los botones rojo Eliminar y amarillo Modificar
El listado del CRUD de la tienda en funcionamiento. La columna Acciones es la que convierte una tabla de datos en el centro del sistema: desde ahí se llega a modificar y a eliminar.

Por qué htmlspecialchars()

Todos los valores que salen de la base de datos están envueltos en esa función, y no es un adorno: es lo que impide que un dato guardado se convierta en parte de la página.

El problema es este. Un dato guardado es texto: no tiene por qué cumplir ninguna regla de HTML. Pero cuando se escribe dentro de la página, el navegador lo lee como HTML. Y si ese texto contiene símbolos con significado —los signos de menor y mayor, las comillas— el navegador los interpreta en lugar de mostrarlos.

Alcanza con imaginar que alguien carga un socio con este nombre:

<script>alert('hola')</script>

Si ese nombre se escribe tal cual en el listado, el navegador no lo muestra: lo ejecuta. Cada persona que abra el listado va a ver una ventana emergente. Y eso, que parece una travesura, es en realidad una de las fallas de seguridad más conocidas de la web: con un poco más de ingenio, el mismo mecanismo permite robar la sesión de otro usuario o redirigirlo a otro sitio.

Un plus: el modo inseguro y el modo seguro

El listado del CRUD de la tienda funciona tal como está escrito, y así como está se entiende todo lo anterior. Pero hay una mejora opcional que conviene conocer: cómo se escriben los valores dentro de la tabla. Hay dos maneras de hacerlo —una insegura y una segura— y la diferencia entre las dos se ve a simple vista.

Para comprobarlo, se cargó en la tienda un producto con este nombre:

<b>Aceite</b>

No es un nombre razonable, y justamente por eso sirve de prueba: es un texto guardado en la base que además contiene símbolos con significado en HTML. La base lo acepta sin opinar —para un VARCHAR es texto como cualquier otro— y el listado lo muestra de una manera o de otra según cómo esté escrito el programa.

Modo inseguro: el valor tal cual

Es lo que hace el CRUD: escribir el valor directamente.

<td><?= $producto['articulo'] ?></td>
Listado del CRUD con un producto llamado <b>Aceite</b> guardado en la base: en la columna Artículo la fila 3 muestra Aceite en negrita, porque el navegador interpretó la etiqueta b en lugar de mostrar el texto
Modo inseguro. El producto de la fila 3 se llama <b>Aceite</b>, pero en pantalla se ve Aceite en negrita: el navegador interpretó la etiqueta en lugar de mostrar el texto.

El navegador no muestra el nombre: interpreta el <b> y lo dibuja. El dato que estaba guardado dejó de ser un dato y pasó a formar parte de la página.

Modo seguro: el valor envuelto en htmlspecialchars()

Es una palabra más por dato, y alcanza:

<td><?= htmlspecialchars($producto['articulo']) ?></td>
El mismo listado del CRUD, con los valores escapados: la fila 3 ahora muestra el texto literal <b>Aceite</b> en la columna Artículo
Modo seguro. El mismo producto, la misma base y el mismo listado: ahora el texto se ve completo, con los signos incluidos.

Solo cambió cómo se escribe el valor en el HTML. Se aplica igual a los otros campos de texto —la descripcion— y no hace falta para los números, como el id o las unidades, que no pueden contener símbolos.

Una segunda mejora, del mismo estilo, es declarar el juego de caracteres en la conexión: mysql:host=localhost;dbname=tienda;charset=utf8mb4. El CRUD de la tienda no lo declara y en este servidor funciona igual, porque MariaDB ya viene con utf8mb4 por defecto. Declararlo sirve para que el programa no dependa de la configuración de cada servidor: en una instalación con otro juego por defecto, las tildes y las eñes llegan deformadas.

Cuando el listado viene vacío

Hay un caso que se pasa por alto siempre y que se ve en la primera prueba: la consulta no devuelve nada. Y eso no es un error. Puede pasar por dos motivos muy distintos, y el usuario tiene derecho a distinguirlos:

  • La tabla está vacía. Todavía no se cargó ningún socio.
  • El filtro no encontró nada. Hay socios cargados, pero ninguno coincide con la búsqueda.

Una tabla HTML vacía no distingue esos dos casos, y tampoco distingue el vacío de una falla: se ve igual que una página que no cargó. El usuario, que no sabe si el sistema está roto, prueba de nuevo, recarga, aprieta el botón otra vez. La solución es un mensaje, y es distinto según el caso:

<?php if (! $socios): ?>

    <?php if ($buscar !== ''): ?>
        <p>No se encontró ningún socio que coincida con «<?= htmlspecialchars($buscar) ?>».</p>
    <?php else: ?>
        <p>Todavía no hay socios cargados.</p>
    <?php endif; ?>

<?php else: ?>

    ... la tabla con los resultados ...

<?php endif; ?>

Los dos mensajes son distintos porque las dos situaciones son distintas, y la acción que el usuario tiene que tomar también: en el primer caso conviene borrar el filtro, en el segundo hay que cargar el primer socio. Un mensaje genérico del tipo «no hay resultados» deja al usuario sin saber qué hacer. Detalle de color: fetchAll() devuelve un arreglo vacío cuando no hay filas, y una lista vacía evaluada como condición es falsa, así que if (! $socios) alcanza para los dos casos.

Contar y paginar

Dos herramientas que se necesitan en cuanto la tabla crece:

-- Cuántas filas hay en total
SELECT COUNT(*) FROM socios;

-- Traer solo una parte: 20 filas, salteando las primeras 40
SELECT id, nombre FROM socios ORDER BY nombre LIMIT 20 OFFSET 40;

El COUNT(*) devuelve un único número: cuántas filas cumplen la condición. Sirve para mostrar «47 socios» arriba del listado y, sobre todo, para armar la paginación. El LIMIT y el OFFSET son los que hacen posible mostrar de a veinte: LIMIT es cuántas filas traer y OFFSET, cuántas saltear. Cambiando el OFFSET se pasa de página en página. Sin eso, un listado de diez mil filas obligaría a traerlas todas de la base, armar un HTML enorme y hacer esperar al usuario —y a todos los demás— mientras tanto. El ORDER BY no es opcional cuando se pagina: sin un orden definido, la base puede devolver las filas en cualquier orden en cada consulta, y entonces la página 2 podría mostrar filas que ya salieron en la 1 y saltearse otras.

Resumen

  • Un listado muestra lo que está guardado y no cambia nada.
  • Es la pantalla principal del sistema y la puerta de entrada a las otras tres operaciones.
  • Se usa SELECT, nombrando las columnas en lugar de usar *.
  • Siempre conviene un ORDER BY: sin él, el orden no está garantizado.
  • Los filtros van con WHERE; el LIKE con % es el de los buscadores.
  • El % se agrega en PHP, no dentro de la consulta.
  • La base devuelve una lista de filas, no una tabla HTML: la tabla la arma PHP recorriéndola.
  • En el listado, editar es un enlace y borrar es un formulario: GET para pedir, POST para cambiar.
  • Todo valor que sale de la base va dentro de htmlspecialchars(): es la defensa contra XSS.
  • Un listado vacío no es un error, y el mensaje cambia según la causa: tabla vacía o filtro sin resultados.
  • COUNT(*), LIMIT y OFFSET permiten contar y paginar.

Ver las bajas