Below is a function designed to handle a search scenario for a custom class.
I've already tripped over the fact that PDO defaults to binding parameters as strings, causing an integer->string conversion even if it's not appropriate. As you'll see, I corrected that by manually checking if the type is integer and then forcing the use of int in those cases. Problem is, my solution only works for a 'start' value of 0 -- anything higher errors out, and I don't know why. If I manually set the start/count values to their appropriate values ( i. e. instead of :count I use {$count}), everything works fine, so it looks like the binding is still messing up.
How? Or if I'm wrong... what is right?
/*Query is:
SELECT tutor_school.id
FROM tutor_school, tutor_states
WHERE tutor_states.stateName=:state AND tutor_states.id=tutor_school.state
GROUP BY tutor_school.id order by tutor_school.name asc
LIMIT :start, :count*/
$db = Database::get_user_db();
$statement = $db->prepare($query);
foreach ($executeArray as $key => $value)
{
if (getType($value) == 'integer')
{
$statement->bindParam($key, $executeArray[$key], PDO::PARAM_INT);
}
else
{
$statement->bindParam($key, $value);
}
}
var_dump($executeArray);//count and start are still ints
if ($statement->execute())
{
var_dump($executeArray);//start and count are now strings
var_dump($statement->errorInfo());
var_dump($query);
$values = $statement->fetchAll();
$return = array();
foreach ($values as $row)
{
$school = School::schoolWithId($row[0]);
if (!empty($school))
{
$return[] = $school;
}
}
return $return;
}
Metadata (such as the LIMIT arguments) can't be parametrized. You will have to use (properly sanitized) interpolation instead.
Related
I have some long MySQL table whose design is not totally fixed yet. So, occasionally, I need to add/delete some columns. But, every time I alter the table, I must re-write all the line dealing with bind_result(). I am looking for a solution which makes this change easy.
Assume I currently have a table with columns like col_a, col_b, col_c, ..., col_z. So, I use the bind_result() to store result values as the manual says.
$res = $stmt->bind_result($a, $b, $c,..., $z);
But, if I change the table design, I must change parameters of all the lines dealing with this bind_result() to match the new MySQL table.
Is there any technique like following?
// Some php file defining constants
define("_SQL_ALL_COLUMNS", "\$a, \$b, \$c, \$d, ... \$z");
// Some SQL process in in other php files
stmt->execute();
$res = $stmt->bind_result(_SQL_ALL_COLUMNS);
So, I don't need to worry about a change of the number of the parameters in other files as long as I once define them correctly somewhere. Of course, I already found that my attempt in the previous example was not a right way.
Is there any good solution for this type of situation?
Use call_user_func_array() to dynamically set the number of parameters:
function execSQL($con, $sql, $params = null)
$statement = $con->prepare($sql);
if (!$statement){
// throw error
die("SQL ERROR: $sql\n $con->error");
}
$type = "";
$arg = array();
if ($params && is_array($params)){
foreach($params as $param){
if (is_numeric($param)){
$type .= 'd';
continue;
}
$type .= 's';
}
$arg[] = $type;
foreach($params as $param){
$arg[] = $param;
}
call_user_func_array(array($statement,'bind_param'), refValues($arg)); // php 7
}
$res = $statement->execute();
if (!$res){
die("Looks like the Execute Query failed.\n\nError:\n{$statement->error}\n\nQuery:\n{$sql}\n\nParams:\n{".implode(",", $arg)."}");
}
return $con->insert_id;
}
function refValues($arr){
if (strnatcmp(phpversion(),'5.3') >= 0) { //Reference is required for PHP 5.3+
$refs = array();
foreach($arr as $key => $value){
$refs[$key] = &$arr[$key];
}
return $refs;
}
return $arr;
}
You can use it by calling the function execSQL with an array of parameters:
$result = execSQL($connection,$sql,["a","b","c","..."]);
What this does is check the data type of the parameters and appends to the $type variable, which will then be passed to the bind method as first parameter.
Sorry if this is a duplicate, I have tried searching but cannot seem to find an answer. I may just have the piece of code in the wrong place.
I have counted the duplicate values input from various select boxes, which were sent over via $_GET. Using these duplicates, if more than (whatever the set amount is) then it will run through a mysql query. This is all working fine.
The issue is that I need to remove duplicates that are returned from the mysql query. Here is my code:
if ($countGearSelected >= 2) {
$gearSets = array_keys(array_filter(array_count_values($_GET['gearPiece']), function($v) {
return $v > 1;
}));
foreach ($gearSets as $gearSetKey => $gearSetValue) {
$result = mysqli_query($con,"SELECT twoPieceBonus FROM sets WHERE setName='".$gearSetValue."';");
while($row = mysqli_fetch_array($result)){
$twoPieceBonus .= urldecode($row['twoPieceBonus']).'</br></br>';
}
$twoPieceBonus = implode(',',array_unique(explode(',', $twoPieceBonus)));
$twoSelected = substr($twoPieceBonus, 0, -10);
}
}else{
$twoSelected = '';
}
As you can see, I have tried the array_unique option on various other posts on SE but it doesn't appear to be working. I think I may be using it incorrectly?
Using DISTINCT doesn't work in the mysql query, as a few of the "sets" that are being queried have the same result (if that makes sense?).
Any help is very much appreciated.
First: your code is vulnerable to SQL injection: use prepared statements to avoid this.
Secondly, it is often a bad idea to execute a query in each iteration of a loop. And in this case it can be avoided. Instead of an equality comparison in your where clause, you could use the in operator and compare to all gear sets in one go.
This will also solve the matter of getting distinct values. With only one query executing, you can use distinct now.
Here is how the code would look like. I could not test this, but I expect mistakes (if any) can be easily fixed:
$twoSelected = '';
if ($countGearSelected >= 2) {
$gearSets = array_keys(array_filter(
array_count_values($_GET['gearPiece']), function($v) {
return $v > 1;
}
));
// Create comma separated list of question marks
$placeHolders = implode(",", array_fill(0, count($gearSets), "?"));
// Prepare SQL statement with it
$stmt = mysqli_prepare($con,
"SELECT DISTINCT twoPieceBonus
FROM sets
WHERE setName IN ($placeHolders);");
// All gearSet values are strings:
$types = str_repeat("s", count($gearSets));
// Turn the gearSets into references
$gearSetRefs = [];
foreach ($gearSets as $i => $_) {
$gearSetRefs[] = &$gearSets[$i];
}
// Bind arguments
mysqli_stmt_bind_param($stmt, $types, ...$gearSetRefs); // the splat operator
// Now we are all set to (safely) execute the query
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
// Let the result of the URL decoding still be an array
$twoPieceBonus = [];
while ($row = mysqli_fetch_array($result)) {
$twoPieceBonus[] = urldecode($row['twoPieceBonus']);
}
mysqli_stmt_close ($stmt);
// ... and then use implode to insert those HTML breaks
$twoSelected = implode("</br></br>", $twoPieceBonus);
}
For many queries, it's faster and easier to just use the question-mark notation for a query. There are various guides and posts about getting the final run query by extending PDO, but I never see one that works when using the question-mark notation. Can this be done? I'm running a query that appears by all accounts to work, but it's not returning results in the actual PDO code for some reason. What does it take to actually get the EXACT final query that PDO is running so I can see where the error is?
$sth = $dbh->prepare("SELECT fromID,toID,m_key FROM messages WHERE (fromID = ? AND toID IN (?)) OR (toID = ? AND fromID IN (?)) AND message LIKE 'addfriend'");
$sth->execute(array($_SESSION['userID'],$include,$_SESSION['userID'],$include));
The problem is not with the use of the ? placeholder, but the fact that you try to bind a single variable to represent a list of variables in the in operator. You have to provide as many ? placeholders in the in operator separated by commas as the number of parameters you want to have there and you need to bind to each placeholder separately.
...fromID IN (?, ?, ?, ..., ?)...
The below method will help you to substitute the value ($params) against ? or :param placeholder. So that we can see the exact query to be executed.
Note: It is useful only for development debugging purpose.
public static function interpolateQuery($query, $params) {
$keys = array();
$values = $params;
# build a regular expression for each parameter
foreach ($params as $key => $value) {
if (is_string($key)) {
$keys[] = '/:'.$key.'/';
} else {
$keys[] = '/[?]/';
}
if (is_array($value))
$values[$key] = implode(',', $value);
if (is_null($value))
$values[$key] = 'NULL';
}
// Walk the array to see if we can add single-quotes to strings
array_walk($values, create_function('&$v, $k', 'if (!is_numeric($v) && $v!="NULL") $v = "\'".$v."\'";'));
$query = preg_replace($keys, $values, $query, 1, $count);
return $query;
}
Theres's a similar question here, but actually that doesn't give me the answer:
PHP + PDO: Bind null if param is empty
I need my statement work in a loop, with only changing the binded variables.
Like:
$this->array = array(
"cell1" => "",
"cell2" => "",
);
$this->sth = $db->prepare("INSERT INTO `table`
(`coloumn1`, `coloumn2`)
VALUES (:coloumn1, :coloumn2)");
$this->sth->bindParam(:coloumn1, $this->array['cell1'], PDO::PARAM_STR);
$this->sth->bindParam(:coloumn2, $this->array['cell2'], PDO::PARAM_STR);
//Data proccessing...
foreach($data as $value){
$this->array['cell1'] = $value['cell1'];
$this->array['cell2'] = $value['cell2'];
try {
this->sth->execute();
print_r($this->sth->errorInfo());
}
catch(PDOException $e){
echo 'sh*t!';
}
}
Everything works well until either of the values is an empty string.
My problem is when 'cell1' is an empty string, the bound parameter is a nullreference, and it won't work. But I need the referenced binding because of the loop, so bindValue isn't a solution.
And I need the loop very bad, because of the huge data I want to process.
Any suggestion?
I tried right before execute:
foreach($this->array as $value){
if(!$value) {
$value = "";
}
}
It doesn't work.
The only way that solved my problem is modifying to this:
$this->array['cell1'] = !empty($value['cell1']) ? $value['cell1'] : "";
$this->array['cell2'] = !empty($value['cell2']) ? $value['cell2'] : "";
But this seems too rubbishy...
I know its necroposting, but maybe will help to someone.
You trying to check if the variable false, but not null. And not reapplying values back to array.
foreach($this->array as $value){
if(!$value) {
$value = "";
}
}
Try to check for null in loop
foreach($this->array as $index => $value)
{
$this->array[$index] = !empty($value) ? $value : '';
}
Your question has nothing to do with PDO but with basic PHP.
When there is no variable available - you can't use it at all. So, you have to create it somehow. The way you are using at the moment is not "rubbishy" but quite acceptable. I'd rather call whole code "rubbishy" as it's twice as big as as it should be.
But I need the referenced binding because of the loop, so bindValue isn't a solution.
This assumption is wrong too. Why do you think you can't use bind by value?
$sql = "INSERT INTO `table` (`coloumn1`, `coloumn2`) VALUES (?, ?)";
$sth = $db->prepare($sql);
foreach($data as $value)
{
$value['cell1'] = !empty($value['cell1']) ? $value['cell1'] : "";
$value['cell2'] = !empty($value['cell2']) ? $value['cell2'] : "";
$sth->execute($value);
}
as simple as this
And I need the loop very bad, because of the huge data I want to process.
I don't think it's really huge, as it fits for the PHP process memory. However, consider to use LOAD DATA INFILE query for the real huge amounts.
I am in the process of converting our site from the PHP Mysql API to PDO, and have run into an issue with data types.
Previously, we escaped all of our variables as if they were strings. for example,
SET varname = '$varvalue'
Now, with PDO, of course, I do
SET varname = :varvalue
then we have a class that handles binding of the value of $varvalue, setting the data type based on the type of the variable.
The problem for us comes when varname is supposed to be a string, and $varvalue is, for some reason, null. previously, '$varvalue' would have just become '' when $varvalue is null. Now, we are "properly" binding $varvalue as null, but the database field does not allow null.
I know that the most correct way to fix this would be to ensure that $varvalue comes into the function with the correct value, but we have a large legacy code base, and that would be really a lot of work to implement. Another solution would be to explicitly cast every variable when we bind it to the correct type. We'd prefer a solution that avoids us having to explicitly cast every variable in our models, if possible. Is there one?
This might not be an answer you were waiting for, but it should be mentioned: use exceptions.
You can configure PDO to throw exceptions of type PDOException instead of relying on return values:
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
You can catch these exceptions and log them, email them, etc. so that you can identify exactly what piece of code was passing in the wrong values and fix it.
This is a somewhat painful operation; we've had to endure this ourselves when we started to report all uncaught exceptions on our website and our inboxes were cluttered with errors. It lasted a few days, but we managed to weed out all the really bad code :)
Since previously you did not mind having empty strings, why not just checking if var value is null or not?
$stmt->bindParam(':varvalue', (is_null($varvalue) ? '' $varvalue), PDO::PARAM_STR);
You should probably be using an abstraction for your application rather than using PDO directly. In your abstraction layer you can perform any type conversions you want to perform.
If this is absolutely not an option and you must use PDO directly, then you can try subclassing and delegation to produce PDO-workalike objects that perform the typecasting you want. However, if there is any column that does expect null, you are going to greatly complicate your abstraction layer. You may need to do introspection on the database or some other trickery. You will probably not be able to preserve the PDO API exactly.
PDOStatement Delegation
This is pretty straightforward for bindValue. However, bindParam uses references and we can't typecast those without rewriting them, so we need a workaround that converts these to bindValue calls when execute is called.
First we subclass PDO so it returns our new wrapped PDOStatement.
class PDO_nullcast extends PDO {
public function prepare($statement, $driver_options=array()) {
$prepared = parent::prepare($statement, $driver_options);
$delegated_prepared = new PDOStatement_nullcast($prepared);
return $delegated_prepared;
}
}
Then we create a delegate PDOStatement_nullcast which has your null-casting semantics. Our first try will only override bindValue.
class PDOStatement_nullcast {
protected $pstmt;
protected $bindparams; // this is for later
function __construct(PDOStatement $pstmt) {
$this->pstmt = $pstmt;
$this->bindparams = array();
}
function __get($k) {
return $this->pstmt->{$k};
}
function __set($k, $v) {
$this->pstmt->{$k} = $v;
}
function __call($k, $a) {
return call_user_func_array(array($this->pstmt, $k), $a);
}
function bindValue($parameter, $value, $data_type=PDO::PARAM_STR) {
$newvalue = $this->castValue($value, $data_type);
return $this->pstmt->bindValue($parameter, $newvalue, $data_type);
}
static public function castValue($val, $typehint) {
$newval = $val;
if ($val===NULL) {
if ($typehint===PDO::PARAM_STR) {
$newval = '';
} else if ($typehint===PDO::PARAM_INT) {
$newval = 0;
} else if ($typehint===PDO::PARAM_BOOL) {
$newval = false;
}
} else {
if ($typehint===PDO::PARAM_STR) {
$newval = (string) $val;
} else if ($typehint===PDO::PARAM_INT) {
$newval = (int) $val;
} else if ($typehint===PDO::PARAM_BOOL) {
$newval = (bool) $val;
}
}
return $newval;
}
}
Here is some demonstration code. We will be using the following table as an example:
CREATE TABLE `typetest` (
`intcol` int(11) NOT NULL,
`strcol` varchar(255) NOT NULL,
`intnullcol` int(11) DEFAULT NULL,
`intstrcol` varchar(255) DEFAULT NULL,
)
Now the PHP code. Assuming you have a PDO_nullcast object assigned to $db:
$sql = 'INSERT INTO typetest (`intcol`, `strcol`, `intnullcol`, `intstrcol`) VALUES (?,?,?,?)';
$insert = $db->prepare($sql);
$insert->bindValue(1, null, PDO::PARAM_INT);
$insert->bindValue(2, null, PDO::PARAM_STR);
$insert->bindValue(3, null, PDO::PARAM_INT);
$insert->bindValue(4, null, PDO::PARAM_STR);
$insert->execute();
$insert->closeCursor();
$select = $d->prepare('SELECT * FROM typetest');
$select->execute();
$res = $select->fetchAll();
$select->closeCursor();
var_dump($res);
You can change the castValue function to the semantics you want.
However, this will not handle the bindParam case. Here we need to keep a reference around internally until execute is called on our wrapper, then convert these to bindValue calls. We can't handle all uses of bindParam this way, though! There's no workaround for INOUT parameters because we can't preserve the reference through typecasting.
We can intercept bindParam and execute calls like so to get what we want (add the following methods to the PDOStatement_nullcast class above):
function bindParam($parameter, &$variable, $data_type=PDO::PARAM_STR, $length=null, $driver_options=null) {
if (isset($length) || isset($driver_options) || ($data_type & PDO::PARAM_INPUT_OUTPUT)) {
// in either of these cases, we cannot wrap!
return $this->pstmt->bindParam($parameter, $variable, $data_type, $length, $driver_options);
}
// note we preserve a reference to the variable
$this->bindparams[] = array($parameter, &$variable, $data_type);
return true; // this is a bit of a lie--we can't know if we would have an error until later.
}
function execute($input_parameters=null) {
if ($input_parameters!==null) {
return $this->pstmt->execute($input_parameters);
}
// for-loop is to preserve references more clearly
// foreach is trickier
for ($i=0; $i < count($this->bindparams); $i++) {
call_user_func_array(array($this,'bindValue'), $this->bindparams[$i]);
}
return $this->pstmt->execute();
}
And here is some test code using bindParam:
$var = null;
$insert->bindParam(1, $var, PDO::PARAM_INT);
$insert->bindParam(2, $var, PDO::PARAM_STR);
$insert->bindParam(3, $var, PDO::PARAM_INT);
$insert->bindParam(4, $var, PDO::PARAM_STR);
error_log($var);
$insert->execute();
$var = 1;
$insert->execute();
$var = 2;
$insert->execute();