Índice de contenido
ToggleIntroducción al uso de MySQLi en PHP
Si bien, muchas veces realizamos nuestros proyectos utilizando algún framerwork de programación como: Codeigniter, Laravel o Simfony. Muchas otras veces necesitamos programar algunas funciones de forma nativa por adaptación al código que ya existe o porque la propia aplicación que estamos creando o modificando no esta programada mediante framerwork por diversos motivos en los que no voy a entrar ahora, ya que estoy depende de cada empresa, su sistema de flujo o incluso un desarrollo mas personalizado que requiere un mayor control del core.
Y aunque es cierto, que para muchos programadores que acaban de empezar las declaraciones preparadas pueden parecer algo intimidantes o complicadas al principio, una vez que te acostumbras a trabajar con estos métodos, comenzarás a utilizarlos de forma natural en todos los proyectos donde los necesites. El objetivo de este tutorial es ayudar a aquellas personas que tienen poco o ningún conocimiento sobre declaraciones preparadas, a ser un total experto en esta materia.
Descargo de responsabilidad: en realidad, aunque las declaraciones preparadas son una parte fundamental de la seguridad de sus consultas a la base de datos. Cuando se trata de seguridad, nunca debe ser una persona conformista, sin importar cuán seguro pueda creer que es su sistema.
¿Cómo funciona una inyección SQL?
Antes de realizar los ejemplos de declaraciones preparadas, es importante analizar y comprender el uso de las inyecciones SQL dentro de un sistema informático, ya que si no somos capaces de comprender o visualizar el fallo, no seremos capaces de corregir o crear otros puntos de seguridad en nuestros sistemas.
En una llamada normal de MySQL, haría algo como:
$name = $_POST['name']; $mysqli->query("SELECT * FROM myTable WHERE name='$name'");
El problema con esto, es que si se basa en la entrada del usuario, como en el ejemplo, un usuario malintencionado podría hacer un ' OR '1'='1
. Haciendo que está afirmación siempre se evaluada como verdadera, ya que 1=1
. En este caso, el usuario malintencionado ahora tiene acceso a toda su tabla. Solo imagine lo que podría pasar si se tratara de una consulta con DELETE
.
Este sería el ejemplo con la consulta modificada por el atacante:
SELECT * FROM myTable WHERE name='' OR '1'='1'
Un hacker podría hacer mucho daño en su sitio web o aplicación si sus consultas se configuran de esta manera. Una solución fácil a esto sería hacer:
$name = $mysqli->real_escape_string($_POST['name']); $mysqli->query("SELECT * FROM myTable WHERE name='$name'");
Observe la similitud con el primer ejemplo, todavía agregué comillas al valor de la columna. Sin comillas, las cadenas siguen siendo igualmente susceptibles a la inyección de SQL . Si va a utilizar una cláusula LIKE, también debería realizar addcslashes($escaped, '%_')
, ya que mysqli::real_escape_string
no lo hará como se indica aquí .
Esto cubre el string, como lo indica el nombre de la función, pero ¿qué pasa con los números? Podría hacerlo utilizando: (int)$mysqli->real_escape_string($_POST['name'])
, lo que sin duda funcionaría, pero eso es redundante. Si está convirtiendo la variable a un int, no necesita escapar el string. Ya le está diciendo que esencialmente se asegure de que el valor sea un número entero. Hacer (int)$_POST['name']
sería suficiente. Como es un número entero, obviamente tampoco es necesario agregar comillas a la columna: name de la llamada SQL
.
En realidad, si sigue estas instrucciones a la perfección, debería ser suficiente para usar mysqli::real_escape_string para sus string
y (int)$var para sus números enteros
. Simplemente no olvide establecer el conjunto de caracteres predeterminado. Esto se puede configurar en php.ini (debería ser el valor predeterminado) como default_charset = "utf-8"
y usando $mysqli->set_charset('utf8mb4')
en cada página que usa $mysqli->real_escape_string()
. Pero solo para las cadenas o números que son valores en una declaración WHERE o valores de una columna ; no use esto para nombres de tabla / columna o palabras clave SQL ya que ocasionara un error en el código.
De todos modos, todavía sugiero el uso de declaraciones preparadas, ya que son claramente más adecuadas para proteger las consultas contra inyecciones de SQL y menos propensas a errores. Ya que no tiene que preocuparse sobre el formato de la consulta, en su lugar sólo debe reemplazar los marcadores de posición con los valores correspondiente. Por supuesto esto no quiere decir que usted no deba filtrar sus variables y desinfectar sus valores de código malicioso para evitar otros ataques como: ataques de inyección XSS sobre nuestra aplicación.
¿Cómo funcionan las declaraciones preparadas de MySQLi?
De manera simple, así es como funcionan las declaraciones preparadas de MySQLi en PHP:
- Prepare una consulta SQL con valores vacíos como marcadores de posición (con un signo de interrogación para cada valor).
- Vincula las variables a los marcadores de posición indicando cada variable, junto con su tipo.
- Ejecutar la solicitud.
Los cuatro tipos de variables permitidos:
- i – Para números enteros (int)
- d – Para números flotantes o decimales (double)
- s – Para cadenas de texto (string)
- b – Para datos (blob)
Una declaración preparada, como su nombre lo indica, es una forma de preparar la llamada MySQL, sin almacenar las variables. Usted le dice que las variables irán allí eventualmente, pero todavía no, de esta manera el sistema sabe en todo momento el tipo de variable que le va a llegar: si es un número, un string u otro tipo de dato. La mejor manera de demostrarlo es con el ejemplo:
$stmt = $mysqli->prepare("SELECT * FROM myTable WHERE name = ? AND age = ?"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); //fetching result would go here, but will be covered later $stmt->close();
Si nunca antes has visto declaraciones preparadas, esto puede parecer un poco extraño. Básicamente, lo que sucede es que está creando una plantilla para lo que será la instrucción SQL. En este caso, estamos seleccionando todo desde myTable
, dónde name
e age es
igual a ?
. El signo de interrogación es solo un marcador de posición que indica donde va la variable, estás deben presentarse en orden y tipo dentro del bind_param.
El método bind_param()
es donde adjunta variables a los valores ficticios en la plantilla preparada. Observe cómo hay dos letras entre comillas antes de las variables. Esto le dice a la base de datos los tipos de variables. Los s especifican
que nombre será un valor de cadena, mientras que la i
identifica a “age” como un número entero. Esta es precisamente la razón por la que no agregué comillas alrededor del signo de interrogación para el nombre, como normalmente lo haría para una cadena en una llamada SQL. Probablemente pensaste que lo olvidé, pero la realidad es que simplemente no hay necesidad de hacerlo (de hecho, en realidad no funcionará si pones comillas entre el signo de interrogación ?
, ya que se tratará como una cadena literal, en lugar de un marcador de posición ficticio).
Ya le está diciendo que será una cadena literal cuando llame abind_param()
, por lo que incluso si un usuario malintencionado intenta insertar SQL en sus formularios, seguirá siendo tratado como una cadena. El método $stmt->execute() sirve para ejecutar la consulta
; la última línea simplemente cierra la declaración preparada.
¿Cómo crear una conexión con MySQLi en PHP?
Crear una conexión de MySQLi es bastante simple. Te sugiero nombrar un archivo como:mysqli_connect.php
y colocar este archivo fuera de su raíz directamente (html, public_html) para que sus credenciales sean seguras. También utilizaremos el manejo de excepciones, mediante la utilización mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT). Esto puede parecer extraño, especialmente si nunca antes ha usado un operador bit a bit . Pero todo lo que hace es informar de todos los errores, mientras los convierte en excepciones, utilizando la clase mysqli_sql_exception.
$mysqli = new mysqli("localhost", "username", "password", "databaseName"); if($mysqli->connect_error) { exit('Error connecting to database'); //Should be a message a typical user could understand in production } mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $mysqli->set_charset("utf8mb4");
Si prefiere usar set_exception_handler () en lugar de try/catch
, puede hacer lo siguiente para evitar el anidamiento. Si está utilizando este método, debe comprender que afectará a todas las páginas en las que está incluido. Por lo tanto, debe reutilizar la función nuevamente con un mensaje personalizado para cada página o usar restore_exception_handler () para volver a la función integrada.
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); set_exception_handler(function($e) { error_log($e->getMessage()); exit('Error connecting to database'); //Should be a message a typical user could understand }); $mysqli = new mysqli("localhost", "username", "password", "databaseName"); $mysqli->set_charset("utf8mb4");
Hay una muy seria repercusión del uso mysqli_report()
, que es que informará la información confidencial de su base de datos. Tiene tres opciones para seguir usándolo, pero no para informar su contraseña.
- También puede usarlo
mysqli_report()
estrictamente en todo, excepto para crear la conexión, si lo hace con el primer método que mostré$mysqli->connect_error
(no se muestra la contraseña) y solo se colocamysqli_report()
despuésnew mysqli()
. - Si llama
mysqli_report()
antes de crear una conexión, debe asegurarse de que esté en un bloque try / catch e imprima específicamente en su registro de errores$e->getMessage()
, no$e
, que aún contiene su información confidencial . Obviamente, esto se aplica estrictamente al constructor. - Use
set_exception_handler()
de la misma manera que el punto 2 y use$e->getMessage()
.
Recomiendo encarecidamente hacer uno de estos métodos. Incluso si es diligente y se asegura de que todos sus errores solo figuren en su registro de errores, personalmente no veo por qué alguien necesitaría registrar su contraseña. Ya sabrá cuál es el problema de todos modos.
¿Cómo insertar, actualizar o eliminar con MySQLi en PHP?
Insertar, actualizar y eliminar tienen una sintaxis idéntica, por lo que se combinarán.
Ejemplo de una consulta para insertar datos:
$stmt = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); $stmt->close();
Ejemplo de una consulta para actualizar datos:
$stmt = $mysqli->prepare("UPDATE myTable SET name = ? WHERE id = ?"); $stmt->bind_param("si", $_POST['name'], $_SESSION['id']); $stmt->execute(); $stmt->close();
Ejemplo de una consulta para eliminar datos:
$stmt = $mysqli->prepare("UPDATE myTable SET name = ?"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); if($stmt->affected_rows === 0) exit('No rows updated'); $stmt->close();
En este caso, verificamos si alguna fila se actualizó. Como referencia, aquí está el uso de los mysqli::$affected_rows
que nos muestra los si han habido filas afectadas o no:
-1 – la consulta devolvió un error; redundante si ya hay manejo de errores paraexecute()
0 : no hay registros actualizados en UPDATE
, no hay filas que coincidan con la cláusula WHERE
o no se ha ejecutado ninguna consulta.
Mayor que 0 : devuelve el número de filas afectadas; comparable a mysqli_result::$num_rows
para SELECT
¿Cómo conseguir filas combinadas?
Un problema común con $mysqli->affectedRows
es que hace imposible saber por qué devolvió un cero en una ACTUALIZACIÓN. Esto se debe al hecho de que imprime la cantidad de filas modificadas, por lo que resulta ambiguo si actualiza sus valores con los mismos datos.
Una característica increíble que es exclusiva de MySQLi y que no existe en PDO es la capacidad de obtener más información sobre una consulta. Técnicamente, puede lograrlo en PDO, pero solo se puede hacer en la conexión, por lo tanto, no puede elegir.
$stmt = $mysqli->prepare("UPDATE myTable SET name = ?"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); $stmt->close(); echo $mysqli->info;
Esto imprimirá:
Rows matched: 1 Changed: 0 Warnings: 0
Considero que esta es una implementación es bastante imprudente, ya que es extremadamente poco elegante usarla tal como está. Afortunadamente, podemos cambiar eso convirtiéndolo en una matriz asociativa. Todo el crédito va para este comentario útil en la documentación de PHP. Si bien el uso mysqli->info
para ACTUALIZACIÓNES es, con mucho, su caso más común, también se puede usar para otros tipos de consultas .
preg_match_all('/(S[^:]+): (d+)/', $mysqli->info, $matches); $infoArr = array_combine ($matches[1], $matches[2]); var_export($infoArr);
Ahora esto generará una matriz.
[‘Rows matched’ => ‘1’, ‘Changed’ => ‘0’, ‘Warnings’ => ‘0’]
¿Cómo obtener la clave primaria insertada en MySQLi con PHP?
$stmt = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); echo $mysqli->insert_id; $stmt->close();
¿Cómo comprobar si la entrada está duplicada?
Esto es útil si crea una restricción única en una tabla, por lo que no se permiten duplicados. Incluso puede hacer esto para varias columnas, por lo que tendrá que ser esa permutación exacta. Si el manejo de excepciones estuviera desactivado, verificaría el código de error con $mysqli->errno
. Con el manejo de excepciones activado, puede elegir entre eso o el método de excepción genérico $e->getCode()
. Tenga en cuenta que esto difiere de PDOException, que imprimirá el SQLSTATE, en lugar del código de error.
Aquí hay una lista de mensajes de error . El código de error para una entrada de fila duplicada de una actualización o inserción es 1062 y SQLSTATE es 23000 . Para verificar específicamente SQLSTATE, debe usar $mysqli->sqlstate
.
try { $stmt = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt->bind_param("si", $_POST['name'], $_POST['age']); $stmt->execute(); $stmt->close(); } catch(Exception $e) { if($mysqli->errno === 1062) echo 'Duplicate entry'; }
Así es como establecería una restricción única:
ALTER TABLE myTable ADD CONSTRAINT unique_person UNIQUE (name, age)
¿Cómo seleccionar datos con MySQLi en PHP?
Todas las declaraciones de selección en consultas preparadas comenzarán aproximadamente de la misma manera. Sin embargo, existe una diferencia clave para almacenar y obtener los resultados. Los dos métodos que existen son get_result()
y bind_result()
.
get_result ()
Este es el más versátil de los dos, ya que se puede utilizar para cualquier escenario. Cabe señalar que esto requiere mysqlnd, que se ha incluido en PHP desde 5.3 y ha sido el controlador nativo predeterminado desde 5.4, como se indica aquí . Dudo que muchas personas estén usando versiones anteriores a esa, por lo que generalmente debería tenerget_result()
sin problemas.
Esto esencialmente expone la API de mysqli_result regular y no preparada. Es decir, que una vez que haga: $result = get_result()
, puede usarlo exactamente de la misma manera que lo haría con $result = $mysqli->query()
.
Ahora puede usar los siguientes métodos para recuperar una fila a la vez o todas a la vez. Estos son solo algunos de los más comunes, pero puede echar un vistazo a toda la clase mysqli_result para todos sus métodos.
Una fila
$result->fetch_assoc()
– Obtener una matriz asociativa$result->fetch_row()
– Obtener una matriz numérica$result->fetch_object()
– Obtener una matriz de objetos
Todos
$result->fetch_all(MYSQLI_ASSOC)
– Obtener una matriz asociativa$result->fetch_all(MYSQLI_NUM)
– Obtener una matriz numérica
$stmt = $mysqli->prepare("SELECT * FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $result = $stmt->get_result(); if($result->num_rows === 0) exit('No rows'); while($row = $result->fetch_assoc()) { $ids[] = $row['id']; $names[] = $row['name']; $ages[] = $row['age']; } var_export($ages); $stmt->close();
Salida:
[22, 18, 19, 27, 36, 7]
bind_result ()
Quizás te estés preguntando, ¿por qué usarlo bind_result()
? Personalmente, considero que es muy inferior a get_result()
en todos los escenarios, excepto cuando se busca una sola fila en variables separadas. Además, antes de que get_result()
existiera y mysqlnd se integrara en PHP, esta era su única opción, razón por la cual podría estar usándolo en una gran cantidad de código heredado.
La parte más molesta sobre el uso bind_result()
es que debe vincular cada columna que seleccione y luego recorrer los valores en un bucle. Obviamente, esto no es ideal para una gran cantidad de valores o para usar *
. Es especialmente molesto usar el selector de estrellas en bind_result()
, ya que ni siquiera sabe cuáles son esos valores sin mirar en la base de datos.
Además, esto hace que su código sea extremadamente imposible de mantener con cambios en las tabla. Por lo general, esto no importará, ya que de todos modos no debería usar el comodín en modo de producción (pero sabes que lo estás haciendo).
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $stmt->store_result(); if($stmt->num_rows === 0) exit('No rows'); $stmt->bind_result($idRow, $nameRow, $ageRow); while($stmt->fetch()) { $ids[] = $idRow; $names[] = $nameRow; $ages[] = $ageRow; } var_export($ids); $stmt->close();
Salida:
[106, 221, 3, 55, 583, 72]
¿Cómo recuperar una matriz asociativa con MySQLi?
Creo que este es el caso de uso más común típicamente. También utilizaré el encadenamiento en lo siguiente, aunque eso obviamente no es necesario.
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $arr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Si necesita modificar el conjunto de resultados, entonces probablemente debería usar un ciclo while con fetch_assoc()
y buscar cada fila de una en una.
$arr = []; $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_assoc()) { $arr[] = $row; } if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Salida:
[
[‘id’ => 27, ‘name’ => ‘Jessica’, ‘age’ => 27],
[‘id’ => 432, ‘name’ => ‘Jimmy’, ‘age’ => 19]
]
En realidad, también puede hacer esto usando bind_result()
, aunque claramente no fue diseñado para ello. Aquí hay una solución inteligente , aunque personalmente siento que es algo que es genial saber que es posible, pero de manera realista no debería usarse.
Obtener una matriz numérica
Esto sigue el mismo formato que una matriz asociativa. Para obtener toda la matriz en un comando, sin bucle, usaría mysqli_result->fetch_all(MYSQLI_NUM)
. Si necesita obtener los resultados en un bucle, debería utilizar mysqli_result->fetch_row()
.
$stmt = $mysqli->prepare("SELECT location, favorite_color, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $arr = $stmt->get_result()->fetch_all(MYSQLI_NUM); if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Y, por supuesto, la adaptación del bucle while.
$arr = []; $stmt = $mysqli->prepare("SELECT location, favorite_color, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_row()) { $arr[] = $row; } if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Salida:
[
[‘Boston’, ‘green’, 28],
[‘Seattle’, ‘blue’, 49],
[‘Atlanta’, ‘pink’, 24]
]
Obtener una sola fila
Personalmente, me resulta más fácil de usar bind_result()
cuando sé, de hecho, que solo buscaré una fila, ya que puedo acceder a las variables de una manera más limpia.
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $stmt->store_result(); if($stmt->num_rows === 0) exit('No rows'); $stmt->bind_result($id, $name, $age); $stmt->fetch(); echo $name; //Output: 'Ryan' $stmt->close();
Ahora puede usar simplemente usar las variables bind_result()
, $name
ya que sabe que solo contendrán un valor, no una matriz.
Aquí está la versión con get_result()
:
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $arr = $stmt->get_result()->fetch_assoc(); if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Entonces usaría la variable como $arr['id']
por ejemplo.
Salida:
[‘id’ => 36, ‘name’ => ‘Kevin’, ‘age’ => 39]
Obtener una matriz de objetos
Esto es muy similar a buscar una matriz asociativa. La única diferencia principal es que accederá a él como $arr[0]->age
. Además, en caso de que no lo supiera, los objetos se pasan por valor, mientras que las matrices son por referencia.
$arr = [] $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE id = ?"); $stmt->bind_param("s", $_SESSION['id']); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_object()) { $arr[] = $row; } if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Salida:
[
stdClass Object [‘id’ => 27, ‘name’ => ‘Jessica’, ‘age’ => 27],
stdClass Object [‘id’ => 432, ‘name’ => ‘Jimmy’, ‘age’ => 19]
]
Incluso puede agregar valores de propiedad a una clase existente también. Sin embargo, debe tenerse en cuenta que existe un posible problema, según este comentario en los documentos de PHP. El problema es que si tiene un valor predeterminado en su constructor con un nombre de variable duplicado, primero buscará el objeto y luego establecerá el valor del constructor, sobrescribiendo así el resultado obtenido. Por extraño que parezca, hubo un “error” de PHP 5.6.21 a 7.0.6 donde esto no sucedería. Aunque esto viola los principios de OOP, a algunas personas les gustaría esta característica, aunque fue un error en ciertas versiones. Algo así como PDO::FETCH_PROPS_LATE
en PDO debe implementarse en MySQLi para darle la opción de elegir.
class myClass {} $arr = []; $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE id = ?"); $stmt->bind_param("s", $_SESSION['id']); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_object('myClass')) { $arr[] = $row; } if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Como dice el comentario, así es como lo haría correctamente. Todo lo que necesita es una condición if simple para verificar si la variable es igual al valor del constructor; si no es así, simplemente no la configure en el constructor. Esto es esencialmente lo mismo que usar PDO::FETCH_PROPS_LATE
en PDO.
class myClass { private $id; public function __construct($id = 0) { if($this->id === 0) $this->id = $id; } } $arr = []; $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE id = ?"); $stmt->bind_param("s", $_SESSION['id']); $stmt->execute(); $result = $stmt->get_result(); while($row = $result->fetch_object('myClass')) { $arr[] = $row; } if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Otro comportamiento inesperado, pero potencialmente útil, de usar fetch_object('myClass')
es que puede modificar variables privadas. Realmente no estoy seguro de cómo me siento con esto, ya que parece violar los principios de encapsulación y no me crea confianza al respecto.
Conclusión
bind_result () : se utiliza mejor para recuperar una sola fila sin demasiadas columnas o *
; extremadamente poco elegante para matrices asociativas.
get_result () : es el preferido para casi todos los casos de uso.
Extras:
Probablemente piense que podría hacer algo como:
$stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE Name LIKE %?%");
Pero esto no está permitido. El ?
marcador de posición debe ser la cadena completa o el valor literal entero. Así es como lo harías correctamente.
$search = "%{$_POST['search']}%"; $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name LIKE ?"); $stmt->bind_param("s", $search); $stmt->execute(); $arr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); if(!$arr) exit('No rows'); var_export($arr); $stmt->close();
Uso de WHERE IN en consultas preparadas:
Esto es definitivamente algo que me gustaría ver mejorado en MySQLi. Por ahora, en WHERE IN
es posible usar declaraciones preparadas con MySQLi , pero se hace un poco largo.
Nota al margen : Los siguientes dos ejemplos usan el operador splat para desempaquetar argumentos, que requiere PHP 5.6+. Si está utilizando una versión inferior a esa, puede sustituirla por call_user_func_array()
.
$inArr = [12, 23, 44]; $clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks $types = str_repeat('i', count($inArr)); //create 3 ints for bind_param $stmt = $mysqli->prepare("SELECT id, name FROM myTable WHERE id IN ($clause)"); $stmt->bind_param($types, ...$inArr); $stmt->execute(); $resArr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); if(!$resArr) exit('No rows'); var_export($resArr); $stmt->close();
Con otros marcadores de posición
El primer ejemplo mostró cómo usar la cláusula WHERE IN
con marcador de posición ficticio únicamente dentro de ella. ¿Qué pasa si desea utilizar otros marcadores de posición en diferentes lugares?
$inArr = [12, 23, 44]; $clause = implode(',', array_fill(0, count($inArr), '?')); //create 3 question marks $types = str_repeat('i', count($inArr)); //create 3 ints for bind_param $types .= 'i'; //add 1 more int type $fullArr = array_merge($inArr, [26]); //merge WHERE IN array with other value(s) $stmt = $mysqli->prepare("SELECT id, name FROM myTable WHERE id IN ($clause) AND age > ?"); $stmt->bind_param($types, ...$fullArr); //4 placeholders to bind $stmt->execute(); $resArr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); if(!$resArr) exit('No rows'); var_export($resArr); $stmt->close();
Extractos múltiples preparados en transacciones:
Esto puede parecer extraño por qué incluso justificaría su propia sección, ya que literalmente puede usar declaraciones preparadas una tras otra. Si bien esto ciertamente funcionará, esto no garantiza que sus consultas sean atómicas. Esto significa que si tuviera que ejecutar diez consultas, y una fallara, las otras nueve tendrían éxito. Si desea que sus consultas SQL se ejecuten solo si todas tuvieron éxito, entonces debe usar transacciones.
try { $mysqli->autocommit(FALSE); //turn on transactions $stmt1 = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt2 = $mysqli->prepare("UPDATE myTable SET name = ? WHERE id = ?"); $stmt1->bind_param("si", $_POST['name'], $_POST['age']); $stmt2->bind_param("si", $_POST['name'], $_SESSION['id']); $stmt1->execute(); $stmt2->execute(); $stmt1->close(); $stmt2->close(); $mysqli->autocommit(TRUE); //turn off transactions + commit queued queries } catch(Exception $e) { $mysqli->rollback(); //remove all queries from queue if error (undo) throw $e; }
Reutilizar la misma plantilla, con valores diferentes
try { $mysqli->autocommit(FALSE); //turn on transactions $stmt = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt->bind_param("si", $name, $age); $name = 'John'; $age = 21; $stmt->execute(); $name = 'Rick'; $age = 24; $stmt->execute(); $stmt->close(); $mysqli->autocommit(TRUE); //turn off transactions + commit queued queries } catch(Exception $e) { $mysqli->rollback(); //remove all queries from queue if error (undo) throw $e; }
Manejo de errores con MiSQLi
Error fatal: Uncaught Error: Call to a member function bind_param() on boolean
Cualquiera que haya usado declaraciones preparadas de MySQLi ha visto este mensaje en algún momento, pero ¿qué significa? Casi nada en absoluto. Entonces, ¿cómo arreglas esto, podrías preguntar? Para comenzar, no olvide activar el manejo de excepciones, en lugar del manejo de errores mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
cuando vayas a crear una conexión a la base de datos.
Manejo de excepciones
Todas las funciones de mysqli devuelven falso en caso de error o fallo, por lo que puede verificar fácilmente la veracidad de cada función e informar errores con $mysqli->error
. Sin embargo, esto es muy tedioso y hay una forma más elegante de hacerlo si habilita los informes internos. Recomiendo hacerlo de esta manera, ya que es mucho más portátil desde el desarrollo hasta la producción.
Esto también se puede usar en producción, siempre que tenga un registro de errores configurado para todos los errores; esto debe establecerse en php.ini. Nunca informe de los errores directamente en su sitio estando en producción. Te patearás por un error tan tonto.
Además la colocación de mysqli_report()
antes de crear una nueva conexión, también mostrará su contraseña de conexión; de lo contrario, solo informará todo después, como sus consultas.
Así es como debería verse su archivo php.ini en producción: haga ambas cosas display_errors = Off
y log_errors = On
. Además, tenga en cuenta que cada página realmente solo debe usar un bloque try/catch
global único , en lugar de ajustar cada consulta individualmente. La única excepción a esto es con las transacciones, que estarían anidadas, pero lanzarían su propia excepción, para que el global try/catch
pueda “atraparlo”.
try { $stmt = $mysqli->prepare("DELETE FROM myTable WHERE id = ?"); $stmt->bind_param("i", $_SESSION['id']); $stmt->execute(); $stmt->close(); $stmt = $mysqli->prepare("SELECT id, name, age FROM myTable WHERE name = ?"); $stmt->bind_param("s", $_POST['name']); $stmt->execute(); $arr = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); $stmt->close(); try { $mysqli->autocommit(FALSE); //turn on transactions $stmt = $mysqli->prepare("INSERT INTO myTable (name, age) VALUES (?, ?)"); $stmt->bind_param("si", $name, $age); $name = 'John'; $age = 21; $stmt->execute(); $name = 'Rick'; $age = 24; $stmt->execute(); $stmt->close(); $mysqli->autocommit(TRUE); //turn off transactions + commit queued queries } catch(Exception $e) { $mysqli->rollback(); //remove all queries from queue if error (undo) throw $e; } } catch (Exception $e) { error_log($e); exit('Error message for user to understand'); }
Manejo de excepciones personalizado
Como se indicó anteriormente , puede usar alternativamente set_exception_handler()
en cada página (o una redirección global). Esto elimina la capa de anidación de llaves. Si está utilizando transacciones, aún debe usar un try catch con eso, pero luego deberás lanzar tu propia excepción.
set_exception_handler(function($e) { error_log($e); exit('Error deleting'); }); $stmt = $mysqli->prepare("DELETE FROM myTable WHERE id = ?"); $stmt->execute(); $stmt->close();
Es de esperar que todos los errores de MySQLi se conviertan en excepciones con mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
. Por extraño que parezca, noté que todavía me daba un error de advertencia cuando bind_param()
tenía demasiadas o muy pocas variables o tipos enlazados. El mensaje enviado es el siguiente:
Warning: mysqli_stmt::bind_param(): Number of variables doesn’t match number of parameters in prepared statement
Una solución a esto es utilizar un controlador de errores global para desencadenar una excepción. Un ejemplo de esto podría ser:
set_error_handler(function($errno, $errstr, $errfile, $errline) {
throw new Exception("$errstr on line $errline in file $errfile");
});
Esto solo sucedió en las advertencias en tiempo de ejecución, pero convertí todos los errores en excepciones. No veo ningún problema para hacer esto, pero hay algunas personas que están fuertemente en contra.
Algunos extras:
¿Necesito $stmt->close()?
Gran pregunta par: $mysqli->close()
y $stmt->close(),ya que
esencialmente tienen el mismo efecto. El primero cierra la conexión MySQLi, mientras que el segundo cierra la declaración preparada. TLDR; en general, ambos no son necesarios en la mayoría de los casos, ya que ambos se cerrarán una vez que se complete la ejecución del script. También hay una función para simplemente liberar la memoria asociada con el resultado de MySQLi y la declaración preparada, respectivamente: $result->free()
y $stmt->free()
. Yo mismo, probablemente nunca lo usaré, pero si está interesado, aquí está el del resultado y la consulta parametrizada . También se debe tener en cuenta lo siguiente: ambos $stmt->close()
y el final de la ejecución del script liberarán la memoria de todos modos.
Veredicto final: generalmente solo lo hago $mysqli->close()
y $stmt->close()
, aunque se puede argumentar que es un poco superfluo. Si planea usar la misma variable $stmt
nuevamente para otras declaraciones preparadas, debe cerrarla o usar un nombre de variable diferente, como $stmt2
. Por último, nunca he encontrado la necesidad de liberarlos simplemente, sin cerrarlos.
Clases: mysqli vs. mysqli_stmt vs. mysqli_result
Una cosa que puede haberse dado cuenta en el camino es que existen ciertos métodos que existen en dos de las clases, como un alias casi. Personalmente, creo que sería mejor tener solo una versión, como en PDO, para evitar confusiones.
mysqli::$affected_rows
omysqli_stmt::$affected_rows
– Pertenece amysqli_stmt
. Funciona igual con cualquiera de los dos, pero será un error si se llama después de que la declaración se cierre con cualquiera de los métodosmysqli_result::$num_rows
omysqli_stmt::$num_rows
–$result->num_rows
solo se puede usar conget_result()
, mientras$stmt->num_rows
que solo se puede usar conbind_result()
.mysqli::$insert_id
omysqli_stmt::$insert_id
– Pertenece amysqli
. Mejor usar$mysqli->insert_id
, ya que seguirá funcionando incluso después de que$stmt->close()
se use. También hay una nota sobre los documentos PHP de 2011 que indica que$stmt->insert_id
solo obtendrá la primera consulta ejecutada. Intenté esto en mi versión actual de 7.1 y este no parece ser el caso. El recomendado para usar es la versión de clase mysqli de todos modos.
Entonces, ¿usar declaraciones preparadas significa que estoy a salvo de los atacantes?
Si bien está a salvo de la inyección de SQL, aún necesita validar y desinfectar sus datos ingresados por el usuario. Puede usar una función como filter_var () para validar los datos antes de insertarlos en la base de datos y htmlspecialchars () para desinfectar después de recuperarlos en sus consultas.
=====================
P.D, si te gusto el tutorial puedes dejar un comentario de agradecimiento, si algo ha cambiado desde entonces, no dudes en dejarme un comentario con los nuevos procedimientos.
¿Tienes alguna duda o tienes un problema que no sabes como resolver? Déjame un comentario igualmente, te contestare tan pronto como me sea posible.