PDO binding values for MySQL IN statement [duplicate] - php

This question already has answers here:
Can I bind an array to an IN() condition in a PDO query?
(23 answers)
Closed 2 years ago.
I have an issue with PDO that I'd really like to get an answer for after being plagued by it for quite some time.
Take this example:
I am binding an array of ID's to a PDO statement for use in a MySQL IN statement.
The array would be say: $values = array(1,2,3,4,5,6,7,8);
The database-safe variable would be $products = implode(',' $values);
So, $products would then be a STRING with a value of: '1,2,3,4,5,6,7,8'
The statement would look like:
SELECT users.id
FROM users
JOIN products
ON products.user_id = users.id
WHERE products IN (:products)
Of course, $products would be bound to the statement as :products.
However, when the statement is compiled and values bound, it would actually look like this:
SELECT users.id
FROM users
JOIN products
ON products.user_id = users.id
WHERE products IN ('1,2,3,4,5,6,7,8')
The problem is it is executing everything inside of the IN statement as a single string, given that I've prepared it as comma-separated values to bind to the statement.
What I actually need is:
SELECT users.id
FROM users
JOIN products
ON products.user_id = users.id
WHERE products IN (1,2,3,4,5,6,7,8)
The only way I can actually do this is by placing the values within the string itself without binding them, however I know for certain there has to be an easier way to do this.

This is the same thing as was asked in this question: Can I bind an array to an IN() condition?
The answer there was that, for a variable sized list in the in clause, you'll need to construct the query yourself.
However, you can use the quoted, comma-separated list using find_in_set, though for large data sets, this would have considerable performance impact, since every value in the table has to be cast to a char type.
For example:
select users.id
from users
join products
on products.user_id = users.id
where find_in_set(cast(products.id as char), :products)
Or, as a third option, you could create a user defined function that splits the comma-separated list for you (cf. http://www.slickdev.com/2008/09/15/mysql-query-real-values-from-delimiter-separated-string-ids/). This is probably the best option of the three, especially if you have a lot of queries that rely on in(...) clauses.

A good way to handle this situation is to use str_pad to place a ? for every value in the SQL query. Then you can pass the array of values (in your case $values) as the argument to execute:
$sql = '
SELECT users.id
FROM users
JOIN products
ON products.user_id = users.id
WHERE products.id IN ('.str_pad('',count($values)*2-1,'?,').')
';
$sth = $dbh->prepare($sql);
$sth->execute($values);
$user_ids = $sth->fetchAll();
This way you get more benefit from using prepared statements rather than inserting the values directly into the SQL.
PS - The results will return duplicate user ids if the products with the given ids share user ids. If you only want unique user ids I suggest changing the first line of the query to SELECT DISTINCT users.id

The best prepared statement you could probably come up with in a situation like this is something resembling the following:
SELECT users.id
FROM users
JOIN products
ON products.user_id = users.id
WHERE products IN (?,?,?,?,?,?,?,?)
You would then loop through your values and bind them to the prepared statement making sure that there are the same number of question marks as values you are binding.

you need to provide same number of ?s in IN as the number of values in your $values array
this can be done easily by creating an array of ?s as
$in = join(',', array_fill(0, count($values), '?'));
and use this $in array in your IN clause
THis will dynamically provide you with tailor made array of ?s as per your changiing $values array

You can do so very easily.
If you have an array of values for your IN() statement
EG:
$test = array(1,2,3);
You can simply do
$test = array(1,2,3);
$values = count($test);
$criteria = sprintf("?%s", str_repeat(",?", ($values ? $values-1 : 0)));
//Returns ?,?,?
$sql = sprintf("DELETE FROM table where column NOT IN(%s)", $criteria);
//Returns DELETE FROM table where column NOT IN(?,?,?)
$pdo->sth = prepare($sql);
$pdo->sth->execute($test);

If the expression is based on user input without binding the values with bindValue(), experimental SQL might not be a great choice. But, you can make it safe by checking the syntax of the input with MySQL's REGEXP.
For example:
SELECT *
FROM table
WHERE id IN (3,156)
AND '3,156' REGEXP '^([[:digit:]]+,?)+$'

Here is an example of binding an unknown number of record columns to values for an insert.
public function insert($table, array $data)
{
$sql = "INSERT INTO $table (" . join(',', array_keys($data)) . ') VALUES ('
. str_repeat('?,', count($data) - 1). '?)';
if($sth = $this->db->prepare($sql))
{
$sth->execute(array_values($data));
return $this->db->lastInsertId();
}
}
$id = $db->insert('user', array('name' => 'Bob', 'email' => 'foo#example.com'));

Please try like this:
WHERE products IN(REPLACE(:products, '\'', ''))
Regards

Related

Dynamic PDO parameter binding issue with WHERE clause

I have a search form where users can enter a few pieces of information to search for records in the database. Due to the fact that some of the fields can be left blank, I am dynamically creating the WHERE clause of the query as well as dynamically binding the PDO parameters. Everything works great if the user only fills out 1 field in the search form but if more than 1 field is used then an empty array is returned. Here is my code.
if(count($_POST)>0)
{
//Remove any key that has no value
$data = array_filter($_POST);
//Define array to hold the pieces of the where clause
$where = array();
//loop each of the variable to build the query
foreach($data as $key=>$value)
{
$key = mysql_real_escape_string($key);
//Push values to array
array_push($where, "$key=:$key");
}
//Create the select query
$query = "SELECT application_ID,
student_last_name,
student_first_name,
s.school_name,
DATE_FORMAT(submission_datetime, '%m/%d/%Y %h:%i:%s %p') AS submission_datetime,
aps.name
FROM application a
LEFT JOIN application_status aps ON(aps.status_ID = a.application_status_ID)
LEFT JOIN schools s ON(s.school_ID = a.school_choice)";
//As long as criteria was selected in the search form then add the where clause to the query with user's search criteria
if(!empty($where))
{
$query .= "WHERE ".implode(" AND ", $where);
}
//Add ORDER BY clause to the query
$query .= " ORDER BY application_ID";
$stmt = $conn->prepare($query);
//loop each of the variables to bind parameters
foreach($data as $key=>$value)
{
$value = mysql_real_escape_string($value);
$stmt->bindparam(':'.$key, $value);
}
$stmt->execute();
$result = $stmt->fetchall(PDO::FETCH_ASSOC);
}
When I echo the query everything looks fine and even returns results when run from PHPMyAdmin. Here is the query.
SELECT application_ID,
student_last_name,
student_first_name,
s.school_name,
DATE_FORMAT(submission_datetime, '%m/%d/%Y %h:%i:%s %p') AS submission_datetime,
aps.name
FROM application a
LEFT JOIN application_status aps ON(aps.status_ID = a.application_status_ID)
LEFT JOIN schools s ON(s.school_ID = a.school_choice)
WHERE school_choice=:school_choice AND status_ID=:status_ID
ORDER BY application_ID ASC
When I print_r I get an empty array.
Thanks for any help you can provide.
When you iterate through an array to bind values to the PDO statement you should use bindValue instead of bindParam.
When you say $stmt->bindparam(':'.$key, $value), the query will use the value of the variable $value as it is at the time of the query execution. Value of $value will be the last element of the array.
http://php.net/manual/en/pdostatement.bindvalue.php
I hope this helps.
You are not supposed to use mysql_real_escape_string() with prepared statements. And in fact, this function will not work if you don't have a mysql_connect() initialized, which you don't.
That must be why it all is failing, your calls to mysql_real_escape_string() are returning FALSE for everything.
Also, what makes you think array keys coming from $_POST are safe to be used in your SQL query? You are taking a serious risk of SQL injection here, don't ever do that.

PDO Prepare Not Replacing Named Placeholders

I have tried using named placeholder to fill in the data as shown here:
$STH = $DBH->prepare("SELECT mixes.* FROM mixes JOIN(SELECT id FROM mixes WHERE id NOT IN ( :noredo_ids ) ORDER BY RAND() LIMIT 1) ips on mixes.id = ips.id");
$STH->bindParam(':noredo_ids', $_GET["noredo"]);
$STH->setFetchMode(PDO::FETCH_ASSOC);
$STH->execute();
As well as trying
$arr2["ids"] = $_GET["noredo"];
$STH = $DBH->prepare("SELECT mixes.* FROM mixes JOIN(SELECT id FROM mixes WHERE id NOT IN ( :ids ) ORDER BY RAND() LIMIT 1) ips on mixes.id = ips.id");
$STH->setFetchMode(PDO::FETCH_ASSOC);
$STH->execute($arr2);
But neither of those are working. But when I try to manually put the string in instead of using placeholders, it does work:
$arr2["ids"] = $_GET["noredo"];
$STH = $DBH->prepare("SELECT mixes.* FROM mixes JOIN(SELECT id FROM mixes WHERE id NOT IN (". $arr2['ids'] .") ORDER BY RAND() LIMIT 1) ips on mixes.id = ips.id");
$STH->setFetchMode(PDO::FETCH_ASSOC);
$STH->execute();
Should I manually escape the string? Am I missing something obvious? Thanks!
I am not certain since PHP's documentation on named placeholders for prepared statements is kinda vague about this. http://php.net/manual/en/pdostatement.bindparam.php
But if $_GET['noredo'] is an array of IDs you need to first implode(',',$_GET['noredo']) before passing into the prepared statement, I do not believe that the placeholder replacement is smart enough to flatten the array into a comma separated list acceptable for use in IN().
And in PHP docs, the replacement is a single value and not an array of values, so (and this is where I'm fuzzy) I don't believe that it looks for :named_placeholder in the array you pass it.
But then again, I've only used the ? placeholder for prepared statements...
A query parameter always takes the place of one single scalar value in an SQL expression.
So if you expect $_GET["noredo"] to be an array or a comma-separated list of values, what you're doing won't work. The query will be run as if you did this:
WHERE id NOT IN ( '1,2,3,4' )
A quoted string value that contains a comma-separated list is not the same as a series of comma-separated values. It's one string, and in a numeric context SQL will convert '1,2,3,4' into the scalar number 1.
So you need to use multiple placeholders if you want to bind multiple values in an IN() predicate.
$id_array = (array) $_GET["noredo"];
$placeholders = implode(",", array_fill(0,count($id_array),"?"));
$STH = $DBH->prepare("SELECT mixes.* FROM mixes JOIN(SELECT id FROM mixes
WHERE id NOT IN ( $placeholders ) ORDER BY RAND() LIMIT 1) ips on mixes.id = ips.id");
$STH->execute($id_array);
The second example doesn't work because you used $arr2["ids"] instead of $arr2[":ids"] as far as I can tell. That is, you still have to use the full parameter name.
Also, you should make sure that the values you try to insert are properly sanitized and cannot cause a syntax error.

Avoiding a query inside a loop in my situation

I have quite a complicated situation here. I can't find a better way to solve this without putting a SELECT query inside a loop that rolls over 70000 times when I enter that page (don't worry, I use array_chunk to split the array into pages). I guess this would be a resource killer if I use a query here. Because of this, here I am, asking a question.
I have this big array I need to loop on:
$images = scandir($imgit_root_path . '/' . IMAGES_PATH);
$indexhtm = array_search('index.htm', $images);
unset($images[0], $images[1], $images[$indexhtm]);
Now I have an array with all file names of the files (images) in my IMAGES_PATH. Now the problem comes here:
Some of these images are registered on the database, because registered users have their images listed on my database. Now I need to retrieve the user_id based on the image name that the array above gives me.
Inside a loop I simply did this:
foreach ($images as $image_name)
{
$query = $db->prepare('SELECT user_id FROM imgit_images WHERE image_name = :name');
$query->bindValue(':name', $image_name, PDO::PARAM_STR);
$query->execute();
$row = $query->fetch(PDO::FETCH_ASSOC);
$user_id = $row['user_id'];
echo $user_id;
}
This works just fine, but the efficiency equals to 0. Using that user_id I plan on getting other stuff from the imgit_users table, such as username, which would require another query inside that loop.
This is too much and I need a simpler way to deal with this.
Is there a way to get those user_ids before going inside the loop and use them IN the loop?
This is the table structure from imgit_images:
While this is the schema for imgit_users:
Something like this would work (I'm not sure if it's possible to prepare the WHERE IN query since the # of values is unknown... Else, make sure you sanatize $images):
$image_names = "'".implode("', '", $images)."'";
$query = $db->prepare("SELECT img.user_id, image_name, username
FROM imgit_images img
INNER JOIN imgit_users u ON u.user_id = img.user_id
WHERE image_name IN(".$image_names.")");
$query->execute();
while($row = $query->fetch(PDO::FETCH_ASSOC))
{
echo $row['user_id']."'s image is ".$row['image_name'];
}
You might need to tweak it a little (haven't tested it), but you seem to be able to, so I'm not worried!
Not sure if it is going to help, but I see a couple of optimizations that may be possible:
Prepare the query outside the loop, and rebound/execute/get result within the loop. If query preparation is expensive, you may be saving quite a bit of time.
You can pass an array as in Passing an array to a query using a WHERE clause and obtain the image and user id, that way you may be able to fragment your query into a smaller number of queries.
Can you not just use an INNER JOIN in your query, this way each iteration of the loop will return details of the corresponding user with it. Change your query to something like (i'm making assumptions as to the structure of your tables here):
SELECT imgit_users.user_id
,imgit_users.username
,imgit_users.other_column_and_so_on
FROM imgit_images
INNER JOIN imgit_users ON imgit_users.user_id = imgit_images.user_id
WHERE imgit_images.image_name = :name
This obviously doesn't avoid the need for a loop (you could probably use string concatenation to build up the IN part of your where clause, but you'd probably use a join here anyway) but it would return the user's information on each iteration and prevent the need for further iterations to get the user's info.
PDO makes writing your query securely a cinch.
$placeholders = implode(',', array_fill(0, count($images), '?'));
$sql = "SELECT u.username
FROM imgit_images i
INNER JOIN imgit_users u ON i.user_id = u.id
WHERE i.image_name IN ({$placeholders})";
$stmt = $db->prepare($sql);
$stmt->execute($images);
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
// use $row['username']
}
Create a string of comma separated ?s and write them into IN's parentheses. Then pass the array of images to execute(). Easily done, and now all of your desired data is available in a single resultset from a single query. (Add additional columns to your query's SELECT clause as needed.)

How to use prepared statements in queries with an IN clause in PHP [duplicate]

This question already has an answer here:
Bind multiple parameters into mysqli query
(1 answer)
Closed 5 years ago.
I need to make a simple query
$array_of_ids = array();
//poulate $array_of_ids, they don't come from another db but from Facebook
//so i can't use a subquery for the IN clause
$wpdb->prepare("SELECT id from table where id IN (%d, %d)", $array_of_ids [0], $array_of_ids [1]);
The question is, if i have 200 elements in the array, what is the correct way to handle this?Do i have to manually build the query with 200 %d? I need this query because i must "sync" my database with facebook data and i have to check if the user i have in the db are present, update those that are present, insert new users and delete those that are not my friend.
If you know for certain that the array elements are numeric:
$wpdb->prepare("SELECT id FROM table WHERE id IN ("
. implode(',',$array_of_ids) . ")");
Otherwise, you can use the vsprintf form of prepare to pass in the array of parameters:
$wpdb->prepare("SELECT id FROM table WHERE id IN ("
. str_repeat("%d,", count($array_of_ids)-1) . "%d)" , $array_of_ids);
I'm not sure that this is a good approach, but you could do it in this fashion:
$sql = "SELECT id from table where id IN ("
. implode(',', array_fill(0, count($array_of_ids), "%d"))
. ")";
call_user_func_array(array($wpdb, 'prepare'), $array_of_ids);
This builds a string with the appropriate number of %d, then uses call_user_func_array to do it dynamically.
That said, I'm not sure this is really a case where prepared statements are worth the hassle, given how easy it is to sanitise integers.
Yes, dynamic sql is the way here. Fortunately, integers are easy to not screw up with.
$vals = array_filter(array_map('intval', $vals));
make sure you have at least one value and then implode it. Not need for a prepared statement here, just execute the sql.
Since this has no accepted answer yet I'll go with my approach with array_filter
$array_of_ids = array(0,1,1,2,3,5,8,13);
echo "SELECT id from table where id IN (".implode(',', array_filter($array_of_ids,'is_int')).")";
will output
SELECT id from table where id IN (0,1,1,2,3,5,8,13)
while
$array_of_ids = array('zero',1,true,2,3,5,8,'thirteen');
echo "SELECT id from table where id IN (".implode(',', array_filter($array_of_ids,'is_int')).")";
will output
SELECT id from table where id IN (1,2,3,5,8)
Please note that is_int doesn't work with $_GET variables so use is_numeric instead
You can do this :
$query = $wpdb->prepare("SELECT id from table where id IN :param");
$query->bindParam("param", "(".implode(',', array_map('intval', $array_of_ids)).")");

Use an array in a mysqli prepared statement: `WHERE .. IN(..)` query [duplicate]

This question already has answers here:
How can I bind an array of strings with a mysqli prepared statement?
(7 answers)
Closed 10 months ago.
Imagine we have a query:
SELECT * FROM somewhere WHERE `id` IN(1,5,18,25) ORDER BY `name`;
and an array of IDs to fetch: $ids = array(1,5,18,25)
With prepared statements it's adviced to prepare one statement and call it multiple times:
$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id`=?;');
foreach ($ids as $id){
$stmt->bind_params('i', $id);
$stmt->exec();
}
But now I'll have to sort the results manually. Do I have any nice alternatives?
you could do it this way:
$ids = array(1,5,18,25);
// creates a string containing ?,?,?
$clause = implode(',', array_fill(0, count($ids), '?'));
$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id` IN (' . $clause . ') ORDER BY `name`;');
call_user_func_array(array($stmt, 'bind_param'), $ids);
$stmt->execute();
// loop through results
Using this you're calling bind_param for each id and you have sorting done by mysql.
Had the same problem and in addition to the answer of #sled 7 years ago, here is a possibility without making the call_user_func_array(array($stmt, 'bind_param'), $ids); step, but only call bind_params once:
$ids = array(1,5,18,25);
// creates a string containing ?,?,?
$bindClause = implode(',', array_fill(0, count($ids), '?'));
//create a string for the bind param just containing the right amount of s
$bindString = str_repeat('s', count($ids));
$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id` IN (' . $bindClause . ') ORDER BY `name`;');
$stmt->bind_param($bindString, ...$ids);
$stmt->execute();
I believe this is the simplest possible answer :
$ids = [1,2,3,4,5];
$pdos = $pdo->prepare("SELECT * FROM somwhere WHERE id IN (:"
. implode(',:', array_keys($ids)) . ") ORDER BY id");
foreach ($ids as $k => $id) {
$pdos->bindValue(":". $k, $id);
}
$pdos->execute();
$results = $pdos->fetchAll();
So long your array of Ids does not contain keys or keys with illegal characters, it wil work.
For the task of executing a secure mysqli query with a dynamic number of incoming values to be fed into the sql string, a prepared statement is the professional technique to implement.
Let's assume that the incoming data payload is user-supplied data -- this means that we cannot guarantee the integrity of the data nor can we guarantee the volume of data. In fact, the expected array of data might be empty. The below snippet will demonstrate how to pass an array of ids to the IN () condition in the WHERE clause of a prepared statement. If there are no values in the array, then a prepared statement provides no benefit and should not be used.
MySQLi result set objects can be immediately iterated by a foreach() loop. Therefore, it is not necessary to make iterated fetch calls; just access the rows' data using array syntax.
The array of ids means that the sql will expect integer values. When calling bind_param(), the first parameter will be a single string of repeated i characters. For general use, if the data will be strings or you might have a mix of data types (e.g. integers, floats/doubles, or strings), then is simpler to just use repeated s characters instead of i characters.
Code: (PHPize.online Demo with SQL)
$ids = [1, 5, 18, 25]; // this could be, for example: $_GET['ids']
$count = count($ids);
$sql = 'SELECT name FROM somewhere';
$orderBy = 'ORDER BY name';
if ($count) {
$placeholders = implode(',', array_fill(0, $count, '?'));
$stmt = $mysqli->prepare("$sql WHERE id IN ($placeholders) $orderBy");
$stmt->bind_param(str_repeat('i', $count), ...$ids);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $mysqli->query("$sql $orderBy"); // a prepared statement is unnecessary
}
foreach ($result as $row) {
echo "<div>{$row['name']}</div>\n";
}
Output from my PHPize demo:
<div>Alan</div>
<div>Bill</div>
<div>Chad</div>
<div>Dave</div>
If you don't need to iterate the result set for any reason, then you can fetch_all(). This is commonly used when immediately echoing or returning a json-encoded string (say, as the response to an ajax call). In this case, you replace the foreach() block with: (PHPize.online Demo with SQL)
echo json_encode($result->fetch_all(MYSQLI_ASSOC));
or simply dump the multidimensional array:
var_export($result->fetch_all(MYSQLI_ASSOC));
Output from my PHPize demo:
[{"name":"Alan"},{"name":"Bill"},{"name":"Chad"},{"name":"Dave"}]
From PHP8.1 and higher, it is no longer necessary to call bind_param() because the execute() method can receive the payload of parameters as an array (like PDO).
This means that...
$stmt->bind_param(str_repeat('i', $count), ...$ids);
$stmt->execute();
can be replaced with...
$stmt->execute($ids);
Here's a complete, basic example: (PHPize.online Demo)
$ids = [1, 2, 3, 4, 5];
$stmt = $mysqli->prepare("SELECT * FROM somewhere WHERE id IN (" . rtrim(str_repeat('?,', count($ids)), ',') . ") ORDER BY id");
$stmt->execute($ids);
var_export($stmt->get_result()->fetch_all(MYSQLI_ASSOC));
Topical Resources:
php.net
The RFC was authored by our very own Dharman ♦ and implemented as part of PHP8.1 after a unanimous vote on 2021-03-27.
phpbackend.com article from 24, October 2021
Reddit thread
PDO can do this concisely.
I'll add a slow & ugly solution which nevertheless uses prepared statements for ANY number of array items :) 3 statements are universal for any case and can be reused everywhere.
CREATE TEMPORARY TABLE `ids`( `id` INT );
INSERT INTO `ids` VALUES(?); this will insert your IDs
SELECT `id` FROM `ids` LEFT JOIN .... ; use data from other tables to sort the ids list
SELECT `id` FROM `ids`; select everything back
Otherwise you'll have to use IN (?,?,?,.... or sort the rows manually. The best idea is to use simple MySQL-queries, or, try to get the list of IDs already sorted in the way you like.
Have you considered rewriting you original query using a JOIN and WHERE clause to get the IDS you need to avoid the need for a WHERE IN clause? I came here with the same question and after reviewing the possible solutions I realized an INNER JOIN was my solution.
Copied from my answer here How to use PDO prepared statements with IN clause?
using named place holders
$values = array(":val1"=>"value1", ":val2"=>"value2", ":val2"=>"$value3");
$statement = 'SELECT * FROM <table> WHERE `column` in(:'.implode(', :',array_keys($values)).') ORDER BY `column`';
using ??
$values = array("value1", "value2", "$value3");
$statement = 'SELECT * FROM <table> WHERE `column` in('.trim(str_repeat(', ?', count($values)), ', ').') ORDER BY `column`';
An alternative would be to use PHP usort function on the result object, but this is "manual."
See this:
Sort Object in PHP

Categories