Mapping PHP associative arrays into PDO prepared statements - php

I'm doing some cleanup and transformation on data (that part is done, whew), and need to insert it into a MySQL table. Having done this kind of thing in Perl previously, I assumed that, as part of processing, it would make sense for me to structure the data as an associative array with the keys being the same as the field names I need to load them into - that way, it would be easy to construct a prepared statement simply by looping over the keys and producing a list of both the named placeholders and the matching values.
However, I can't seem to make this work in PHP/PDO. Test code:
$x = <<<EOD
1 1 1 1 1
2 2 2 2 2
3 3 3 3 3
4 4 4 4 4
EOD;
$fields = array('name', 'job', 'wallet_size', 'inseam', 'pet_name');
foreach(explode("\n", $x) as $line){
$data = array_combine($fields, explode(' ', $line));
# print_r($data);
$stmt = $dbh->prepare('INSERT INTO foobar VALUES('.':'.implode(', :', $fields));
foreach($fields as $field){
$stmt->bindParam(':'.$field, $data[$field]);
}
$stmt->execute();
}
Frankly, it feels too... graceless and hacky to work - and it doesn't.
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1' in tst.php:24
There is a correct way to do this, right? I'd appreciate it if someone would introduce me to the appropriate PHP-flavored phrasing for it.

It's hard to debug SQL when you're staring at the PHP code that formats SQL, instead of the final SQL string itself. I suggest you always create a string variable so you can output it during debugging.
$sql = 'INSERT INTO foobar VALUES('.':'.implode(', :', $fields);
echo "$sql\n";
$stmt = $dbh->prepare($sql);
Outputs:
INSERT INTO foobar VALUES(:name, :job, :wallet_size, :inseam, :pet_name
Now it's very easy to see that you forgot the closing ) at the end of that INSERT statement!
Also, your use of PDO is more difficult than it needs to be. You don't need to use named parameters. You don't need to use bindParam(). Here's how I would write this code:
$fields = array('name', 'job', 'wallet_size', 'inseam', 'pet_name');
$columns = implode(',', $fields);
$placeholders = implode(',', array_fill(1, count($fields), '?'));
$sql = "INSERT INTO foobar ($columns) VALUES ($placeholders)";
echo "$sql\n"; // use this during debugging
$stmt = $dbh->prepare($sql);
foreach(explode("\n", $x) as $line){
$param_values = explode(' ', $line);
$stmt->execute($param_values);
}
Tips:
Prepare the query once, and re-use the prepared statement for each row of data you want to insert.
Pass an array of data values as an argument to execute(). This is easier than using bindParam().
Use positional parameter placeholders (?) instead of named parameter placeholders when your data is in a simple array instead of an associative array.

Related

Insert data with associative array using PDO

Before you duplicate your question, I read all answers that it's has a relation with my question. I'm trying to insert data with associative array for example
<?php
$data = array(
'fname'=>'joe',
'lname'=>'sina'
);
foreach ($data as $key=>$value) {
}
?>
I want to display data like this
/*insert into tblname($key)values($value);
finally the query will appear correctly format */
insert into tblname('fname','lname') values('joe','sina');
You don't need to use foreach here. If you just prepare and bind the query, you can pass $data to the execute() and get the keys by implode() on the keys.
$data = array(
'fname'=>'joe',
'lname'=>'sina'
);
$stmt = $pdo->prepare("INSERT INTO tblname (".implode(', ', array_keys($data)).") VALUES (:".implode(', :', array_keys($data)).")");
$stmt->execute($data);
The keys in the array must match the placeholders in the query (the ones with a colon in front of it). You also had a syntax error in the query, as columns cannot be quoted by singlequotes.

Error:sqlstate[hy093]: invalid parameter number

I can convert the echo'ed output in to an SQL statement that executes in phpMyAdmin going...
From this:
INSERT INTO crumbs (ip_address,ip_address_2,device_info,user_id,connections) VALUES(?,?,?,?,?)Value:'00.000.000.000', '00.000.000.000', 0,0000, 1
Into this:
INSERT INTO crumbs (ip_address,ip_address_2,device_info,user_id,connections) VALUES('00.000.000.000', '00.000.000.000', 0,0000, 1)
It inserts the data in to the DB, no errors, however it executes through PHP-PDO...
With:
SQLSTATE[HY093]: Invalid parameter number
The code:
$columns = '('.implode(',', array_keys($user_connection)).''.",user_id,connections)";
$inserts="(".implode(',',array_fill(0,count($user_connection)+2, '?')).")";
$values = implode(', ',($user_connection)).",$user_id, 1";
$sql_insert = "INSERT INTO crumbs ".$columns." VALUES".$inserts;
$stmt = $this->_db->prepare($sql_insert);
$stmt->execute(array($values));
Edit-Adding $user_connection
$user_connection [ 'ip_address'] = "'".$_SERVER['REMOTE_ADDR']."'";
$user_connection [ 'ip_address_2']="'".$_SERVER['HTTP_X_FORWARDED_FOR']."'";
$user_connection ['device_info']=0;
The error occurs during the execution of the SQL code. I've gone over all the examples and found nothing that's equivalent, I'm thinking it's something simple I'm missing (a rule?) since the code executes locally.
You have to do something like this:
// ..code..
$values = $user_connection;
$values[] = $user_id;
$values[] = 1;
// ..code..
$stmt->execute($values);
Explanation of the problem:
When you have multiple ? placeholders, you can pass each value to be bounded as the values of an array (See Example #3 from the manual).
Now, since you are using implode, $values will be a single string, something like
'192.168.0.1', '8.8.8.8', 0, 'userid', 1
That means that when you call execute(array($values)), it will, in fact, bound it like this (representation-only, it's not really like this)
INSERT INTO crumbs (ip_address,ip_address_2,device_info,user_id,connections) VALUES ("'192.168.0.1', '8.8.8.8', 0, 'userid', 1", ?, ?, ?, ?)
because you only sent and array that has one value: the implosion of the other array. Since you didn't provide the same amount of values (1) as you have placeholders (5), you end up with
Invalid parameter number

PHP and MySQL, call_user_func_array doesn't work with variable as argument

I'm trying to set up a dynamic MySQL query, using mysqli prepared statements. I need to compare multiple values (unknown how many) to a single column.
The following works:
$qs = '?,?';
$query = 'SELECT r_id FROM sometable WHERE someindex IN ('.$qs.')';
$statement = $db_conn->prepare($query);
call_user_func_array(array($statement, 'bind_param'), array('ss', 'Something', 'somethingelse'));
I get a result back from the DB, and can do as I please with the return. BUT, the following does not work:
$qs = '?,?';
$query = 'SELECT r_id FROM sometable WHERE someindex IN ('.$qs.')';
$statement = $db_conn->prepare($query);
$test = array('ss', 'Something', 'something else');
call_user_func_array(array($statement, 'bind_param'), $test);
With the only difference being the assignment of $test, instead of creating the array directly within call_user_func_array.
Similarly:
$one = 'pizza';
$two = 'pie';
call_user_func_array(array($statement, 'bind_param'), array('ss', $one, $two));
also does not work. The statement doesn't even execute.
If I can't put variable names directly into the statement, I can't have the dynamic queries. What's going on? Is there a fix for this, or maybe another (equally simple) way I can run a query on a single column, with an unknown number of terms? References or pointers (i.e. &$one) do not work either.
There seems to be a couple known issues with call_user_func_array back in 2012, or around there, but I can't find anything more recent. I am using PHP 5.5
EDIT 1
Known issue, back in 2007 here

Using prepared statements but quotes not being escaped or removed

I am having an issue getting some things to insert into my database. If I put quotes single or double into my text fields it will break the query and will not escape them. I just got done reading that using prepared statements eliminates the need to call mysql_real_escape_string. Can someone tell me if I am executing my query wrong. $companyInfo is an array that contains about 8 rows to be inserted.
function InsertCompanyInfo($companyInfo, $conn) {
foreach($companyInfo as $key => $table) {
$keys = array_keys($table);
$values = null;
$x = 1;
foreach($table as $row => $value) {
$values .= "'$value'";
if($x < count($keys)) {
$values .= ', ';
}
$x++;
}
$sql = $conn->prepare("INSERT INTO {$key} (`" . implode('`, `', $keys) . "`) VALUES ({$values});");
$sql->execute();
$CompanyID = $conn->lastInsertId('CompanyID');
}
return $CompanyID;
}
This is the error I get when I insert qoutes:
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax
error or access violation: 1064 You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax to use near '1',
''''"'"''"';;''';';')' at line 1' in /var/www/Survey/InsertFunctions.php:20 Stack trace: #0
/var/www/Survey/InsertFunctions.php(20): PDOStatement->execute() #1
/var/www/Survey/testProcess.php(8): InsertCompanyInfo(Array, Object(PDO)) #2 {main} thrown
in /var/www/Survey/InsertFunctions.php on line 20
Prepared statements work by separating the query structure and the values in code like so:
$stmt = $pdo->prepare('INSERT INTO foo (bar) VALUES (?)');
This is the query structure, which the database is given first to understand. Then you give it the values separately:
$stmt->execute(array('baz'));
What you're doing instead is you call prepare on a completely formed query which includes crudely interpolated values. There's nothing prepare can do here. The entire problem of escaping values is that the database cannot figure out what a value was and what your part of the query was after the fact. If you're giving the query fully formed and incorrectly escaped to the database, it can't magically recognise what was supposed to be what. You need to add placeholders to the query and provide the corresponding values in a separate step.

MySQL Prepared statements with a variable size variable list

How would you write a prepared MySQL statement in PHP that takes a differing number of arguments each time? An example such query is:
SELECT `age`, `name` FROM `people` WHERE id IN (12, 45, 65, 33)
The IN clause will have a different number of ids each time it is run.
I have two possible solutions in my mind but want to see if there is a better way.
Possible Solution 1 Make the statement accept 100 variables and fill the rest with dummy values guaranteed not to be in the table; make multiple calls for more than 100 values.
Possible Solution 2 Don't use a prepared statement; build and run the query checking stringently for possible injection attacks.
I can think of a couple solutions.
One solution might be to create a temporary table. Do an insert into the table for each parameter that you would have in the in clause. Then do a simple join against your temporary table.
Another method might be to do something like this.
$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$parmcount=count($parms); // = 4
$inclause=implode(',',array_fill(0,$parmcount,'?')); // = ?,?,?,?
$sql='SELECT age, name FROM people WHERE id IN (%s)';
$preparesql=sprintf($sql,$inclause); // = example statement used in the question
$st=$dbh->prepare($preparesql);
$st->execute($parms);
I suspect, but have no proof, that the first solution might be better for larger lists, and the later would work for smaller lists.
To make #orrd happy here is a terse version.
$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$st=$dbh->prepare(sprintf('SELECT age, name FROM people WHERE id IN (%s)',
implode(',',array_fill(0,count($parms),'?'))));
$st->execute($parms);
There is also the FIND_IN_SET function whose second parameter is a string of comma separated values:
SELECT age, name FROM people WHERE FIND_IN_SET(id, '12,45,65,33')
decent sql wrappers support binding to array values.
i.e.
$sql = "... WHERE id IN (?)";
$values = array(1, 2, 3, 4);
$result = $dbw -> prepare ($sql, $values) -> execute ();
Please take #2 off the table. Prepared statements are the only way you should consider protecting yourself against SQL injection.
What you can do, however, is generate a dynamic set of binding variables. i.e. don't make 100 if you need 7 (or 103).
I got my answer from: http://bugs.php.net/bug.php?id=43568.
This is my working mysqli solution to my problem. Now I can dynamically use as many parameters as I want. They will be the same number as I have in an array or as in this case I am passing the ids from the last query ( which found all the ids where email = 'johndoe#gmail.com') to the dynamic query to get all the info about each of these id no matter how many I end up needing.
<?php $NumofIds = 2; //this is the number of ids I got from the last query
$parameters=implode(',',array_fill(0,$NumofIds,'?'));
// = ?,? the same number of ?'s as ids we are looking for<br />
$paramtype=implode('',array_fill(0,$NumofIds,'i')); // = ii<br/>
//make the array to build the bind_param function<br/>
$idAr[] = $paramtype; //'ii' or how ever many ?'s we have<br/>
while($statement->fetch()){ //this is my last query i am getting the id out of<br/>
$idAr[] = $id;
}
//now this array looks like this array:<br/>
//$idAr = array('ii', 128, 237);
$query = "SELECT id,studentid,book_title,date FROM contracts WHERE studentid IN ($parameters)";
$statement = $db->prepare($query);
//build the bind_param function
call_user_func_array (array($statement, "bind_param"), $idAr);
//here is what we used to do before making it dynamic
//statement->bind_param($paramtype,$v1,$v2);
$statement->execute();
?>
If you're only using integer values in your IN clause, there's nothing that argues against constructing your query dynamically without the use of SQL parameters.
function convertToInt(&$value, $key)
{
$value = intval($value);
}
$ids = array('12', '45', '65', '33');
array_walk($ids, 'convertToInt');
$sql = 'SELECT age, name FROM people WHERE id IN (' . implode(', ', $ids) . ')';
// $sql will contain SELECT age, name FROM people WHERE id IN (12, 45, 65, 33)
But without doubt the solution here is the more general approach to this problem.
I had a similiar problem today and I found this topic. Looking at the answers and searching around the google I found a pretty solution.
Although, my problem is a little bit more complicated. Because I have fixed binding values and dynamic too.
This is the mysqli solution.
$params = array()
$all_ids = $this->get_all_ids();
for($i = 0; $i <= sizeof($all_ids) - 1; $i++){
array_push($params, $all_ids[$i]['id']);
}
$clause = implode(',', array_fill(0, count($params), '?')); // output ?, ?, ?
$total_i = implode('', array_fill(0, count($params), 'i')); // output iiii
$types = "ss" . $total_i; // will reproduce : ssiiii ..etc
// %% it's necessary because of sprintf function
$query = $db->prepare(sprintf("SELECT *
FROM clients
WHERE name LIKE CONCAT('%%', ?, '%%')
AND IFNULL(description, '') LIKE CONCAT('%%', ?, '%%')
AND id IN (%s)", $clause));
$thearray = array($name, $description);
$merge = array_merge($thearray, $params); // output: "John", "Cool guy!", 1, 2, 3, 4
// We need to pass variables instead of values by reference
// So we need a function to that
call_user_func_array('mysqli_stmt_bind_param', array_merge (array($query, $types), $this->makeValuesReferenced($merge)));
And the function makeValuesreferenced:
public function makeValuesReferenced($arr){
$refs = array();
foreach($arr as $key => $value)
$refs[$key] = &$arr[$key];
return $refs;
}
Links for getting this 'know-how': https://bugs.php.net/bug.php?id=49946, PHP append one array to another (not array_push or +), [PHP]: Error -> Too few arguments in sprintf();, http://no2.php.net/manual/en/mysqli-stmt.bind-param.php#89171, Pass by reference problem with PHP 5.3.1

Categories