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.
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.
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.
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)
);
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.
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.
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.
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 <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.
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.
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,
});
}
};
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:
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
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. Gracias por sus comentarios :)
Si tiene alguna duda puede hacérmela llegar en https://parzibyte.me/#contacto
Saludos!
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!