Consultas con PDO extension en PHP (Mysql database)
En esta ocasión veremos como utilizar la extensión PDO para hacer consultas a tu base de datos, tanto como para insertar, editar, borrar, seleccionar.
Veremos muchos de los diferentes métodos internos de la clase de esta extensión, aunque se recomienda también ver en la página oficial de PHP estos métodos, ya que otro método puede que facilite la operación que queramos más eficientemente.
En otra sección futura tal vez expliquemos método por método su funcionalidad.
A diferencia de los ejemplos de MySQLi que la idea era que interactué creando código en esta sección se dará por completo como se hace.
Si por alguna razón no sabes como se hacia algo en el post consultas-con-mysqli-en-php de mysqli extesion puedes apoyarte en este post.
Introducción
Para ver estas diferentes formas de interactuar con nuestra base de datos reciclaremos los formularios que habíamos realizado en consultas con mysqli en PHP borrando la parte de PHP ya que sera toda nueva.
Nuestros archivos serán los siguientes:
- pdoindex.php
- pdoborrar.php
- pdomodificar.php (obtiene datos para el formulario de editar)
- pdoinsertar.php
- pdoupdate.php (esta es la que realmente hace el cambio en base de datos).
Reciclando nuestro index obtendremos el siguiente código y lo guardaremos como pdoindex.php
<style> form{ width:420px; background-color:lightgrey; padding:7px; } div.form-section{ margin:4px; text-align:right; } label{ width:100px; display:inline-block; } .form-section input{ display:inline-block; width:300; } .form-button{ margin:4px; text-align:right; } .form-table{ width:510px; padding:0; margin:0; } .form-cels{ width:19.333%; display:inline-block; } </style> <form method="POST" action="pdoinsertar.php"> <h2>New Product</h2> <div class="form-section"> <label>product</label> <input type="text" name="product"> </div> <div class="form-section"> <label>Price</label> <input type="text" name="price"> </div> <div class="form-section"> <label>quantity</label> <input type="text" name="quantity"> </div> <div class="form-button"> <input type="submit" value="confirm"> </div> </form> <hr> <div class="form-table"> <div class="form-cels">id</div> <div class="form-cels">product</div> <div class="form-cels">price</div> <div class="form-cels">quantity</div> <div class="form-cels">selector</div> <div class="form-button"> <button onclick="borrar()">borrar</button> </div> </div> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <script> function borrar(){ var sarray=new Array(); let selected = document.querySelectorAll("input[name='selected']:checked"); for (let x of selected){ alert(x.value); sarray.push(x.value); } $.ajax({ method: "POST", url: "pdoborrar.php", data: { "s": sarray} }) .done(function( msg ) { alert(msg); alert("los productos fueron borrados con exito"); }); } </script>
En breve veremos la parte de inserción de datos en la base de datos, desde el comienzo front-end hasta el final donde se hace la inserción de datos con PHP PDO extension.
Pero primero veamos como mostrar datos en nuestro pdoindex.php, en mi caso estoy poniendo pdo delante de cada nombre para saber que archivos estoy utilizando, ya que hago múltiples pruebas.
[ADS_A1/]mostrar datos
Como podemos ver en lo que hemos reciclado de nuestro post anterior, este div con clase form-table sera el que muestre la información de los productos que tenemos y dará opciones tanto como para editar o borrar.
pdoindex.php<div class="form-table"> <div class="form-cels">id</div> <div class="form-cels">product</div> <div class="form-cels">price</div> <div class="form-cels">quantity</div> <div class="form-cels">selector</div> <!-- El proximo fragmento de código va aqui --> <div class="form-button"> <button onclick="borrar()">borrar</button> </div> </div>
Ahora modificaremos este código para que muestre todos los productos de nuestra tabla products de nuestra base de datos.
En el siguiente código vera como conectarse a una base MySQL utilizando la extension PDO de PHP, ejecutar una consulta con query y recorrerla.
Si el siguiente código puede ser mejorado para que no quede todo junto, en particular a mi no me gusta tener la parte de base de datos junto con la parte de HTML. Me gusta mas aplicar un patrón de diseño MVC separando ambas cosas ademas de tener una tercera capa que comunica entre ambas.
En esta ocasión no estamos utilizando objetos para hacerlo lo mas simple posible. Aunque quedaría mas ordenado utilizando objetos.
pdoindex.php<?php $servername = "localhost"; $username = "root"; $password = ""; $database = "ejemplomysqli"; try { $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo "Connection failed: " . $e->getMessage(); exit; } $query = $conn->query("SELECT * FROM products"); if ($query->execute()) { $result = $query->fetchAll(); foreach ($result as $fila){?> <div class="form-cels"><?php echo $fila['id']; ?></div> <div class="form-cels"><?php echo $fila['product'] ; ?></div> <div class="form-cels"><?php echo $fila['price'] ; ?></div> <div class="form-cels"><?php echo $fila['quantity']; ?></div> <div class="form-cels"> <input type="checkbox" name="selected" value="<?php echo $fila['id'] ; ?>"> <button data-selected="<?php echo $fila['id'] ;?>" onclick="edit(this);" >edit</button> </div> <?php } }?>
En el último div podemos observar que hemos puesto tanto el checkbox como el botón.
En el checkbox vea que hemos puesto como valor el id, esto es para que cuando vayamos a borrar sepa que filas borrar en nuestra base.
El botón tiene un evento onclick
para llamar a la función edit, la cual tiene como parámetro a this
, que en este caso es el objeto HTML donde está el evento.
Lamentablemente aun no tenemos datos aun ingresados para ver como se vería nuestra tabla.
[ADS_A2/]Agregando datos a nuesta base de datos – Insert
Es probable que notaras que el form para insertar data en nuestra base no posee ninguna manera de que haga la carga en nuestra pagina luego de confirmar en el submit ya que es un submit html.
Con jQuery puedes cambiar este comportamiento para que en vez de que el formulario se ejecute tal como tiene previsto lo haga con ajax. Para ahondar más en este tema puedes verlo aquí https://learn.jquery.com/ajax/ajax-and-forms/
Nosotros en este POST lo utilizaremos pero no daremos muchas explicaciones.
Para nuestro form haremos lo siguiente, cambiaremos esta línea en pdoindex.php
pdoindex.php<form method="POST" action="pdoinsertar.php">
por
pdoindex.php<form id="form">
y agregaremos a nuestro script de javascript lo siguiente:
pdoindex.php$("#form").submit(function(e){ var datos = $( "#form" ).serialize(); $.ajax({ method: "POST", url: "pdoinsertar.php", contentType:'application/x-www-form-urlencoded', data: datos, dataType:"json", statusCode: { 404: function() { console.log("page not found"); }, 405:function(){ console.log("Method Not Allowed"); }, 500:function(){ console.log("Internal Server Error"); } }, error: function(xhr, status, error){ var errorMessage = xhr.status + ': ' + xhr.statusText alert('Error - ' + errorMessage); } }) .done(function( response ) { alert(response.message); refreshtable(response.result); }) e.preventDefault(); return false; });
Esta función que correrá cuando le damos a submit
en nuestro formulario de agregar puede ver que en el final de la misma pose e.preventDefault();
esto evita que corra nuestro formulario como lo hace normalmente.
Si preventDefault
y return false
no estuvieran, nuestro browser trataría de enviar nuestro formulario HTML.
También podemos ver que nuestra consulta AJAX espera que le devolvamos un objeto JSON
Ahora necesitaremos nuestro archivo para ingresar nuestros datos a nuestra base, en mi caso le llamare pdoinsertar.php
pdoinsertar.php<?php header('Content-Type: application/json; charset=utf-8'); $servername = "localhost"; $username = "root"; $password = ""; $database = "ejemplomysqli"; try { $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo json_encode(["message" => "Connection failed: " . $e->getMessage()]); exit; } $message = ""; $data = []; $statement = $conn->prepare("INSERT INTO products(product,price,quantity) VALUES (:product,:price,:quantity)"); $statement->bindParam(':product', $_POST['product'], PDO::PARAM_STR); $statement->bindParam(':price', $_POST['price'], PDO::PARAM_INT); $statement->bindParam(':quantity', $_POST['quantity'], PDO::PARAM_INT); if ($statement->execute()) { $message = "el producto " . $_POST['product'] . " se ha agregado con exito"; $query = $conn->query("SELECT * FROM products"); if ($query->execute()) { $result = $query->fetchAll(); echo json_encode(compact("message","result")); exit; }else{ echo json_encode(["message" => "i- could not get table data"]); exit; } } else { $message = "el producto no se ha podido agregar"; }
En la primera línea tenemos a header que sería el encabezado de nuestro archivo, recordemos que no puede ser algo que no sea JSON en esta ocasión.
Luego tenemos nuestra conexión a base de datos, y después de esto está lo que realmente queremos ver.
También tenemos a query y $message
que los devuelve como JSON a nuestro AJAX. Veamos como manejamos esta información en el AJAX jQuery.
Para que no tengas que volver a subir he puesto el código que esta dentro de .done
pdoindex.php (.done en $(«#form»).submit)alert(response.message); refreshtable(response.result);
Vea que estamos obteniendo nuestro mensaje tal y como lo hacemos en un JSON, no hay misterio alguno en esto. Y también tenemos una llamada a una función refreshtable(response.result)
A continuación la función refreshtable, la cual actualiza la tabla:
pdoindex.phpfunction refreshtable(result){ var table = document.querySelector(".form-table"); while (table.firstChild) { table.removeChild(table.firstChild); } if (typeof result !== 'undefined') { try { for (fila of result){ var div0 = document.createElement("div"); div0.classList.add("form-cels"); div0.textContent = fila.id; var div1 = document.createElement("div"); div1.classList.add("form-cels"); div1.textContent = fila.product; var div2 = document.createElement("div"); div2.classList.add("form-cels"); div2.textContent = fila.price; var div3 = document.createElement("div"); div3.classList.add("form-cels"); div3.textContent = fila.quantity; var div4 = document.createElement("div"); div4.classList.add("form-cels"); var selector = document.createElement("input"); var button = document.createElement("button"); button.setAttribute("data-selected",fila.id) button.setAttribute("onclick","edit(this)"); button.textContent = "edit"; selector.setAttribute('type', 'checkbox'); selector.setAttribute('name','selected'); selector.setAttribute('value',fila.id); div4.appendChild(selector); div4.appendChild(button); c = [div0,div1,div2,div3,div4] for(var i=0;i<5;i++){ table.appendChild(c[i]); } } div5 = document.createElement("div"); div5.classList.add("form-button"); bborrar = document.createElement("button"); bborrar.textContent = "delete"; bborrar.setAttribute("onclick","borrar()"); div5.appendChild(bborrar); table.appendChild(div5); } catch (error) { console.error(error); } } }
¿La pregunta aquí es porque no lo he hecho con innerHTML? Por la misma razón por la que probamos prepare contra ataques XSS. Este es un ejemplo de como evitar innerHTML.
[ADS_A1/]Editando datos de nuestra base de datos
Ya vimos en la sección anterior como ingresar datos, en esta ocasión en vez de utilizar serialize para obtener los datos de los inputs lo haremos a JavaScript puro para ver la diferencia.
A mí me gusta más esta manera, ya que vez lo que estás obteniendo, pero esos son gustos.
pdoindex.phpfunction edit(container){ console.log("enter in edit button"); var id = container.getAttribute("data-selected"); console.log(id); $.ajax({ method: "POST", url: "pdomodificar.php", data: { "id":id }, contentType:'application/x-www-form-urlencoded', dataType:"json", statusCode: { 404: function() { console.log("page not found"); }, 405:function(){ console.log("Method Not Allowed"); }, 500:function(){ console.log("Internal Server Error"); } } }) .done(function( response ) { console.log(response.message); if(typeof response.result !== 'undefined'){ console.log("dispaly Edit form") let editform = document.querySelector(".hiddenforms"); editform.style.display = 'block'; document.querySelector('input[name="eproduct"]').value = response.result.product; document.querySelector('input[name="eprice"]').value = response.result.price; document.querySelector('input[name="equantity"]').value = response.result.quantity; document.querySelector('input[name="eid"]').value = response.result.id; } }); return false; }
El código anterior, a diferencia de lo que puedes estar pensando si es que no lo leíste, hace una consulta a base de datos y devuelve los datos del producto a cambiar.
De esta manera podremos cambiar el dato del producto que queramos rapidamente.
En este caso el return false
del final no sería necesario es un residuo que quedo de reciclar la funcion que usábamos en insert.
El archivo pdomodificar.php simplemente obtiene el resultado de hacer un SELECT en nuestra base de datos de nuestro producto, con este resultado es con el cual rellenamos el formulario de Editar Producto.
pdomodificar.php<?php header('Content-Type: application/json; charset=utf-8'); $conn = baseconnection(); $prepare = $conn->prepare("SELECT * FROM products WHERE id=:id"); $prepare->bindParam(":id",$_POST['id']); $prepare->execute(); $result = $prepare->fetch(PDO::FETCH_ASSOC); $message= "charging form"; echo json_encode(compact('message','result')); exit(); function baseconnection():PDO|PDOException{ $servername = "localhost"; $username = "root"; $password = ""; $database = "ejemplomysqli"; try { $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo json_encode(["message"=>"Connection failed: " . $e->getMessage()]); exit(); } return $conn; }
veamos el formulario que rellenamos con nuestro datos:
pdoindex.php<div class="hiddenforms"> <div id="editProduct" class="form"> <h2>Edit Product</h2> <div class="form-section"> <label>product</label> <input type="text" name="eproduct"> </div> <div class="form-section"> <label>Price</label> <input type="text" name="eprice"> </div> <div class="form-section"> <label>quantity</label> <input type="text" name="equantity"> </div> <input type="hidden" value="" name="eid"> <div class="form-button"> <button onclick="editClose()">close</button> <button onclick="editConfirm()">confirm</button> </div> </div> </div>
una vez el formulario se vea cuando corre el código editform.style.display = 'block';
podremos modificar nuestro producto y podremos darle a los botones y
El boton close llamara a la funcion editClose()
y el botón confirm llamara a editConfim()
.
editClose()
cumple una funcionalidad simple y es no dejar visible nuestro formulario:
function editClose(){ let editform = document.querySelector(".hiddenforms"); editform.style.display = 'none'; }
Por otro lado tenemos el boton de confirmar, y este si hara la peticion para modificar nuestros datos en la base de datos.
pdoindex.phpfunction editConfirm(){ let product = document.querySelector('input[name="eproduct"]').value; let price = document.querySelector('input[name="eprice"]').value; let quantity = document.querySelector('input[name="equantity"]').value; let id = document.querySelector('input[name="eid"]').value; $.ajax({ method: "POST", url: "pdoupdate.php", data: { "id":parseInt(id), "product":product, "quantity":parseInt(quantity), "price":price }, contentType:'application/x-www-form-urlencoded', dataType:"json", statusCode: { 404: function() { console.log("page not found"); }, 405:function(){ console.log("Method Not Allowed"); }, 500:function(){ console.log("Internal Server Error"); } } }) .done(function( response ) { alert(response.message); refreshtable(response.result); let editform = document.querySelector(".hiddenforms"); editform.style.display = 'none'; }); }
De nuevo no estamos utilizando serialize, ya que no es un form HTML, sobre los statusCode puede ver que diferentes status existen en http status code
Como podemos ver mandamos nuestra data(datos) a un archivo pdoupdate.php este procesara los datos y devolverá un resultado el cual sera nuestro response en .done
Luego recargaremos la tabla y cerraremos el formulario de editar nuestro producto.
Modificando los datos en la base de datos – UPDATE
Ahora nos toca ver nuestro pdoupdate.php:
pdoupdate.php<?php header('Content-Type: application/json; charset=utf-8'); $message=""; $product = $price = $quantity = null; if(isset($_POST['product'])){ $product = $_POST['product']; } if(isset($_POST['price'])){ if(is_numeric($_POST['price'])){ $price = $_POST['price']; } } if(isset($_POST['quantity'])){ if(is_numeric($_POST['quantity'])){ $quantity = $_POST['quantity']; } } //TODO:crear el proceso de UPDATE en la base de datos if(isset($_POST['id'])){ if(!is_numeric($_POST['id'])){ $message = "(-_-) stop right there / {$_POST['id']} - ".gettype($_POST['id']); echo json_encode(compact("message")); exit(); } $conn = baseconnection(); $prepare=$conn->prepare("UPDATE products SET product = :product, price= :price, quantity=:quantity WHERE id=:id"); $prepare->bindParam(":product",$product); $prepare->bindParam(":price",$price); $prepare->bindParam(":quantity",$quantity); $prepare->bindParam(":id",$_POST['id']); if($prepare->execute()){ $message = "El producto $product fue modificado con exito"; }else{ $message = "No se pudo modificar el producto"; } $prepare = null; getproducts($conn,$message); } else{ $message = "m- no element selected"; echo json_encode(compact("message")); exit(); } //TODO: creaar el proceso de SELECT para hacer una recarga a la lista function getproducts($conn,string $message = ""){ $query = $conn->query("SELECT * FROM products"); if ($query->execute()) { $result = $query->fetchAll(); echo json_encode(compact("message","result")); $conn = null; exit; } else { echo json_encode(["message" => "m- could not get table data"]); $conn = null; exit; } } function baseconnection():PDO|PDOException{ $servername = "localhost"; $username = "root"; $password = ""; $database = "ejemplomysqli"; try { $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo "Connection failed: " . $e->getMessage(); exit; } return $conn; }
Como podemos ver en este archivo pdoupdate.php tenemos primero una validación aunque no cumple para validar contra un ataque XSS, para ello podríamos utilizar la función htmlentities() donde se espera un string.
Podemos ver también el uso de la funcion is_numeric()
para chequear que el string es un número.
En la parte en que creamos él prepare statement podemos ver lo siguiente:
pdoupdate.php$prepare=$conn->prepare("UPDATE products SET product = :product, price= :price, quantity=:quantity WHERE id=:id"); $prepare->bindParam(":product",$product); $prepare->bindParam(":price",$price); $prepare->bindParam(":quantity",$quantity); $prepare->bindParam(":id",$_POST['id']);
¿que falta aqui? ya habiamos visto bindParam antes
Si estás pensando en que no estamos chequeando los datos que le estamos pasando. Chequeemos ahora la sintaxis de bindParam()
bindParam y los PDO::Param_
Sintaxispublic PDOStatement::bindParam( mixed $parameter, mixed &$variable, int $data_type = PDO::PARAM_STR, int $length = ?, mixed $driver_options = ? ): bool
Como podemos ver tenemos 5 posibles parametros para pasarle a bindParam donde 3 son opcionales, si en estos 3 no definimos sus valores tomara valores por defecto.
Por lo tanto, en nuestro ejemplo tenemos un gran problema y es que podríamos estar pasándole strings en vez de números. Veamos los posibles $data_type
que tenemos:
PDO::PARAM_BOOL
PDO::PARAM_NULL
PDO::PARAM_INT
PDO::PARAM_STR
PDO::PARAM_STR_NATL
(NATL – national character set PHP 7.2.0)PDO::PARAM_STR_CHAR
(PHP 7.2.0)PDO::PARAM_LOB
PDO::PARAM_STMT
PDO::PARAM_INPUT_OUTPUT
Este archivo devolverá un JSON tal como hacíamos en insertar, pero antes de seguir note el exit() una función que no explicamos en el ejemplo anterior, esta función no permite que se siga ejecutando el código en el archivo donde estamos.
[ADS_FD/]Borrar datos de nuestra base.
por ultimo tenemos la parte de borrar datos en nuestra base de datos en este ejemplo borramos productos.
El proceso de borrar comienza desde la selección de los productos en la tabla o lista como quieras verlo, siguiendo por el botón borrar que al hacer click sobre este comenzara el pedido AJAX para borrar los productos seleccionados tal y como lo harías en un carrito de compras por ejemplo a diferencia que en esa ocasión no interactuaremos solo con la base de datos.
Primero que nada tenemos que analizar la funcion borrar()
que se acciona al clickear sobre el boton.
function borrar(){ var sarray=new Array(); let selected = document.querySelectorAll("input[name='selected']:checked"); for (let x of selected){ sarray.push(x.value); } $.ajax({ method: "POST", url: "pdoborrar.php", data: { "s": sarray}, contentType:'application/x-www-form-urlencoded', dataType:"json", statusCode: { 404: function() { console.log("page not found"); }, 405:function(){ console.log("Method Not Allowed"); }, 500:function(){ console.log("Internal Server Error"); } } }) .done(function( response ) { alert(response.message); refreshtable(response.result); }); return false; }
A diferencia de insert y edit en ele caso de borrar debere crear un array con los ids a borrar y es lo que hacemos.
pdoindex.php (primeras líneas de borrar())var sarray = new Array(); let selected = document.querySelectorAll("input[name='selected']:checked"); for (let x of selected){ sarray.push(x.value); }
Como puede ver utilizamos un for para recorrer los objetos HTML y obtener los valores de estos, luego los agregamos a nuestro array con push()
.
En la petición AJAX pasamos este array a pdoborrar.php, una vez procesado obtenemos tanto el mensaje como el resultado.
Miremos ahora el archivo pdoborrar.php:
pdoborrar.php<?php header('Content-Type: application/json; charset=utf-8'); $message=""; if(isset($_POST['s'])){ $conn = baseconnection(); foreach ($_POST['s'] as $item){ $prepare=$conn->prepare("DELETE FROM products WHERE id=:id"); $prepare->bindParam(":id",$item); $prepare->execute(); } $prepare = null; $message = "todos los elementos seleccionados fueron borrados con exito"; $query = $conn->query("SELECT * FROM products"); if ($query->execute()) { $result = $query->fetchAll(); echo json_encode(compact("message","result")); $conn = null; exit; } else { echo json_encode(["message" => "b- could not get table data"]); $conn = null; exit; } } else{ $message = "variable s not setted"; echo json_encode(compact("message")); } function baseconnection(){ $servername = "localhost"; $username = "root"; $password = ""; $database = "ejemplomysqli"; try { $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo "Connection failed: " . $e->getMessage(); exit; } return $conn; }
Como puede ver las acciones en esta parte son muy parecidas a las que realizamos en las consultas INSERT y UPDATE excepto la utilización de foreach
para borrar cada uno de estos elementos.
Esto está claro, lo podríamos hacer de otra manera, hay maneras en SQL para hacer esta clase de cosas sin tener que hacer este foreach
.
Dejando cosas en claro (ataques XSS)
En este ejemplo podemos ver que utilizamos prepare que esta bien nos garantiza que no nos vayan a hacer un ataque de inyeccion sql ¿pero nos garantizara que no nos puedan hacer un ataque XSS?
Del todo, sabemos que no, no estamos haciendo una protección CSRF en nuestro form y, por lo tanto, no podemos saber de donde está viniendo nuestra información.
Pero por otro lado prepare cuando estamos ingresando un string nos protege realmente.
¿Me permitirá ingresar un script?
Por ejemplo si ingreso:
<script>alert("ma ma mia");</script>
Hara un alert? si lo hace estamos en problemas. Y si lo hace, el problema de ataques XSS es comun hoy en dia y deberemos tener precauciones para esto.
Que podemos hacer para evitar esto?
Tenemos muchas opciones, podríamos usar el paquete htmlpurifier por ejemplo. O si queremos tener una solución más simple que esta, es utilizar la función
htmlentities()
.