[PHP] Eliminar registros en ventana modal con confirmación.

Eliminar registros con confirmación mediante una ventana modal

Cuando desarrollamos una aplicación que permite eliminar registros de una base de datos, hay algo que conviene evitar siempre que sea posible: que un clic accidental termine borrando información.

Una solución sencilla consiste en mostrar una ventana de confirmación antes de ejecutar la eliminación.

Hay que evitar el clásico: «Lo borré por accidente».

En este ejemplo voy a construir un pequeño listado de clientes y añadiré una acción para eliminar cada registro. Antes de ejecutar el DELETE, JavaScript mostrará una ventana modal solicitando confirmación.

El ejemplo utilizará:

  • PHP para generar el listado y procesar la eliminación.
  • MySQL como base de datos.
  • PDO para trabajar con la base de datos.
  • JavaScript para gestionar la interacción.
  • Fetch API para enviar la petición al servidor sin recargar la página.

Importante

La ventana de confirmación mejora la experiencia de usuario, pero no es una medida de seguridad. La eliminación debe validarse siempre en el servidor.


La estructura del proyecto

Antes de empezar, voy a organizar mínimamente el proyecto para separar cada responsabilidad.

La estructura será la siguiente:

/
├── index.php
├── css/
│   └── estilos.css
├── js/
│   └── modal.js
└── privado/
    ├── conn.php
    └── eliminar.php

La idea es sencilla:

  • index.php mostrará los registros.
  • css/ contendrá los estilos.
  • js/ contendrá el código JavaScript.
  • privado/ contendrá los archivos que no queremos mezclar con la presentación.
  • conn.php gestionará la conexión con MySQL.
  • eliminar.php procesará la eliminación.

En un proyecto real, por supuesto, esta estructura podría ser mucho más elaborada. Para este ejemplo es suficiente.


Crear la tabla de clientes

Para realizar la prueba utilizaré una tabla sencilla llamada clientes.

CREATE TABLE clientes (
    idCliente INT UNSIGNED NOT NULL AUTO_INCREMENT,
    nombreCliente VARCHAR(80) NOT NULL,
    telefonoCliente VARCHAR(30) NOT NULL,
    PRIMARY KEY (idCliente)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Podemos introducir posteriormente algunos registros para realizar las pruebas.


Conexión con la base de datos mediante PDO

El siguiente archivo será privado/conn.php.

<?php

$host   = 'localhost';
$dbname = 'prueba';
$user   = 'pepito';
$pass   = 'pepito_prueba';

try {

    $dbconn = new PDO(
        "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
        $user,
        $pass,
        [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false
        ]
    );

} catch (PDOException $e) {

    http_response_code(500);
    exit('No se ha podido establecer la conexión con la base de datos.');

}

Hay un pequeño cambio respecto al código original que merece la pena destacar.

En lugar de ejecutar posteriormente:

$dbconn->query("SET NAMES 'utf8';");

he indicado directamente el juego de caracteres en el DSN:

charset=utf8mb4

Además, he configurado PDO para trabajar con excepciones y utilizar consultas preparadas de forma real.


Mostrar los registros

Ahora podemos crear nuestro index.php.

Lo primero será obtener los clientes de la base de datos:

<?php

require_once __DIR__ . '/privado/conn.php';

$query = "
    SELECT idCliente, nombreCliente, telefonoCliente
    FROM clientes
    ORDER BY idCliente DESC
";

$stmt = $dbconn->prepare($query);
$stmt->execute();

$clientes = $stmt->fetchAll();

?>

Y posteriormente generaremos la tabla:

<table class="table table-striped">

    <thead>
        <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Teléfono</th>
            <th>Acción</th>
        </tr>
    </thead>

    <tbody>

        <?php foreach ($clientes as $cliente): ?>

            <tr id="cliente-<?php echo (int) $cliente['idCliente']; ?>">

                <td>
                    <?php echo (int) $cliente['idCliente']; ?>
                </td>

                <td>
                    <?php echo htmlspecialchars($cliente['nombreCliente'], ENT_QUOTES, 'UTF-8'); ?>
                </td>

                <td>
                    <?php echo htmlspecialchars($cliente['telefonoCliente'], ENT_QUOTES, 'UTF-8'); ?>
                </td>

                <td>

                    <button
                        type="button"
                        class="dltBtn"
                        data-id="<?php echo (int) $cliente['idCliente']; ?>"
                    >
                        Eliminar
                    </button>

                </td>

            </tr>

        <?php endforeach; ?>

    </tbody>

</table>

<script src="js/modal.js" defer></script>

Aquí también hay algunos cambios importantes respecto al código original.

En lugar de utilizar:

extract($row);

accedo directamente a los valores del array:

$cliente['idCliente']
$cliente['nombreCliente']
$cliente['telefonoCliente']

Es mucho más explícito y evita introducir variables automáticamente en el ámbito actual.

También utilizo un <button> en lugar de un enlace con:

href="javascript:void(0)"

No necesitamos navegar a ningún sitio. Estamos ejecutando una acción, así que un botón representa mejor lo que estamos haciendo.


El atributo data-id

Ahora llegamos a una de las partes interesantes del ejemplo.

Cada botón contiene el identificador del registro mediante un atributo data-*:

<button
    type="button"
    class="dltBtn"
    data-id="23"
>
    Eliminar
</button>

El atributo:

data-id="23"

nos permite asociar el botón con el registro que queremos eliminar.

Cuando el usuario pulse el botón, JavaScript podrá obtener ese valor y utilizarlo posteriormente en la petición al servidor.


La ventana de confirmación

Ahora viene la parte que originalmente realizábamos mediante jQuery y Bootbox.

Hoy podemos hacerlo directamente con JavaScript sin necesidad de añadir una biblioteca adicional únicamente para mostrar una confirmación.

En js/modal.js podemos utilizar:

document.addEventListener('DOMContentLoaded', function () {

    const botonesEliminar = document.querySelectorAll('.dltBtn');

    botonesEliminar.forEach(function (boton) {

        boton.addEventListener('click', async function () {

            const id = this.dataset.id;

            if (!id) {
                return;
            }

            const confirmado = window.confirm(
                '¿Estás seguro de que quieres eliminar este registro?'
            );

            if (!confirmado) {
                return;
            }

            try {

                const response = await fetch('privado/eliminar.php', {

                    method: 'POST',

                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded'
                    },

                    body: new URLSearchParams({
                        id: id
                    })

                });

                const data = await response.json();

                if (!response.ok || !data.success) {
                    throw new Error(
                        data.message || 'No se ha podido eliminar el registro.'
                    );
                }

                const fila = document.getElementById(
                    'cliente-' + id
                );

                if (fila) {
                    fila.remove();
                }

                alert(data.message);

            } catch (error) {

                alert(error.message);

            }

        });

    });

});

La lógica es bastante sencilla:

  1. Localizamos todos los botones de eliminación.
  2. Esperamos a que el usuario pulse uno de ellos.
  3. Obtenemos su data-id.
  4. Mostramos la confirmación.
  5. Si el usuario cancela, no hacemos nada.
  6. Si confirma, enviamos el ID mediante POST.
  7. PHP procesa la eliminación.
  8. Si todo ha ido correctamente, eliminamos la fila de la tabla sin recargar la página.

¿Y dónde está la ventana modal?

En este punto he simplificado deliberadamente el ejemplo original.

Para una confirmación sencilla, el navegador ya dispone de:

window.confirm()

Por tanto, no necesitamos cargar Bootbox ni otra biblioteca externa.

Si queremos una ventana modal completamente personalizada, con nuestros propios estilos, botones y animaciones, podemos sustituir window.confirm() por un elemento HTML de tipo <dialog> o por nuestro propio componente modal.

La comunicación con el servidor no cambia.

La confirmación no elimina nada

La ventana únicamente pregunta al usuario si quiere continuar. La eliminación real se produce posteriormente en el servidor.


Procesar la eliminación con PHP

Ahora llegamos a la parte realmente importante: el servidor.

En privado/eliminar.php tendremos algo parecido a esto:

<?php

require_once __DIR__ . '/conn.php';

header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {

    http_response_code(405);

    echo json_encode([
        'success' => false,
        'message' => 'Método no permitido.'
    ]);

    exit;
}

$id = filter_input(
    INPUT_POST,
    'id',
    FILTER_VALIDATE_INT
);

if (!$id || $id < 1) {

    http_response_code(400);

    echo json_encode([
        'success' => false,
        'message' => 'El identificador no es válido.'
    ]);

    exit;
}

$query = "
    DELETE FROM clientes
    WHERE idCliente = :id
";

$stmt = $dbconn->prepare($query);

$stmt->execute([
    ':id' => $id
]);

if ($stmt->rowCount() === 0) {

    http_response_code(404);

    echo json_encode([
        'success' => false,
        'message' => 'El registro no existe.'
    ]);

    exit;
}

echo json_encode([
    'success' => true,
    'message' => 'El registro ha sido eliminado correctamente.'
]);

Aquí sí que tenemos que prestar atención.

El usuario ha confirmado en el navegador, pero el servidor no debe confiar en JavaScript.

Por eso volvemos a validar el identificador:

$id = filter_input(
    INPUT_POST,
    'id',
    FILTER_VALIDATE_INT
);

Y utilizamos una consulta preparada:

$query = "
    DELETE FROM clientes
    WHERE idCliente = :id
";

$stmt = $dbconn->prepare($query);

$stmt->execute([
    ':id' => $id
]);

De esta manera no estamos concatenando directamente el valor recibido por el usuario dentro de la consulta SQL.


¿Por qué no utilizar $_REQUEST?

En el código original utilizábamos:

$_REQUEST['eliminar']

Funciona, pero no es la opción que utilizaría actualmente.

$_REQUEST mezcla datos procedentes de diferentes fuentes, como GET, POST y, dependiendo de la configuración, cookies.

Si nuestro endpoint espera recibir un ID mediante POST, es mucho más claro utilizar:

$_POST['id']

o, mejor todavía, validar directamente el valor:

$id = filter_input(
    INPUT_POST,
    'id',
    FILTER_VALIDATE_INT
);

Así dejamos claro qué esperamos recibir y de dónde.


¿Y la seguridad?

Aquí es donde este ejemplo deja de ser simplemente una cuestión de interfaz.

Mostrar una confirmación antes de eliminar un registro está muy bien, pero no proporciona seguridad.

Un usuario podría realizar directamente una petición HTTP contra nuestro endpoint sin utilizar nuestra interfaz.

Por eso, en una aplicación real deberíamos añadir, como mínimo:

  • Autenticación del usuario.
  • Autorización para comprobar que puede eliminar ese registro.
  • Validación de todos los datos recibidos.
  • Consultas preparadas.
  • Protección CSRF.
  • Control de errores.
  • Registro de operaciones cuando sea necesario.

Por ejemplo, en una aplicación con sesiones podríamos comprobar en eliminar.php que el usuario tiene permisos suficientes antes de ejecutar el DELETE.

if (!usuarioPuedeEliminar()) {

    http_response_code(403);

    echo json_encode([
        'success' => false,
        'message' => 'No tienes permisos para realizar esta acción.'
    ]);

    exit;
}

La función concreta dependerá de cómo esté construida nuestra aplicación.


Una cosa importante: CSRF

Si esta eliminación forma parte de una aplicación real con usuarios autenticados, tampoco deberíamos quedarnos únicamente con la validación del ID.

Una petición que modifica información debe estar protegida frente a ataques CSRF.

El funcionamiento habitual consiste en generar un token asociado a la sesión, enviarlo con la petición y comprobarlo en el servidor antes de ejecutar la operación.

Por ejemplo, podríamos enviar un token junto al ID:

body: new URLSearchParams({
    id: id,
    csrf_token: csrfToken
})

Y comprobarlo posteriormente en PHP.

No voy a desarrollar aquí todo el sistema de autenticación y CSRF porque nos alejaríamos bastante del objetivo original del artículo, pero es algo que debemos tener presente cuando pasamos de un ejemplo didáctico a una aplicación real.


El resultado

El flujo completo queda bastante claro:

Usuario pulsa "Eliminar"
            ↓
JavaScript obtiene data-id
            ↓
Ventana de confirmación
            ↓
       ¿Confirma?
        /       \
      NO         SÍ
      ↓           ↓
    Fin       POST al servidor
                  ↓
             Validación PHP
                  ↓
             Consulta PDO
                  ↓
             DELETE SQL
                  ↓
            Respuesta JSON
                  ↓
          Eliminar la fila
          del listado

Y todo ello sin necesidad de recargar la página.


¿Por qué he cambiado el ejemplo original?

La idea del artículo original sigue siendo perfectamente válida, pero algunas decisiones técnicas han envejecido.

Por ejemplo:

  • He sustituido $_REQUEST por una entrada explícita mediante POST.
  • He eliminado extract() para que las variables sean más claras.
  • He sustituido javascript:void(0) por un botón.
  • He sustituido el AJAX de jQuery por la Fetch API.
  • He eliminado Bootbox para la confirmación básica.
  • He añadido validación del identificador.
  • He añadido respuestas JSON.
  • He añadido códigos HTTP adecuados para los errores.
  • He mantenido las consultas preparadas mediante PDO.
  • He separado claramente la confirmación de usuario de la seguridad del servidor.

No significa que jQuery o Bootbox sean incorrectos. Simplemente, para una funcionalidad tan pequeña, actualmente podemos resolverlo con las APIs que ya proporciona el navegador.


Conclusión

Una confirmación antes de eliminar un registro es una funcionalidad sencilla, pero puede evitar bastantes disgustos.

En este ejemplo hemos creado un pequeño flujo completo:

  • Mostrar registros procedentes de MySQL.
  • Asociar cada botón con el ID correspondiente mediante data-id.
  • Solicitar confirmación antes de eliminar.
  • Enviar el ID mediante POST.
  • Validarlo en PHP.
  • Eliminar el registro mediante PDO.
  • Devolver una respuesta JSON.
  • Actualizar la interfaz sin recargar la página.

Una última consideración

La ventana de confirmación es para el usuario. La seguridad está en el servidor. Nunca debemos confiar en que, porque alguien ha pulsado «Confirmar», la petición sea legítima.

El ejemplo es sencillo a propósito. A partir de aquí podemos construir el resto de operaciones de un CRUD: insertar, consultar, modificar y eliminar registros.

Y, sobre todo, podemos hacerlo sin convertir el proyecto en un chorizo de código imposible de mantener.

Visitas: 16244

Website |  + posts

Automation & Data Specialist | Web Development | Data Processing & Integration

Deja un comentario