I'm trying to convert a complex php file I made a year ago over to prepared statements. Parameters will be passed in like so:
file.php?name=John&age=20
However there could be many more parameters that are potentially used. job, address, phone number, etc etc. This is where I tend to get confused with prepared statements.
For example:
$query = "SELECT name, id, age, address, phone_number from person_db WHERE 1=1 ";
if (isset($_REQUEST['name'])) {
$query .= " and name = ?";
}
if (isset($_REQUEST['age'])) {
$query .= " and age = ?";
}
if (isset($_REQUEST['address'])) {
$query .= " and address = ?";
}
$stmt = $db->prepare($query);
$stmt->bind_param('sis', $_REQUEST['name'], $_REQUEST['age'], $_REQUEST['address']);
$stmt->execute();
The issue here is bind_param because I don't know how many parameters could potentially be available.
How would I go about this in a logical manner?
A very good question. And the solution is very simple.
What you need is called argument unpacking operator. It will allow you to use an array of arbitrary length with bind_param. All you need is to collect accepted parameters into array, then create the types string dynamically and finally use the aforementioned operator:
$query = "SELECT name, id, age, address, phone_number from person_db WHERE 1=1 ";
$params = array();
if (isset($_REQUEST['name'])) {
$query .= " and name = ?";
$params[] = $_REQUEST['name'];
}
if (isset($_REQUEST['age'])) {
$query .= " and age = ?";
$params[] = $_REQUEST['age'];
}
if (isset($_REQUEST['address'])) {
$query .= " and address = ?";
$params[] = $_REQUEST['address'];
}
if ($params)
$stmt = $db->prepare($query);
$types = str_repeat("s", count($params));
$stmt->bind_param($types, ...$params);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $db->query($query);
}
Well, the process is going to be quite similar to how you build up your $query variable - i.e. you add to the parameters list one at a time.
Consider that bind_param requires two things:
First is a list of the data types as a simple string. So you can just have a string variable which you add to for each parameter, and then pass that to bind_param at the end.
Second is a list of parameters. This is trickier, because you can't just supply an array to bind_param. However, you can create an array of your own, and then use call_user_func_array to convert that to a flat list.
Here's one I wrote earlier (and works nicely). Note that it attempts to detect the parameter types and create a suitable string, but you could just build up a string manually in your if statements if you prefer:
$query = "SELECT name, id, age, address, phone_number from person_db WHERE 1=1 ";
$params = array();
if (isset($_REQUEST['name'])) {
$query .= " and name = ?";
$params[] = $_REQUEST['name'];
}
if (isset($_REQUEST['age'])) {
$query .= " and age = ?";
$params[] = $_REQUEST['age'];
}
if (isset($_REQUEST['address'])) {
$query .= " and address = ?";
$params[] = $_REQUEST['address'];
}
$stmt = $db->prepare($query);
if (!is_null($params))
{
$paramTypes = "";
foreach ($params as $param)
{
$paramTypes .= mysqliContentType($param);
}
$bindParamArgs = array_merge(array($paramTypes), $params);
//call bind_param using an unpredictable number of parameters
call_user_func_array(array(&$stmt, 'bind_param'), getRefValues($bindParamArgs));
}
$stmt->execute();
function mysqliContentType($value)
{
if(is_string($value)) $type = 's';
elseif(is_float($value)) $type = 'd';
elseif(is_int($value)) $type = 'i';
elseif($value == null) $type = 's'; //for nulls, just have to assume a string. hopefully this doesn't mess anything up.
else throw new Exception("type of '".$value."' is not string, int or float");
return $type;
}
function getRefValues($arr)
{
$refs = array();
foreach($arr as $key => $value)
{
$refs[$key] = &$arr[$key];
}
return $refs;
}
Related
Trying to get a function working to create simple CRUD "Select" with multiple parameters to any table. I think I got the hardest part, but couldn't fetch the data right now. Maybe I'm doing something wrong I can't figure out.
My prepared statement function:
function prepared_query($mysqli, $sql, $params, $types = ""){
$types = $types ?: str_repeat("s", count($params));
if($stmt = $mysqli->prepare($sql)) {
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt;
} else {
$error = $mysqli->errno . ' ' . $mysqli->error;
error_log($error);
}
}
The query creator:
function create_select_query($table, $condition = "", $sort = "", $order = " ASC ", $clause = ""){
$table = escape_mysql_identifier($table);
$query = "SELECT * FROM ".$table;
if(!empty($condition)){
$query .= create_select_query_where($condition,$clause);
}
if(!empty($sort)){
$query .= " ORDER BY ".$sort." $order";
}
return $query;
}
The helper function to create the WHERE clause:
function create_select_query_where($condition,$clause){
$query = " WHERE ";
if(is_array($condition)){
$pair = array();
$size = count($condition);
$i = 0;
if($size > 1){
foreach($condition as $field => $val){
$i++;
if($size-1 == $i){
$query .= $val." = ? ".$clause. " ";
}else{
$query .= $val." = ? ";
}
}
}else{
foreach($condition as $field => $val){
$query .= $val." = ? ";
}
}
}else if(is_string($condition)){
$query .= $condition;
}else{
$query = "";
}
return $query;
}
The select function itself:
function crud_select($conn, $table, $args, $sort, $order, $clause){
$sql = create_select_query($table, array_keys($args),$sort, $order, $clause);
print_r($sql);
if($stmt = prepared_query($conn, $sql, array_values($args))){
return $stmt;
}else{
$errors [] = "Something weird happened...";
}
}
When I create the query, it seems to be OK but can't fetch the data. If I create an array with only one argument the query translates into:
SELECT * FROM `teste_table` WHERE id = ?
If I create with multiple parameters, it turns like this:
SELECT * FROM `teste_table` WHERE id = ? AND username = ?
So, how can I properly fetch the data from the select. This should be used for multiple purposes, so I could get more than one result, so the best way would be fetch data as array I guess.
I guess I'm close, but can't figure it out. Thanks
I told you to limit your select function to a simple primary key lookup. And now you opened a can of worms. As a result you are getting entangled implementation code and unreadable application code.
$table, $args, $sort, $order, $clause
What all these variables are for? How you're going to call this function - a list of gibberish SQL stubs in a random order instead of plain and simple SQL string? And how to designate a list of columns to select? How to use JOINS? SQL functions? Aliases? Why can't you just write a single SQL statement right away? You already have a function for selects, though without this barbaric error reporting code you added to it:
function prepared_query($mysqli, $sql, $params, $types = ""){
$types = $types ?: str_repeat("s", count($params));
$stmt = $mysqli->prepare($sql)) {
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt;
}
Just stick to it and it will serve you all right.
$sql = "SELECT * FROM `teste_table` WHERE id = ? AND username = ?";
$stmt = prepared_query($mysqli, $sql, [$id, $name]);
$row = $stmt->get_result()->fetch_assoc();
The only specific select function could be, again, a simple primary key lookup:
function crud_find($conn, $table, $id)
{
$table = escape_mysql_identifier($table);
$sql = "SELECT * FROM $table WHERE id=?";
$stmt = prepared_query($conn, $sql, [$id], "i");
return $stmt->get_result()->fetch_assoc();
}
And for the everything else just use a generic function with native SQL.
I have a database with student records, about 50 or so columns, all of which need to be searchable. I'm familiar with how to build a dynamic search query by appending fields. I can't seem to find any examples on searching a MySQL database using prepared statements that are dynamic in nature. For example, when searching you will only enter the fields you need to search by, so therefore, how do you build a dynamic prepared statement based on user input?
Here is an example of how to do it without prepared statements, however this is prone to SQL injection. I would need to "clean" each variable with a function. However, with a prepared statement this would be much more secure.
function clean($con,$var){
$var = trim($var);
$var = strtolower($var);
$var = mysqli_real_escape_string($con,$var);
return $var;
}
if(isset($_REQUEST['sub'])){
if(isset($_REQUEST['student_fname'])){
$student_fname = null;
if($_REQUEST['student_fname'] != ''){
$student_fname = '&& `student_fname` = \''.clean($con,$_REQUEST['student_fname']).'\' ';
}
}
if(isset($_REQUEST['student_lname'])){
$student_lname = null;
if($_REQUEST['student_lname'] != ''){
$student_lname = '&& `student_lname` = \''.clean($con,$_REQUEST['student_lname']).'\' ';
}
}
$sql = "SELECT * FROM `student_records`
WHERE 1=1
$student_fname
$student_lname
ORDER BY class ASC, student_lname ASC";
echo $sql;
}
As with many things, this would be a LOT easier with PDO. But, just off the top of my head this is how I would approach it. You need to build 3 structures here. The SQL, the list of parameter types ("s" or "i") and the list of parameters themselves. This is all pretty straightforward.
Getting this into the bind_param() function can be a bit tricky if you haven't done it before, but as detailed elsewhere the argument unpacking operator works nicely, once you have all your arguments into an array.
For your POST variables, remember that empty() checks if the array element is present, and also if it's non-empty. This will save you an extra check, with the only caveat being that empty("0") === true.
<?php
if (isset($_REQUEST['sub'])) {
$types = "";
$where = [];
$params = [];
if (!empty($_REQUEST['student_fname'])) {
$types .= 's';
$where[] = 'student_fname = ?';
$params[] = $_REQUEST['student_fname'];
}
if (!empty($_REQUEST['student_lname'])) {
$types .= 's';
$where[] = 'student_lname = ?';
$params[] = $_REQUEST['student_lname'];
}
if (count($where)) {
$where = "AND " . implode(" AND ", $where);
} else {
$where = "";
}
$sql = "SELECT * FROM student_records
WHERE 1=1
$where
ORDER BY class ASC, student_lname ASC";
$stmt = $con->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
while ($res = $stmt->get_result()) {
// ....
}
}
Note that this code allows someone to list the entire database; if that isn't your intention, wrap everything in the if (count($where)) statement.
In PDO, things are much easier, no binding needed and parameters are passed as an array already.
<?php
if(isset($_REQUEST['sub'])){
$where = [];
$params = [];
if(!empty($_REQUEST['student_fname'])) {
$where[] = 'student_fname = ?';
$params[] = $_REQUEST['student_fname'];
}
if(!empty($_REQUEST['student_lname'])){
$where[] = 'student_lname = ?';
$params[] = $_REQUEST['student_lname'];
}
if (count($where)) {
$where = "AND " . implode(" AND ", $where);
} else {
$where = "";
}
$sql = "SELECT * FROM student_records
WHERE 1=1
$where
ORDER BY class ASC, student_lname ASC";
$stmt = $con->prepare($sql);
$stmt->execute($params);
$data = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
The prepared statement that gets generated dynamically from my PHP (as an example) looks like this:
SELECT COUNT(exuid) as result_count FROM full_db3 WHERE `Age Range` = :Age Range
Age Range is one of my column names.
The problem here is that having the space in between "Age" and "Range" in my parameter, but I'm not sure how to handle this. The query is generated dynamically like so (only relevant code shown):
$all_attributes = $_POST['attris'];
$sql = "SELECT COUNT(exuid) as result_count FROM {$table}";
$any_condition = false;
foreach($all_attributes as $key=>$val) {
if (!empty($val) && in_array($key,$validKeys)) {
if ($any_condition) {
$sql .= ' AND `'.$key.'` = :'.$key;
} else {
$sql .= ' WHERE `'.$key.'` = :'.$key;
$any_condition = true;
}
}
}
$stmt = $dbh->prepare($sql);
foreach($all_attributes as $key=>$val) {
if (!empty($val) && in_array($key,$validKeys)) {
$stmt ->bindValue(':'.$key, $val, PDO::PARAM_STR);
}
}
$stmt->execute();
If I change my column name in the DB to Age_Range everything works perfectly fine. For a number of reasons, I'd like to be able to exclude that underscore, as I display my column names in a select and all the underscores look terrible.
Using my idea, and copying syntax from u_mulder's comment, this should work?
$all_attributes = $_POST['attris'];
$sql = "SELECT COUNT(exuid) as result_count FROM {$table}";
$any_condition = false;
foreach($all_attributes as $key=>$val) {
if (!empty($val) && in_array($key,$validKeys)) {
if ($any_condition) {
$sql .= ' AND `'.$key.'` = :'.str_replace(' ', '_', $key);
} else {
$sql .= ' WHERE `'.$key.'` = :'.str_replace(' ', '_', $key);
$any_condition = true;
}
}
}
It leaves the field names as is, and only changes the parameter names.
But this
$stmt ->bindValue(':'.$key, $val, PDO::PARAM_STR)
will probably need changed to this as well
$stmt ->bindValue(':'.str_replace(' ', '_', $key), $val, PDO::PARAM_STR)
so far I've come to this.
My goal is to be able to apply optional search filters such as a.chainid and a.branchid if needed, for that to happen I need dynamic bind_param.
The code seems to be fine, but in fact it's not working for some reason.
Care to tell me what's wrong?
The fetch returns me nothing instead of 5 rows that should.
Thanks in advance
$sql = "SELECT a.COUPONID, a.TRUSTANDUSEID FROM `custom_redemptions` a WHERE a.couponid = 3";
$types = '';
$params = array(&$types);
if ($branchid != null) {
$sql .= "AND a.branchid = ?";
$types .= 's';
$params[] = $branchid;
}
if ($chainid != null) {
$sql .= "AND a.chainid = ?";
$types .= 's';
$params[] = $chainid;
}
if ($stmt = $this->dbCon->prepare($sql)) {
call_user_func_array(array($stmt, 'bind_param'), $params);
$stmt->execute();
$stmt->bind_result($couponid, $trustanduseid);
while ($stmt->fetch()) { echo $couponid; }
$stmt->close();
}
This solution might help you
$sql = "SELECT a.COUPONID, a.TRUSTANDUSEID FROM `custom_redemptions` a WHERE a.couponid = 3";
$types = '';
$params = array(&$types);
if ($branchid != null) {
$sql .= " AND a.branchid = ?";
$types .= 's';
$params[] = $branchid;
}
if ($chainid != null) {
$sql .= " AND a.chainid = ?";
$types .= 's';
$params[] = $chainid;
}
if ($stmt = $this->dbCon->prepare($sql)) {
call_user_func_array(array($stmt, 'bind_param'), $params);
$stmt->execute();
$stmt->bind_result($couponid, $trustanduseid);
while ($stmt->fetch()) { echo $couponid; }
$stmt->close();
}
Finally what was needed was &$chainid instead of $chainid.
Spent 8 hours researching about it. LOL
I've made a function to query the database. This function takes an array, the id of the user I want to update
and a query operation.
if the query operation is UPDATE
if you look at the code below, would this be a good coding practice or is this bad code?
public function query($column, $search_value, $query_operation = "SELECT"){
if(strtoupper($query_operation == "UPDATE")){
$query = "UPDATE users SET ";
if(is_array($column)){
$counter = 1;
foreach($column as $key => $value){
if($counter < count($column)){
$query .= $key . ' = ?, ';
}else{
$query .= $key . ' = ? ';
}
$counter++;
}
$query .= "WHERE id = ?";
$stmt = $this->database->prepare($query);
$counter = 1;
foreach($column as $key => &$value){
$stmt->bindParam($counter, $value);
$counter++;
}
$stmt->bindParam($counter, $search_value);
if($stmt->execute()){
$stmt = $this->database->prepare("SELECT* FROM
users WHERE id = ?");
$stmt->bindParam(1, $search_value, PDO::PARAM_INT);
$stmt->execute();
return $this->build_array($stmt);
}
}
}
}
would love to hear some feedback.
I would NOT mix SELECT and UPDATE in the same function.
The following update function uses arrays for column names and values $columnNames & $values using unnamed parameters.
function update($tableName,$columnNames,$values,$fieldName,$fieldValue){
$sql = "UPDATE `$tableName` SET ";
foreach($columnNames as $field){
$sql .= $field ." = ?,";
}
$sql = substr($sql, 0, -1);//remove trailing ,
$sql .= " WHERE `$fieldName` = ?";
return $sql;
}
As table and column names cannot be passed as parameters in PDO I have demonstrated whitelistng of table names.
$tables = array("client", "Table1", "Table2");// Array of allowed table names.
Also array_push()to add value for last parameter (WHERE) into $values array
Use
if (in_array($tableName, $tables)) {
$sql = update($tableName,$columnNames,$values,$fieldName,$fieldValue);
array_push($values,$fieldValue);
$STH = $DBH->prepare($sql);
$STH->execute($values);
}
You can use similar technique for SELECT