php

CRUD con PHP, MySQL y AJAX

En este post te mostraré un ejemplo completo de CRUD que hace las operaciones fundamentales de la base de datos usando MySQL y PHP, pero además las llamadas se hacen con AJAX desde JavaScript.

CRUD con MySQL, PHP, JavaScript y AJAX

Al final te dejaré el código completo que podrás descargar, probar y modificar, mismo que tendrá todas las operaciones para enviar y recibir datos desde JavaScript a un servidor PHP que se conecta a MySQL.

Básicamente todo se hará del lado del cliente, no vamos a procesar formularios con PHP, solo llamadas AJAX con JSON. Así que tendremos un CRUD con PHP y AJAX.

No vamos a usar ninguna librería como React, Vue o Angular; será JavaScript puro.

Descripción general del proyecto

Vamos a usar PHP para reutilizar la plantilla con el encabezado y pie, además de usarlo para la conexión a la base de datos con MySQL y responder las peticiones desde JavaScript.

Ya con JavaScript vamos a procesar todo el funcionamiento del lado del cliente. Podría decirse que PHP se usará para renderizar las vistas y atender peticiones.

Por cierto, igualmente usaremos SweetAlert para los avisos, fetch para las llamadas AJAX y Bulma para los estilos.

Conexión a la base de datos

Usaremos MySQL que bien puede ser reemplazado por MariaDB. Recuerda que las credenciales viven en un archivo llamado env.php que debes crear tú mismo,  basándote en el archivo env.ejemplo.php.

En mi caso se ve así:

; <?php exit; ?>
MYSQL_DATABASE_NAME = "tienda"
MYSQL_USER = "root"
MYSQL_PASSWORD = ""

Y la conexión así como la lectura del archivo se hace con estas funciones:

<?php
function obtenerVariableDelEntorno($key)
{
    if (defined("_ENV_CACHE")) {
        $vars = _ENV_CACHE;
    } else {
        $file = "env.php";
        if (!file_exists($file)) {
            throw new Exception("El archivo de las variables de entorno ($file) no existe. Favor de crearlo");
        }
        $vars = parse_ini_file($file);
        define("_ENV_CACHE", $vars);
    }
    if (isset($vars[$key])) {
        return $vars[$key];
    } else {
        throw new Exception("La clave especificada (" . $key . ") no existe en el archivo de las variables de entorno");
    }
}
function obtenerConexion()
{
    $password = obtenerVariableDelEntorno("MYSQL_PASSWORD");
    $user = obtenerVariableDelEntorno("MYSQL_USER");
    $dbName = obtenerVariableDelEntorno("MYSQL_DATABASE_NAME");
    $database = new PDO('mysql:host=localhost;dbname=' . $dbName, $user, $password);
    $database->query("set names utf8;");
    $database->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
    $database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $database->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
    return $database;
}

Finalmente, el esquema solo contiene una tabla. Para este ejemplo voy a usar una tabla de productos en donde los mismos llevan nombre, descripción y precio, además del respectivo ID.

CREATE TABLE IF NOT EXISTS productos(
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(255) NOT NULL,
    descripcion VARCHAR(1024) NOT NULL,
    precio DECIMAL(9,2)
);

CRUD de productos

Antes de pasar al uso de la técnica AJAX con JavaScript veamos las operaciones que se hacen desde PHP a la base de datos. Luego vamos a exponer estas funciones en archivos separados que vamos a consultar con JavaScript.

<?php
function actualizarProducto($nombre, $precio, $descripcion, $id)
{
    $bd = obtenerConexion();
    $sentencia = $bd->prepare("UPDATE productos SET nombre = ?, precio = ?, descripcion = ? WHERE id = ?");
    return $sentencia->execute([$nombre, $precio, $descripcion, $id]);
}

function obtenerProductoPorId($id)
{
    $bd = obtenerConexion();
    $sentencia = $bd->prepare("SELECT id, nombre, descripcion, precio FROM productos WHERE id = ?");
    $sentencia->execute([$id]);
    return $sentencia->fetchObject();
}

function obtenerProductos()
{
    $bd = obtenerConexion();
    $sentencia = $bd->query("SELECT id, nombre, descripcion, precio FROM productos");
    return $sentencia->fetchAll();
}

function eliminarProducto($id)
{
    $bd = obtenerConexion();
    $sentencia = $bd->prepare("DELETE FROM productos WHERE id = ?");
    return $sentencia->execute([$id]);
}

function guardarProducto($nombre, $precio, $descripcion)
{
    $bd = obtenerConexion();
    $sentencia = $bd->prepare("INSERT INTO productos(nombre, precio, descripcion) VALUES(?, ?, ?)");
    return $sentencia->execute([$nombre, $precio, $descripcion]);
}

Como puedes ver tenemos las operaciones básicas que una API debería tener. Esto es, obtener todos los datos, obtener por id, actualizar, insertar y eliminar. Ahora cada uno de estos métodos estará en su respectivo archivo que llamaremos desde el cliente.

En cuanto a la interacción con MySQL estamos usando el driver PDO para conectarnos, además de usar sentencias preparadas para evitar inyecciones SQL.

Agregar producto

Agregar nuevo producto usando formulario, JavaScript y AJAX

Vemos el formulario de datos. Cada input tiene un id, esto es muy importante porque más tarde vamos a recuperarlos usando querySelector.

<div class="columns">
    <div class="column is-one-third">
        <h2 class="is-size-2">Nuevo producto</h2>
        <div class="field">
            <label for="nombre">Nombre</label>
            <div class="control">
                <input required id="nombre" class="input" type="text" placeholder="Nombre" name="nombre">
            </div>
        </div>
        <div class="field">
            <label for="descripcion">Descripción</label>
            <div class="control">
                <textarea name="descripcion" class="textarea" id="descripcion" cols="30" rows="5" placeholder="Descripción" required></textarea>
            </div>
        </div>
        <div class="field">
            <label for="precio">Precio</label>
            <div class="control">
                <input required id="precio" name="precio" class="input" type="number" placeholder="Precio">
            </div>
        </div>
        <div class="field">
            <div class="control">
                <button id="btnGuardar" class="button is-success">Guardar</button>
                <a href="productos.php" class="button is-warning">Volver</a>
            </div>
        </div>
    </div>
</div>
<script src="js/agregar_producto.js"></script>

Fíjate en que se está incluyendo un archivo de JavaScript en la última línea. Esta práctica se sigue para los demás archivos. En cuanto al código de JavaScript, primero declaramos unas constantes para los elementos del DOM:

const $nombre = document.querySelector("#nombre"),
$descripcion = document.querySelector("#descripcion"),
$precio = document.querySelector("#precio"),
$btnGuardar = document.querySelector("#btnGuardar");

Y escuchamos el clic del botón:

$btnGuardar.onclick = async () => {
    const nombre = $nombre.value,
        descripcion = $descripcion.value,
        precio = parseFloat($precio.value);
    // Pequeña validación, aunque debería hacerse del lado del servidor igualmente, aquí es pura estética
    if (!nombre) {
        return Swal.fire({
            icon: "error",
            text: "Escribe el nombre",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }
    if (!descripcion) {
        return Swal.fire({
            icon: "error",
            text: "Escribe la descripción",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }

    if (!precio) {
        return Swal.fire({
            icon: "error",
            text: "Escribe el precio",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }
    // Lo que vamos a enviar a PHP
    const cargaUtil = {
        nombre: nombre,
        descripcion: descripcion,
        precio: precio,
        // Nota: podríamos hacerlo más simple, y escribir:
        // nombre,
        // En lugar de:
        // nombre: nombre
        // Pero eso podría confundir al principiante
    };
    // Codificamos...
    const cargaUtilCodificada = JSON.stringify(cargaUtil);
    // Enviamos
    try {
        const respuestaRaw = await fetch("guardar_producto.php", {
            method: "POST",
            body: cargaUtilCodificada,
        });
        // El servidor nos responderá con JSON
        const respuesta = await respuestaRaw.json();
        if (respuesta) {

            // Y si llegamos hasta aquí, todo ha ido bien
            Swal.fire({
                icon: "success",
                text: "Producto guardado",
                timer: 700, // <- Ocultar dentro de 0.7 segundos
            });
            // Limpiamos el formulario
            $nombre.value = $descripcion.value = $precio.value = "";
        } else {
            Swal.fire({
                icon: "error",
                text: "El servidor no envió una respuesta exitosa",
            });
        }
    } catch (e) {
        // En caso de que haya un error
        Swal.fire({
            icon: "error",
            title: "Error de servidor",
            text: "Inténtalo de nuevo. El error es: " + e,
        });
    }
};

Lo que se está haciendo es recuperar los valores de los campos y hacer una pequeña validación, aunque recuerda que siempre es importante validar en el servidor. En este caso he omitido esa parte por simplicidad.

En caso de que la validación pase, preparamos la carga útil que será un objeto con los datos, y luego lo codificamos con JSON.stringify. La verdadera magia con AJAX está sucediendo cuando invocamos a fetch para que haga la petición.

Finalmente esperamos la respuesta del servidor que también estará en JSON y la manejamos como debe ser. Esto último está en un try catch ya que siempre puede haber problemas al hacer estas llamadas.

Lado del servidor

Pasando a PHP, el archivo es guardar_producto.php, mismo que solo se encarga de obtener el cuerpo de la petición (que será lo que enviamos con JavaScript a través de AJAX), validar su presencia y llamar a una de las funciones que vimos anteriormente:

<?php
$cargaUtil = json_decode(file_get_contents("php://input"));
// Si no hay datos, salir inmediatamente indicando un error 500
if (!$cargaUtil) {
    // https://parzibyte.me/blog/2021/01/17/php-enviar-codigo-error-500/
    http_response_code(500);
    exit;
}
// Extraer valores
$nombre = $cargaUtil->nombre;
$precio = $cargaUtil->precio;
$descripcion = $cargaUtil->descripcion;
include_once "funciones.php";
$respuesta = guardarProducto($nombre, $precio, $descripcion);
// Devolver al cliente la respuesta de la función
echo json_encode($respuesta);

Ya para terminar en la parte de PHP, devolvemos un booleano dependiendo de lo que devuelva la función para indicar el éxito o fracaso de la operación. Y de manera similar haremos las otras operaciones; ya no explicaré a detalle el código, solo las partes más importantes.

Obtener productos

Obtener datos desde PHP con JavaScript usando AJAX – Recibir arreglo de objetos

Este apartado es un poco largo en cuanto al código, pues vamos a obtener los datos, procesarlos para dibujar una tabla (todo a mano) y agregar los dos botones con su respectivo listener.

Veamos el diseño:

<div class="columns">
    <div class="column">
        <h2 class="is-size-2">Productos existentes</h2>
        <a class="button is-success" href="agregar_producto.php">Nuevo&nbsp;<i class="fa fa-plus"></i></a>
        <table class="table">
            <thead>
                <tr>
                    <th>Nombre</th>
                    <th>Descripción</th>
                    <th>Precio</th>
                    <th>Editar</th>
                    <th>Eliminar</th>
                </tr>
            </thead>
            <tbody id="cuerpoTabla">
            </tbody>
        </table>
    </div>
</div>
<script src="js/productos.js"></script>

Lo único destacable es que estamos definiendo el cuerpo de la tabla, pues sobre ella vamos a dibujar, de manera dinámica, todas las filas dependiendo de la cantidad de productos.

En cuanto al código de JavaScript queda como se ve a continuación:

const $cuerpoTabla = document.querySelector("#cuerpoTabla");

const obtenerProductos = async () => {
    // Es una petición GET, no necesitamos indicar el método ni el cuerpo
    const respuestaRaw = await fetch("obtener_productos.php");
    const productos = await respuestaRaw.json();
    // Limpiamos la tabla
    $cuerpoTabla.innerHTML = "";
    // Ahora ya tenemos a los productos. Los recorremos
    for (const producto of productos) {
        // Vamos a ir adjuntando elementos a la tabla.
        const $fila = document.createElement("tr");
        // La celda del nombre
        const $celdaNombre = document.createElement("td");
        // Colocamos su valor y lo adjuntamos a la fila
        $celdaNombre.innerText = producto.nombre;
        $fila.appendChild($celdaNombre);
        // Lo mismo para lo demás
        const $celdaDescripcion = document.createElement("td");
        $celdaDescripcion.innerText = producto.descripcion;
        $fila.appendChild($celdaDescripcion);
        const $celdaPrecio = document.createElement("td");
        $celdaPrecio.innerText = producto.precio;
        $fila.appendChild($celdaPrecio);
        // Extraer el id del producto en el que estamos dentro del ciclo
        const idProducto = producto.id;
        // Link para eliminar
        const $linkEditar = document.createElement("a");
        $linkEditar.href = "./editar_producto.php?id=" + idProducto;
        $linkEditar.innerHTML = `<i class="fa fa-edit"></i>`;
        $linkEditar.classList.add("button", "is-warning");
        const $celdaLinkEditar = document.createElement("td");
        $celdaLinkEditar.appendChild($linkEditar);
        $fila.appendChild($celdaLinkEditar);

        // Para el botón de eliminar primero creamos el botón, agregamos su listener y luego lo adjuntamos a su celda
        const $botonEliminar = document.createElement("button");
        $botonEliminar.classList.add("button", "is-danger")
        $botonEliminar.innerHTML = `<i class="fa fa-trash"></i>`;
        $botonEliminar.onclick = async () => {
          // Código aquí...
        };
        const $celdaBoton = document.createElement("td");
        $celdaBoton.appendChild($botonEliminar);
        $fila.appendChild($celdaBoton);
        // Adjuntamos la fila a la tabla
        $cuerpoTabla.appendChild($fila);
    }
};

En las primeras líneas invocamos a un archivo de PHP igualmente usando AJAX, mismo que nos va a devolver un array con los datos. Decodificamos esos datos y después recorremos toda la respuesta.

Lo que hacemos más tarde es crear un elemento <tr>, adjuntarle varias celdas con <td> y finalmente adjuntar esa fila al cuerpo de la tabla. En el caso de los botones, uno de ellos en realidad es un enlace.

Botón de eliminar

Confirmación con SweetAlert para eliminar producto usando AJAX, PHP y MySQL

Para el clic del botón de eliminar, el código es el siguiente. Si te preguntas de dónde sale la variable idProducto mira la línea 26 del archivo anterior. Cuando el usuario hace clic en este botón se le muestra una alerta de confirmación con SweetAlert:

const respuestaConfirmacion = await Swal.fire({
    title: "Confirmación",
    text: "¿Eliminar el producto? esto no se puede deshacer",
    icon: 'warning',
    showCancelButton: true,
    cancelButtonColor: '#3085d6',
    confirmButtonColor: '#d33',
    confirmButtonText: 'Sí, eliminar',
    cancelButtonText: 'Cancelar',
});
if (respuestaConfirmacion.value) {
    const url = `./eliminar_producto.php?id=${idProducto}`;
    const respuestaRaw = await fetch(url, {
        method: "DELETE",
    });
    const respuesta = await respuestaRaw.json();
    if (respuesta) {
        Swal.fire({
            icon: "success",
            text: "Producto eliminado",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    } else {
        Swal.fire({
            icon: "error",
            text: "El servidor no respondió con una respuesta exitosa",
        });
    }
    // De cualquier modo, volver a obtener los productos para refrescar la tabla
    obtenerProductos();
}

En caso de que el usuario confirme la acción, se hace igualmente una petición AJAX con el método HTTP delete al archivo eliminar_producto.php pasándole el ID. El contenido del mismo es muy simple y parecido a los otros:

<?php
if (!isset($_GET["id"])) {
    http_response_code(500);
    exit();
}

include_once "funciones.php";
$respuesta = eliminarProducto($_GET["id"]);
echo json_encode($respuesta);

Como puedes ver, estos archivos solo sirven para comunicar lo que quiere JavaScript con las funciones definidas en el archivo que se incluye en la línea 7.

Editar producto

Editar producto – Ejemplo completo de conexión JavaScript con PHP y MySQL usando AJAX

La última operación para completar este CRUD es la de editar. En este caso primero debemos extraer el id del producto leyendo valores de la URL, obtener sus detalles, rellenar el formulario y luego escuchar el clic del botón que actualiza el producto.

Ésta última parte del clic del botón es muy similar a cuando insertamos un producto por primera vez, lo que cambia es la forma en la que se rellena el formulario y la redirección que se hace al terminar de editar.

Veamos el código HTML:

<div class="columns">
    <div class="column is-one-third">
        <h2 class="is-size-2">Editar producto</h2>
        <div class="field">
            <label for="nombre">Nombre</label>
            <div class="control">
                <input required id="nombre" class="input" type="text" placeholder="Nombre" name="nombre">
            </div>
        </div>
        <div class="field">
            <label for="descripcion">Descripción</label>
            <div class="control">
                <textarea name="descripcion" class="textarea" id="descripcion" cols="30" rows="5" placeholder="Descripción" required></textarea>
            </div>
        </div>
        <div class="field">
            <label for="precio">Precio</label>
            <div class="control">
                <input required id="precio" name="precio" class="input" type="number" placeholder="Precio">
            </div>
        </div>
        <div class="field">
            <div class="control">
                <button id="btnGuardar" class="button is-success">Guardar</button>
                <a href="productos.php" class="button is-warning">Volver</a>
            </div>
        </div>
    </div>
</div>
<script src="js/editar_producto.js"></script>

Y en cuanto al código de JavaScript queda así:

const $nombre = document.querySelector("#nombre"),
    $descripcion = document.querySelector("#descripcion"),
    $precio = document.querySelector("#precio"),
    $btnGuardar = document.querySelector("#btnGuardar");

// Una global para establecerla al rellenar el formulario y leerla al enviarlo
let idProducto;

const rellenarFormulario = async () => {

    // https://parzibyte.me/blog/2020/08/14/extraer-parametros-url-javascript/
    const urlSearchParams = new URLSearchParams(window.location.search);
    idProducto = urlSearchParams.get("id"); // <-- Actualizar el ID global
    // Obtener el producto desde PHP
    const respuestaRaw = await fetch(`./obtener_producto_por_id.php?id=${idProducto}`);
    const producto = await respuestaRaw.json();
    // Rellenar formulario
    $nombre.value = producto.nombre;
    $descripcion.value = producto.descripcion;
    $precio.value = producto.precio;
};

// Al incluir este script, llamar a la función inmediatamente
rellenarFormulario();

$btnGuardar.onclick = async () => {
    // Se comporta igual que cuando guardamos uno nuevo
    const nombre = $nombre.value,
        descripcion = $descripcion.value,
        precio = parseFloat($precio.value);
    // Pequeña validación, aunque debería hacerse del lado del servidor igualmente, aquí es pura estética
    if (!nombre) {
        return Swal.fire({
            icon: "error",
            text: "Escribe el nombre",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }
    if (!descripcion) {
        return Swal.fire({
            icon: "error",
            text: "Escribe la descripción",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }

    if (!precio) {
        return Swal.fire({
            icon: "error",
            text: "Escribe el precio",
            timer: 700, // <- Ocultar dentro de 0.7 segundos
        });
    }
    // Lo que vamos a enviar a PHP. También incluimos el ID
    const cargaUtil = {
        id: idProducto,
        nombre: nombre,
        descripcion: descripcion,
        precio: precio,
    };
    // Codificamos...
    const cargaUtilCodificada = JSON.stringify(cargaUtil);
    // Enviamos
    try {
        const respuestaRaw = await fetch("actualizar_producto.php", {
            method: "PUT",
            body: cargaUtilCodificada,
        });
        // El servidor nos responderá con JSON
        const respuesta = await respuestaRaw.json();
        if (respuesta) {
            // Y si llegamos hasta aquí, todo ha ido bien
            // Esperamos a que la alerta se muestre
            await Swal.fire({
                icon: "success",
                text: "Producto actualizado",
                timer: 700, // <- Ocultar dentro de 0.7 segundos
            });
            // Redireccionamos a todos los productos
            window.location.href = "./productos.php";
        } else {
            Swal.fire({
                icon: "error",
                text: "El servidor no envió una respuesta exitosa",
            });
        }
    } catch (e) {
        // En caso de que haya un error
        Swal.fire({
            icon: "error",
            title: "Error de servidor",
            text: "Inténtalo de nuevo. El error es: " + e,
        });
    }
};

Poniendo todo junto

Lo que te he mostrado a través de todo el post solo ha sido el código más importante de todo este proyecto. El código completo y actualizado lo dejaré, como siempre, en mi GitHub.

Recuerda que te he dejado enlaces interesantes a lo largo del artículo por si quieres profundizar más en el tema. De igual modo te dejo algunas cosas que pueden gustarte:

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • Hola, estuve probando tu código y funciona muy bien, gracias por compartirlo, me gustaría saber como agregar para que el sweet alert me avise que estoy ingresando datos duplicados y que no lo guarde.

    Muchas gracias 🙂

  • Hola, el post es realmente interesante, te felicito por el contenido y las explicaciones. No veo pero quizá está alguna validación aplicada, es decir, cómo asegurarse que alguien externo no aprenda cómo funcionan los parámetros en los PHP viéndolos en los JS y los ejecute directamente en el navegador... por ejemplo el de borrado metiendo ids aleatorios, o bien esto no está por simplificar y está en algún otro tutorial.

    • Hola. Así es, es para simplificar pues el tutorial no está enfocado en validación.
      Gracias por su comentario. Saludos!

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

17 horas hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

1 semana hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

1 semana hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

1 semana hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

1 semana hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

1 semana hace

Esta web usa cookies.