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)
Related
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;
}
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'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
The following code needs to make use of 3 variables given by the user. By default all of these variables equal to 0.
time (textbox)
city (drop down list)
type (drop down list)
If for example time and city is given by the user, but lets the type zero, it will not return any results.
My question is what is an effective and efficient way to modify my existing code so that if the user chooses not to select time, city or type or any combination of these, there will be results returned?
For example if time 21:00 is added with city number 3, it will show all the types that meet the 2 criteria should be listed.
$question= 'SELECT * FROM events WHERE ABS(TIMESTAMPDIFF( HOUR , `time`, :time )) < 2 AND city=:city AND type=:type';
$query = $db->prepare($question);
$query->bindValue(":time", $time, PDO::PARAM_INT);
$query->bindValue(":city", $city, PDO::PARAM_INT);
$query->bindValue(":type", $type, PDO::PARAM_INT);
$query->execute();
<?php
$question= 'SELECT * FROM events WHERE ';
$hasTime = false;
if(!empty($time)) { // #note better validation here
$hasTime = true;
$question .= 'ABS(TIMESTAMPDIFF( HOUR , `time`, :time )) < 2 ';
}
$hasCity = false;
if(!empty($city)) { // #note better validation here
$hasCity = true;
$question .= 'AND city=:city ';
}
$hasType = false;
if(!empty($type)) { // #note better validation here
$hasType = true;
$question .= 'AND type=:type';
}
$query = $db->prepare($question);
if($hasTime)
$query->bindValue(":time", $time, PDO::PARAM_INT);
if($hasCity)
$query->bindValue(":city", $city, PDO::PARAM_INT);
if($hasType)
$query->bindValue(":type", $type, PDO::PARAM_INT);
$query->execute();
$results = $query->fetchAll();
if(empty($results))
echo 'no results';
else
// $results is an array of arrays
I prefer using an array of conditions, and checking through to see if the conditions exist, to built the individual parts of the SQL query:
$conditions = array(); // Creating an array of conditions.
if ($time) // Checks to see if value exists.
{
$timeCondition = "ABS(TIMESTAMPDIFF( HOUR , `time`, :time )) < 2";
$conditions[] = $timeCondition; // Adds this condition string to the array.
}
if ($city)
{
$cityCondition = "city=:city";
$conditions[] = $cityCondition;
}
if ($type)
{
$typeCondition = "type=:type";
$conditions[] = $typeCondition;
}
$conditionString = implode(" AND ", $conditions); // Gluing the values of the array with " AND " in between the string conditions.
if (count($conditions) > 0) // If conditions exist, add "WHERE " to the condition string.
{
$conditionString = "WHERE ".$conditionString;
}
else // Otherwise, the condition string is blank by default.
{
$conditionString = '';
}
$question= 'SELECT * FROM events '.$conditionString; // If no conditions, will return all from events. Otherwise, conditions will be slotted in through $conditionString.
$query = $db->prepare($question);
if($time)
$query->bindValue(":time", $time, PDO::PARAM_INT);
if($city)
$query->bindValue(":city", $city, PDO::PARAM_INT);
if($type)
$query->bindValue(":type", $type, PDO::PARAM_INT);
$query->execute();
You can use a series of IF() statements in your SQL statement, and return true if the value isn't set. So something like this:
...WHERE IF(:time, ABS(TIMESTAMPDIFF(HOUR, `time`, :time)) < 2, 1)
AND IF(:city, city=:city, 1) AND IF(:type, type=:type, 1)
Build the query dynamically so that if the field is at its default, do not include it in the where clause.
$conditions = array();
if ($_POST['time']) {
$conditions[] = "ABS(TIMESTAMPDIFF( HOUR , `time`, :time )) < 2";
}
if ($_POST['city']) {
$conditions[] = "city=:city";
}
if ($_POST['type']) {
$conditions[] = "type=:type";
}
$conditionString = implode(" AND ", $conditions);
if (count($conditions) > 0) {
$conditionString = "WHERE " . $conditionString;
}
else {
$conditionString = '';
}
$question = 'SELECT * FROM events ' . $conditionString;
I have the following code:
$sql = "SELECT name, address, city FROM tableA, tableB WHERE tableA.id = tableB.id";
if (isset($price) ) {
$sql = $sql . ' AND price = :price ';
}
if (isset($sqft) ) {
$sql = $sql . ' AND sqft >= :sqft ';
}
if (isset($bedrooms) ) {
$sql = $sql . ' AND bedrooms >= :bedrooms ';
}
$stmt = $dbh->prepare($sql);
if (isset($price) ) {
$stmt->bindParam(':price', $price);
}
if (isset($sqft) ) {
$stmt->bindParam(':price', $price);
}
if (isset($bedrooms) ) {
$stmt->bindParam(':bedrooms', $bedrooms);
}
$stmt->execute();
$result_set = $stmt->fetchAll(PDO::FETCH_ASSOC);
What I notice is the redundant multiple IF statements I have.
Question: is there any way to clean up my code so that I don't have these multiple IF statements for prepared statements?
This is very similar to a question a user asked me recently the forum for my book SQL Antipatterns. I gave him an answer similar to this:
$sql = "SELECT name, address, city FROM tableA JOIN tableB ON tableA.id = tableB.id";
$params = array();
$where = array();
if (isset($price) ) {
$where[] = '(price = :price)';
$params[':price'] = $price;
}
if (isset($sqft) ) {
$where[] = '(sqft >= :sqft)';
$params[':sqft'] = $sqft;
}
if (isset($bedrooms) ) {
$where[] = '(bedrooms >= :bedrooms)';
$params[':bedrooms'] = $bedrooms;
}
if ($where) {
$sql .= ' WHERE ' . implode(' AND ', $where);
}
$stmt = $dbh->prepare($sql);
$stmt->execute($params);
$result_set = $stmt->fetchAll(PDO::FETCH_ASSOC);
Instead of if else just use PHP ternary operator
if (isset($_POST['statusID']))
{
$statusID = $_POST['statusID'];
}
else
{
$statusID = 1;
}
instead of that you can do:
$statusID = (isset($_POST['statusID'])) ? $_POST['statusID'] : 1;
The format of the ternary operator is: $variable = condition ? if true : if false
The beauty of it is that you will shorten your if/else statements down to one line and if compiler ever gives you errors, you can always go back to that line instead of 3 lines.