mysql_query to PDO and prepared statements - php

I have this function in my CMS, which inserts a variable list of fields and values into two different tables; one is a static index table and one is dynamic. This is the function:
function insertFields($fields)
{
$stdfields = array();
$extfields = array();
/* Separate the fields based on if the fields is standard or extra. $this->fields is a csv list of the defined extra fields */
foreach($fields as $field => $value)
{
$fields[mysql_real_escape_string($field)] = mysql_real_escape_string($value);
if(strstr($this->fields, $field))
$extfields[$field] = $value;
else
$stdfields[$field] = $value;
}
//Build the 2 queries -- Maybe there is a better way to do this?
$extfieldcount = count($extfields);
$stdfieldcount = count($stdfields);
$stditers = 0;
$extiters = 0;
foreach($extfields as $field => $value)
{
if($extiters != $extfieldcount)
{
$extfields.= $field.", ";
$extvalues.= "'".$value."', ";
}
else
{
$extfields.= $field." ";
$extvalues.= "'".$value."' ";
}
$extiters++;
}
foreach($stdfields as $field => $value)
{
if($stditers != $stdfieldcount)
{
$newfields.= $field.", ";
$newvalues.= "'".$value."', ";
}
else
{
$newfields.= $field." ";
$newvalues.= "'".$value."' ";
}
$stditers++;
}
//Inset the standard fields
$stdquery = "INSERT INTO masteridx (".$newfields.") VALUES (".$newvalues.")";
$this->dbQuery($stdquery);
/* not perfect. I need a better way to find the id that was inserted, so I can combine three queries into at least two */
$findlastquery = "SELECT `id` FROM `masteridx` WHERE `slug`='".$fields['slug']."' LIMIT 1";
$result = $this->dbQuery($findlastquery);
$result = mysql_fetch_assoc($result);
$tempfield = "id, ";
$tempvalue = "'".$result['id']."', ";
//Insert the extra fields
$extquery = "INSERT INTO ".$this->type." (".$tempfield.$extfields.") VALUES (".$tempvalue.$extvalues.")";
$this->dbQuery($extquery);
}
So for a prepared statement, I can't Bind the fields, just the values, right? So I would still have to escape the fields if I did something like:
for ($i = 0; $i <= $stdfieldcount; $i++)
{
if($i < $stdfieldcount)
$qs.= '?, ';
else
$qs.= '? ';
}
$sth = $dbh->prepare("INSERT INTO masteridx ({$stdfields}) VALUES ({$qs})");
$sth->execute($array_of_stdfield_values);
What's the point here if I still have to escape the fields? This function will eventually take an array of multiple articles and their fields. The fields themselves would be different each time, as well.. I figured that when I first looked at prepared statements, I could just hand it an array of fields, and an array of values, but I guess that's not the case.
My question is really, how would you all accomplish this? I would like to start being database agnostic, and PDO looked like a great way to do this.

PHP provides quite a few convenience functions that do a lot of the stuff you're doing by hand.
PDO supports named parameters in your SQL statements, so you can then pass a key/value array where the keys match your named parameter placeholders.
The join() function is very useful for building comma-separated lists.
Many functions exist to manipulate arrays.
Some functions allow you to give a callback (which can be a closure in PHP 5.3), to process arrays dynamically.
Example (not tested):
function insertFields($fields) {
$columns = join(",", array_map(
function($col) { return "`".preg_replace("/`/gu","``",$col)."`"},
array_keys($fields)));
$params = join(",", array_map(
function($col) { return ":".preg_replace("/[`\s]/gu","",$col)},
array_keys($fields)));
$stdquery = "INSERT INTO masteridx ({$columns}) VALUES ({$params})";
$stmt = $pdo->prepare($stdQuery);
$stmt->execute($fields);
}

Related

PDO filter system with Ajax. Is there a better way?

I am trying to filter some inputs of the user with select boxes. I am figuring if there is a better way doing this.
if(isset($_POST['action']))
{
$sql = "SELECT * FROM occasions WHERE naam IS NOT NULL";
if(isset($_POST['merk'])){
$merk = $_POST['merk'];
$merkQuery = implode(',', array_fill(0, count($merk), '?'));
$sql .= " AND merk IN(".$merkQuery.")";
}
if(isset($_POST['brandstof'])){
$brandstof = $_POST['brandstof'];
$brandstofQuery = implode(',', array_fill(0, count($brandstof), '?'));
$sql .= " AND brandstof IN(".$brandstofQuery.")";
}
//We prepare our SELECT statement.
$statement = $pdo->prepare($sql);
if(isset($_POST['merk'])){
//Execute statement.
$statement->execute(array_merge(array_values($merk)));
}
if(isset($_POST['brandstof'])){
//Execute statement.
$statement->execute(array_merge(array_values($brandstof)));
}
if(isset($_POST['merk']) && isset($_POST['brandstof']))
{
$statement->execute(array_merge(array_values($merk), array_values($brandstof)));
}
else
{
$statement->execute();
}
}
Cause if there are many select boxes that need filtering, the code would become long. I was wondering if there is a better way of filtering multiple select boxes.
Here is an example: link
I would suggest renaming the post variables; grouping them into a single two dimensional array.
<input type="checkbox" name="data[merk][bmw]" />
<input type="checkbox" name="data[merk][skoda] />
and so forth.
What this does, is it allows you to use a foreach to iterate through whatever values are checked.
$data = $_POST['data'] ?? []; // null coalesce defaults to a blank array if post var is null
foreach($data as $category=>$val) {
settype($val, 'array');
$query = implode(',', array_fill(0, count($val), '?'));
foreach($val as $k=>$v) {
$params[] = $k;
}
// DON'T DO THIS!
$sql .= " AND $category IN(".$query.")";
}
The reason you shouldn’t do it as shown is because you should never build a query with user-supplied data.
What you can do, however, is map user-supplied data with hard-coded data.
$map = [
// form value => db field
'merk' => 'MERK',
'brandstof' => 'BRANDSTOF',
// ... etc
];
and then when building your query,
$sql .= " AND $map[$category] IN($query)";
In the meantime, you have built your parameters in $params.
—-
Bottom line, what we have done is refactor the code since we were noticing things getting repeated. For example, you were having to repeat code for each occasion(?). One solution would be to continue to check each post value and call a function to calculate the ?s. But even then, it would be repetitive to type out all those isset()s.
In retrospect, it probably would have been better to do inputs like this:
<input type="checkbox" name="data[merk][]" value="bmw" />
<input type="checkbox" name="data[merk][]" value="skoda" />
This would no doubt be more intuitive, although you would still have to build the params array.
foreach($val as $v) {
$params[] = $v;
}

Inserting multiple values from Checkbox into 4 Columns

I'm a little confused here. I want to add 4 values to the db. Exept if the user only add < 4.
DB columns are:
ticket1, ticket2, ticket3, ticket4
There are multiple checkboxes 1-19.
<input class='' id='cbx' value='1' name='check_num[]' type='checkbox'/><label>1</label>
My code is...
if(!empty($_POST['check_num'])) {
foreach($_POST['check_num'] as $key => $value) {
//i just need the ARRAY or something to divide the numbers in peaces to put on the db.
}
} else {
echo "empty";
}
I'd use something like this, but there are plenty of alternatives.
if(!empty($_POST['check_num'])) {
$count = count($_POST['check_num']);
if($count <= 4){
$columns = "";
$values = "";
$i = 1;
while($i <= $count){
$columns .= "ticket".$i.", ";
$params .= "?, ";
$types .= "i";
$i++;
}
$columns = substr($columns, 0, 2);
$params = substr($columns, 0, 2);
$stmt = $mysqli->prepare("INSERT INTO [table_name] (".$columns.") VALUES (".$params.");";);
mysqli_stmt_bind_param($stmt, $types, ...$_POST['check_num']);
mysqli_stmt_execute($stmt);
}
else{
echo "Too many selections";
}
}
else{
echo "empty";
}
I haven't used anything but PDO for prepared in php in several years. Might be my bias but I'd recommend the switch, this seemed more complicated to me. You may need to update the portions using mysqli prepare and bind I haven't tested this code at all, just wrote it in the answer.
I also assumed each column was an integer type, and of course, update to use your table name in the query.
I added the substr() functions to remove trailing ", " from last entry of each, you could also check if $i == $count before those assignments, and use a different assignment set without the trailing ", " inside that if() with the current assignments in the following else. The $i++ would obviously be outside this if()/else.

How to properly build a "criterion-like" filter over a PHP array?

How do I build a Criterion-like (like we have on Hibernate (Java)) filtering for PHP arrays?
little explanation
Here I built a basic "Criterion" object to dynamically add conditions to a SQL string statement just like this (select * is just a sample, for the sake of simplicity):
public function GetAll($where_hashmap = null, Closure $optionalFn = null)
{
// function that retrieve table name from concrete class implementation
$table = $this->getTableName();
$sql = "select * from {$table}";
$params = array();
if ($where_hashmap != null)
{
$sql .= " where 1=1";
for ($i = 0; $i < count($where_hashmap); $i++)
{
$column = $where_hashmap[$i]["column"];
$logic = $where_hashmap[$i]["logic"]; // AND, OR, NOT, ...
$comparator = $where_hashmap[$i]["comparator"]; // <, <=, >, >=, =, !=, IN ...
$value = $where_hashmap[$i]["value"];
$sql .= " {$logic} {$column} {$comparator} ? "; // i.e. AND column <= ?
array_push($params, $value);
}
}
// function that take the SQL statement, parameters and an optional function,
// prepare and execute the statement
return $this->Query($sql, $params, $optionalFn);
}
This is kinda simple, because we are dynamically adding conditions to a SQL string. But now, I need to do this to an in-memory array. The array_filter() function from PHP do this pretty well when "we program by ourselves" the callback (criteria) function (like the sample below, from the docs)
array_filter($arr, function($v, $k) {
return $k == 'b' || $v == 4; // all our criteria from $where_hashmap should be here
}, ARRAY_FILTER_USE_BOTH);
How could I dinamically build the if statement, without hard-coding all possible cases for the logical and relational operations? Should I write a function in a PHP file at runtime? Is this approach scallable?
Thank you in advance!

Loop through an array to create an SQL Query

I have an array like the following:
tod_house
tod_bung
tod_flat
tod_barnc
tod_farm
tod_small
tod_build
tod_devland
tod_farmland
If any of these have a value, I want to add it to an SQL query, if it doesnt, I ignore it.
Further, if one has a value it needs to be added as an AND and any subsequent ones need to be an OR (but there is no way of telling which is going to be the first to have a value!)
Ive used the following snippet to check on the first value and append the query as needed, but I dont want to copy-and-paste this 9 times; one for each of the items in the array.
$i = 0;
if (isset($_GET['tod_house'])){
if ($i == 0){
$i=1;
$query .= " AND ";
} else {
$query .= " OR ";
}
$query .= "tod_house = 1";
}
Is there a way to loop through the array changing the names so I only have to use this code once (please note that $_GET['tod_house'] on the first line and tod_house on the last line are not the same thing! - the first is the name of the checkbox that passes the value, and the second one is just a string to add to the query)
Solution
The answer is based heavily upon the accepted answer, but I will show exactly what worked in case anyone else stumbles across this question....
I didnt want the answer to be as suggested:
tod_bung = 1 AND (tod_barnc = 1 OR tod_small = 1)
rather I wanted it like:
AND (tod_bung = 1 OR tod_barnc = 1 OR tod_small = 1)
so it could be appended to an existing query. Therefore his answer has been altered to the following:
$qOR = array();
foreach ($list as $var) {
if (isset($_GET[$var])) {
$qOR[] = "$var = 1";
}
}
$qOR = implode(' OR ', $qOR);
$query .= " AND (" .$qOR . ")";
IE there is no need for two different arrays - just loop through as he suggests, if the value is set add it to the new qOR array, then implode with OR statements, surround with parenthesis, and append to the original query.
The only slight issue with this is that if only one item is set, the query looks like:
AND (tod_bung = 1)
There are parenthesis but no OR statements inside. Strictly speaking they arent needed, but im sure it wont alter the workings of it so no worries!!
$list = array('tod_house', 'tod_bung', 'tod_flat', 'tod_barnc', 'tod_farm', 'tod_small', 'tod_build', 'tod_devland', 'tod_farmland');
$qOR = array();
$qAND = array();
foreach ($list as $var) {
if (isset($_GET[$var])) {
if (!empty($qAND)) {
$qOR[] = "$var = 1";
} else {
$qAND[] = "$var = 1";
}
$values[] = $_GET[$var];
}
}
$qOR = implode(' OR ', $qOR);
if ($qOR != '') {
$qOR = '(' . $qOR . ')';
}
$qAND[] = $qOR;
$qAND = implode(' AND ', $qAND);
echo $qAND;
This will output something like tod_bung = 1 AND (tod_barnc = 1 OR tod_small = 1)
As the parameter passed to $_GET is a string, you should build an array of strings containing all the keys above, iterating it and passing the values like if (isset($_GET[$key])) { ...
You could then even take the key for appending to the SQL string.
Their are a lot of ways out their
$list = array('tod_house', 'tod_bung', 'tod_flat', 'tod_barnc', 'tod_farm', 'tod_small', 'tod_build', 'tod_devland', 'tod_farmland');
if($_GET){
$query = "";
foreach ($_GET as $key=>$value){
$query .= (! $query) ? " AND ":" OR ";
if(in_array($key,$list) && $value){
$query .= $key." = '".$value."'";
}
}
}
Sure you have to take care about XSS and SQL injection
If the array elements are tested on the same column you should use IN (...) rather than :
AND ( ... OR ... OR ... )
If the values are 1 or 0 this should do it :
// If you need to get the values.
$values = $_GET;
$tod = array();
foreach($values as $key => $value) {
// if you only want the ones with a key like 'tod_'
// otherwise remove if statement
if(strpos($key, 'tod_') !== FALSE) {
$tod[$key] = $value;
}
}
// If you already have the values.
$tod = array(
'tod_house' => 1,
'tod_bung' => 0,
'tod_flat' => 1,
'tod_barnc' => 0
);
// remove all array elements with a value of 0.
if(($key = array_search(0, $tod)) !== FALSE) {
unset($tod[$key]);
}
// discard values (only keep keys).
$tod = array_keys($tod);
// build query which returns : AND column IN ('tod_house','tod_flat')
$query = "AND column IN ('" . implode("','", $tod) . "')";

php $_POST to get values - not the best way

EDIT:
Thank you so much for your answers, you really amaze me with so much wisdom :)
I am trying to relay on TuteC's code a bit changed, but can't figure how to make it work properly:
$valor = $_POST['valor'];
$post_vars = array('iphone3g1', 'iphone3g2', 'nome', 'iphone41', 'postal', 'apelido');
foreach($post_vars as $var) {
   $$var = "'" . mysql_real_escape_string($_POST[$var]). "', ";
}
$sql = "INSERT INTO clientes (iphone3g1, iphone3g2, nome, iphone41, postal, apelido, valor) VALUES ($$var '$valor')";
$query= mysql_query($sql);
I know there's a bit of cheating on the code, i would need to use substring so the $$var wouldn't output a "," at the end where i need the values, instead i tried to insert a variable that is a value ($valor = $_POST['valor'];)
What is going wrong?
And for the others who tried to help me, thank you very much, i am learning so much with you here at stackoverflow.
I have a form with several field values, when trying to write a php file that reads those values it came out a mostruosity:
$codigounico= md5(uniqid(rand()));
$modelo=$_POST['selectName'];
$serial=$_POST['serial'];
$nif=$_POST['nif'];
$iphone3g1=$_POST['iphone3g1'];
$iphone3g2=$_POST['iphone3g2'];
$iphone3g3=$_POST['iphone3g3'];
$iphone3g4=$_POST['iphone3g4'];
$iphone3gs1=$_POST['iphone3gs1'];
$iphone3gs2=$_POST['iphone3gs2'];
$iphone3gs3=$_POST['iphone3gs3'];
$iphone3gs4=$_POST['iphone3gs4'];
$iphone41=$_POST['iphone41'];
$iphone42=$_POST['iphone42'];
$iphone43=$_POST['iphone43'];
$iphone44=$_POST['iphone44'];
$total=$_POST['total'];
$valor=$_POST['valor'];
$nome=$_POST['nome'];
$apelido=$_POST['apelido'];
$postal=$_POST['postal'];
$morada=$_POST['morada'];
$notas=$_POST['notas'];
$sql="INSERT INTO clientes (postal, morada, nome, apelido, name, serial, iphone3g1, iphone3g2, iphone3g3, iphone3g4, total, valor, iphone3gs1, iphone3gs2, iphone3gs3, iphone3gs4, iphone41, iphone42, iphone43, iphone44, nif, codigounico, Notas)VALUES('$postal', '$morada', '$nome', '$apelido', '$modelo', '$serial', '$iphone3g1', '$iphone3g2', '$iphone3g3', '$iphone3g4', '$total', '$valor', '$iphone3gs1', '$iphone3gs2', '$iphone3gs3', '$iphone3gs4', '$iphone41', '$iphone42', '$iphone43', '$iphone44', '$nif', '$codigounico', '$notas')";
$result=mysql_query($sql);
This is a very dificult code to maintain,
can I make my life easier?
To restrict which POST variables you "import", you can do something like:
$post_vars = array('iphone3g1', 'iphone3g2', '...');
foreach($post_vars as $var) {
$$var = mysql_real_escape_string($_POST[$var]);
}
EDIT: Changed addslashes by mysql_real_escape_string (thanks #Czechnology).
The issue I see is repetition of the same names four times over. This is how I would reduce it to two occurrences (you could drop it to one with more finagling).
$sql = 'INSERT INTO clientes (postal, morada, nome, apelido, name, serial, iphone3g1, iphone3g2, iphone3g3, iphone3g4, total, valor, iphone3gs1, iphone3gs2, iphone3gs3, iphone3gs4, iphone41, iphone42, iphone43, iphone44, nif, codigounico, Notas) VALUES(:postal, :morada, :nome, :apelido, :modelo, :serial, :iphone3g1, :iphone3g2, :iphone3g3, :iphone3g4, :total, :valor, :iphone3gs1, :iphone3gs2, :iphone3gs3, :iphone3gs4, :iphone41, :iphone42, :iphone43, :iphone44, :nif, :codigounico, :notas)';
preg_match_all('/:(\w+)/', $sql, $inputKeys);
$tokens = $inputKeys[0];
$values = array_map($inputKeys[1], function($k){
return mysql_real_escape_string($_POST[$k]);
});
$sql = str_replace($tokens, $values, $sql);
$result = mysql_query($sql);
Depending on how you want to separate your logic, a reversed approach might be more useful, where you would specify the array of key names and iterate over that to generate the SQL string.
<?php
$inputKeys = array('postal', 'morada', 'nome', 'apelido', 'name', 'serial', 'iphone3g1', 'iphone3g2', 'iphone3g3', 'iphone3g4', 'total', 'valor', 'iphone3gs1', 'iphone3gs2', 'iphone3gs3', 'iphone3gs4', 'iphone41', 'iphone42', 'iphone43', 'iphone44', 'nif', 'codigounico', 'Notas');
$keyList = '(' . implode(',', $inputKeys) . ')';
$valueList = 'VALUES (';
foreach ($inputKeys as $k) {
$valueList .= mysql_real_escape_string($_POST[$k]);
$valueList .= ',';
}
$valueList = rtrim($valueList, ',');
$valueList .= ')';
$sql = 'INSERT INTO clientes '.$keyList.' '.$valueList;
$result = mysql_query($sql);
This approach drops the occurrences of the keys to one and will probably more naturally with your application.
TuteC had a good aim but failed in details.
It makes me wonder, why noone has a ready made solution, but had to devise it on-the-fly. Nobody faced the same problem before?
And why most people trying to solve only part of the problem, getting variables only.
The goal is not to get variables.
The goal is to get a query. So, get yourself a query.
//quite handy way to define an array, saves you from typing zillion quotes
$fields = explode(" ","postal morada nome apelido name serial iphone3g1 iphone3g2 iphone3g3 iphone3g4 total valor iphone3gs1 iphone3gs2 iphone3gs3 iphone3gs4 iphone41 iphone42 iphone43 iphone44 nif codigounico Notas");
$sql = "INSERT INTO clientes SET ";
foreach ($fields as $field) {
if (isset($_POST[$field])) {
$sql.= "`$field`='".mysql_real_escape_string($_POST[$field])."', ";
}
}
$sql = substr($set, 0, -2);
This code will create you a query without boring repeating the same field name many times.
But that's still not all improvements you can make.
A really neat thing is called a function.
function dbSet($fields) {
$set = '';
foreach ($fields as $field) {
if (isset($_POST[$field])) {
$set.="`$field`='".mysql_real_escape_string($_POST[$field])."', ";
}
}
return substr($set, 0, -2);
}
put this function into your code library being included into all your scripts (you have one, don't you?)
and then use it for both insert and update queries:
$_POST['codigounico'] = md5(uniqid(rand()));//a little hack to add custom field(s)
if ($action=="update") {
$id = intval($_POST['id']);
$sql = "UPDATE $table SET ".dbSet($fields)." WHERE id = $id";
}
if ($action=="insert") {
$sql = "INSERT $table SET ".dbSet($fields);
}
So, your code become extremely short and reliable and even reusable.
The only thing you have to change to handle another table is $fields array.
It seems your database is not well planned as it contains seemingly repetitive fields (iphone*). You have to normalize your database.
The same approach to use with prepared statements can be found in this my question: Insert/update helper function using PDO
You could use a rather ugly part of PHP called variable variables, but it is generally considered a poor coding practice. You could include your database escaping at the same time. The code would look something like:
foreach($_POST as $key => $value){
$$key = mysql_real_escape_string($value);
}
The variable variables manual section says they do not work with superglobals like $_PATH, but I think it may work in this case. I am not somewhere where I can test right now.
PHP: extract
Be careful though and make sure you clean the data before using it.
$set = array();
$keys = array('forename', 'surname', 'email');
foreach($keys as $val) {
$safe_value = mysqli_escape_string($db, $_POST[$val]);
array_push($set, "$val='$safe_value'");
}
$set_query = implode(',', $set);
Then make your MySQL query something like UPDATE table SET $set_query WHERE... or INSERT INTO table SET $set_query.
If you need to validate, trim, etc, do it before the above code like this:
$_POST["surname"] = trim($_POST["surname"];
Actually, you could make your life easier by making your code a bit more complicated - escape the input before inserting into the database!
$sql =
"INSERT INTO clientes SET
"postal = '" . mysql_real_escape_string($_POST['postal']) . "', ".
"morada = '" . mysql_real_escape_string($_POST['morada']) . "', ".
...
First, I recommend you to create a key-value array like this:
$newClient = array(
'codigounico' => md5(uniqid(rand())),
'postal' => $_POST['postal'],
'modelo' => $_POST['selectName'],
...
);
In this array key is the column name your MySQL table.
In the code you've provided not every field is copied right from POST array (some are calculated, and some keys of the POST aren't equal with the tables column names), so you should use a flexible method.
You should still specify all columns and values but only once so code is still maintainable and you won't have any security errors if someone sends you a broken POST. As for me it looks more like configuration than coding.
Then I recommend you to write a function similar to this:
function buildInsertQuery($tableName, $keyValue) {
$result = '';
if (!empty($keyValue)) {
$delimiter = ', ';
$columns = '';
$values = '';
foreach ($keyValue as $key => $value) {
$columns .= $key . $delimiter;
$values .= mysql_real_escape_string($value) . $delimiter;
}
$columns = substr($columns, 0, -length($delimiter));
$values = substr($values, 0, -length($delimiter));
$result = 'INSERT INTO `' . $tableName . '` (' . $columns . ') VALUES (' . $values . ')';
}
return $result;
}
And then you can simply build your query with just one function call:
$query = buildInsertQuery('clientes', $newClient);

Categories