Build a dynamic SQL statement via $_POST - php

So this is more about getting your opinion on the best approach for this.
I have what I think is quite an elegant way of building a simple dynamic SQL statement with a straightforward WHERE clause. The WHERE clause can include multiple fields but it is limited as it does not allow for different operators (comparative or logical).
I can build the following with this:
SELECT * from table_name WHERE field_1 = "value_1" AND field_2 = "value_2";
//or I can do
SELECT * from table_name WHERE field_1 = "value_1" OR field_2 = "value_2";
//or I can do
SELECT * from table_name WHERE field_1 <> "value_1" AND field_2 <> "value_2";
I can not build the following:
SELECT * from table_name WHERE field_1 = "value_1" AND field_2 <> "value_2";
//nor can I do
SELECT * from table_name WHERE field_1 = "value_1" AND field_2="value_2" OR field_3 = "value_3
It becomes a real problem when working with numbers and dates when I want to look for records with values between meaning I need to pass the same filed in twice with two separate values.... doesnt it?
SELECT * from table_name WHERE price BETWEEN 10 AND 20;
SELECT * from table_name WHERE date BETWEEN "2016-08-01" AND "2016-08-15";
And not forgetting multiple criteria with "IN" or LIKE statements which this also does not build, i.e.:
SELECT * from table_name WHERE field_1 IN("value_1","value_2, "value_3");
SELECT * from table_name WHERE field_1 LIKE "val%";
Here is what my current code looks like:
// db contains my DB connection
$db = new DB();
$where = 'WHERE';
$criteria = array();
foreach ($_GET as $key => $value) {
$where = $where.' '.$key.'=? AND';
array_push($criteria,$value);
}
if(count($_GET) > 0){
// $sql will look like: SELECT * FROM table_name WHERE field_1 = ? AND field_2 = ?
// $criteria is an array of values to pair with the above prepared statement.
// Will look like: $criteria("value_1", "value_2")
$sql = 'SELECT * FROM mcl_data_gap '.$where;
$results = $db->query($sql,$criteria);
} else {
$sql = 'SELECT * FROM mcl_data_gap';
$results = $db->query($sql);
}
// .... continue on using above SQL statement
In the above code I have used get but my assumption is post would also work.
The only idea I have come up with is to insert more key value pairs that contain the operators required in a coded format that would allow me to then look for these operators and build the statement based on them but I just feel like there is a better way and that is what I am hoping you can help with.
Another option I have just thought of is building the SQL before passing it to the server and just executing that.
Or can I post objects that contain the whole segment of the WHERE statement?

You are using query parameters for the dynamic values (the right side of the equality comparison). This is good.
But you can't use a parameter for the dynamic column names (the left side of a comparison). This is how your code is vulnerable to SQL injection. Prepared statements don't help with that.
The solution is to make sure every column name that comes from your $_GET keys is actually one of the columns in your table. In other words, this is called whitelisting the input.
$mcl_data_gap_columns = ["field_1", "field_2", "field_3"];
You only want to process $_GET parameters that match columns in your list of columns that exist in your table. Anything that isn't in this list should be ignored.
As for predicates that have multiple values, you can access them in PHP by naming the GET parameter with "[]" at the end.
$terms = [];
$parameters = [];
// only look for $_GET keys that match one of the known columns.
// this automatically ignores all other $_GET keys.
foreach ($mc_data_gap_columns as $col) {
// get the single value, or the array of multiple values.
// convert to an array in either case.
if (isset($_GET[$col])) {
$values = (array) $_GET[$col];
$default_op = "=";
} elseif (isset($_GET[$col."[]"])) {
$values = $_GET[$col."[]"];
$default_op = "IN";
} else {
continue;
}
// if your comparison is anything other than equality,
// there should be another request parameter noting that.
if (isset($_GET[$col."_SQLOP"])) {
$op = $_GET[$col."_SQLOP"];
} else {
$op = $default_op;
}
Process only the known operations. If $op is not one of the specific supported operations, ignore it or else throw an error.
switch ($op) {
case "=":
case ">":
case "<":
case ">=":
case "<=":
case "<>":
// all these are simple comparisons of one column to one value
$terms[] = "$col $op ?";
$parameters[] = $values[0];
break;
case "BETWEEN":
// comparisons of one column between two values
if (count($values) != 2) {
error_log("$col BETWEEN: wrong number of arguments: " . count($values));
die("Sorry, there has been an error in your request.");
}
$terms[] = "$col BETWEEN ? AND ?";
$parameters[] = $values[0];
$parameters[] = $values[1];
break;
case "IN":
// comparisons of one column IN a list of any number of values
$placeholders = implode(",", array_fill(1, count($values), "?"));
$terms[] = "$col IN ($placeholders)";
$parameters = array_merge($parameters, $values);
break;
default:
error_log("Unknown operation for $col: $op");
die("Sorry, there has been an error in your request.");
}
}
Then finally after that's done, you'll know that $terms is either an empty array or else the array of search conditions.
if ($terms) {
$sql .= " WHERE " . join(" AND ", $terms);
}
$db->query($sql, $parameters);
I have not tested the above code, but it should illustrate the idea:
Never use $_GET input verbatim in your SQL queries
Always filter input against a fixed list of safe values
Or use switch to test against a fixed set of safe cases
Another option I have just thought of is building the SQL before passing it to the server and just executing that.
No, no, no! This would just be an invitation to get hacked. Never do this!
Your wrong if you think that your HTML page is the only way someone can submit a request to your server. Anyone can form any URL they want, and submit it to your site, even if it contains GET parameters and values you don't expect.

Related

Php search Splitting criteria type

I have a php search form with two fields. One for $code another for '$name'.The user uses one or the other, not both.
The submit sends via $_POST.
In the receiving php file I have:
SELECT * FROM list WHERE code = '$code' OR name = '$name' ORDER BY code"
Everything works fine, however I would like that $code is an exact search while $name is wild.
When I try:
SELECT * FROM list WHERE code = '$code' OR name = '%$name%' ORDER BY code
Only $code works while $name gives nothing. I have tried multiple ways. Changing = to LIKE, putting in parentheses etc. But only one way or the other works.
Is there a way I can do this? Or do I have to take another approach?
Thanks
If you only want to accept one or the other, then only add the one you want to test.
Also, when making wild card searches in MySQL, you use LIKE instead of =. We also don't want to add that condition if the value is empty since it would become LIKE '%%', which would match everything.
You should also use parameterized prepared statements instead of injection data directly into your queries.
I've used PDO in my example since it's the easiest database API to use and you didn't mention which you're using. The same can be done with mysqli with some tweaks.
I'm using $pdo as if it contains the PDO instance (database connection) in the below code:
// This will contain the where condition to use
$condition = '';
// This is where we add the values we're matching against
// (this is specifically so we can use prepared statements)
$params = [];
if (!empty($_POST['code'])) {
// We have a value, let's match with code then
$condition = "code = ?";
$params[] = $_POST['code'];
} else if (!empty($_POST['name'])){
// We have a value, let's match with name then
$condition = "name LIKE ?";
// We need to add the wild cards to the value
$params[] = '%' . $_POST['name'] . '%';
}
// Variable to store the results in, if we get any
$results = [];
if ($condition != '') {
// We have a condition, let's prepare the query
$stmt = $pdo->prepare("SELECT * FROM list WHERE " . $condition);
// Let's execute the prepared statement and send in the value:
$stmt->execute($params);
// Get the results as associative arrays
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
The variable $results will now contain the values based on the conditions, or an empty array if no values were passed.
Notice
I haven't tested this exact code IRL, but the logic should be sound.

Filters not working (Executing different PHP queries based on parameters)

I am trying to implement filters which will help users refine there search for other users. Here is an image of my search parameters just to provide you with a graphical representation of what I will soon convey:
There are three filters:
Gender
Age
Similarity in studies
By default, I want to convey all users on the system. So when a user goes onto users.php, every single user will be displayed, then, when the filters are applied, refine the results accordingly.
Not all three parameters have to be completed to start the search, for example, a user can simply search a female user and it should display all female users on search click.
I have tried to implement different queries for each scenario, but all users are always being displayed. If I specify I want to search for a female and then click search, it will do nothing, still showing me all users.
Also, I am struggling with the similarity in studies parameter. The way this works is that in a table called user_bio I am storing data regarding what the user is studying, the user can choose to not provide this information, so studying can also be empty in my table.
The way I want it to work is to look at what the logged in user is studying, and then find words which match in other peoples bio's. For example, I am currently logged in as Conor, and Conor is studying Computer Science. Ideally, an algorithm will run which searches other users bio from the user_bio table, and return all the users who have computer or science in their bio's. Im pretty sure this concerns the LIKE clause but I have never used it before so I cannot be certain.
Here is my current approach:
// processing filters
$refined_gender = htmlentities (strip_tags(#$_POST['gender']));
$age_from = htmlentities (strip_tags(#$_POST['age_from']));
$age_to = htmlentities (strip_tags(#$_POST['age_to']));
$studying = htmlentities (strip_tags(#$_POST['studying']));
$get_all_users = mysqli_query ($connect, "SELECT * FROM users" );
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
if (isset($_POST['submit'])){
// if gender parameter is used ...
if ($refined_gender){
$gender_statement = mysqli_prepare ($connect, "SELECT * FROM users WHERE gender = ?");
mysqli_stmt_bind_param($gender_statement, "s", $refined_gender);
mysqli_stmt_execute ($gender_statement);
mysqli_stmt_close($gender_statement);
}
// if studying parameter used...
if ($studying) {
// see explanation below...
}
// if gender and age parameter used...
if ($refined_gender && $age_from && $age_to){
$gen_and_age_statement = mysqli_prepare ($connect, "SELECT * FROM users WHERE gender = ? AND age BETWEEN ? AND ?");
mysqli_stmt_bind_param($gen_and_age_statement, "sss", $refined_gender, $age_from, $age_to);
mysqli_stmt_execute ($gen_and_age_statement);
mysqli_stmt_close($gen_and_age_statement);
}
}
Summary, what I need:
The SELECT * FROM users query to be executed by default on users.php. This will show all the users in the system.
For any filter to be applied. Not all filters need to be applied to get a result, a user can search for a female and click search, loading all female users in the system.
I need the query to change based on what filters have been applied. So if a user has searched for a male user, and the other two options are not selected, then query will be "SELECT * FROM users WHERE gender = '$var_here'.
Here iam providing code such that how can you write multiple filter option inside a single query..but here i didn't mention about your 3rd filter option studing,because its about another table and you were not mentioned it clearly such that it's linked to this table using foreign keys or following relational database structure.any way multi filter option is as follows..here i added database connect and escape injection's functions...if u don't need that neglect that part..
function escape($e_string)
{
global $connect;
if(!isset($connect))
{
// DATABASE CONNECTION QUERY
$connect = mysqli_connect("servername", "username", "password", "");
if (!$connect)
die("Connection failed: " . mysqli_connect_error());
}
$e_string = trim(utf8_encode($e_string));
$e_string = mysqli_real_escape_string($connect,$e_string);
return $e_string;
}
// processing filters
$refined_gender = isset($_POST['gender']) ? escape($_POST['gender']) : '';
$age_from = isset($_POST['age_from']) ? escape($_POST['age_from']) : '';
$age_to = isset($_POST['age_to']) ? escape($_POST['age_to']) : '';
$studying = isset($_POST['studying']) ? escape($_POST['studying']) : '';
$query = "SELECT * FROM users WHERE 1=1";
if (isset($_POST['submit'])){
$addstring1 = $addstring2 = $addstring3 = $and1 = $and2 = $and3 = "";
$andcnt =3;
if($refined_gender != '')
$addstring1 = " gender = '$refined_gender'";
if($age_from != '')
$addstring2 = " age >= '$age_from'";
if($age_to != '')
$addstring3 = " age <= '$age_to'";
for($i=1;$i<=$andcnt;$i++)
${"and".$i} = ${"addstring".$i} != '' ? " AND" : "";
$query .= $and1.$addstring1.$and2.$addstring2.$and3.$addstring3;
}
$get_all_users = mysqli_query ($connect, $query);
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
Instead of this code:
htmlentities (strip_tags(#$_POST['gender']));
you should validate it, like so:
$gender = filter_input(INPUT_POST, 'gender', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^[mf]$/i']]);
$ageFrom = filter_input(INPUT_POST, 'age_from', FILTER_VALIDATE_INT, [ 'default' => 1, 'min_range' => 1, 'max_range' => 100]);
$ageTo = filter_input(INPUT_POST, 'age_to', FILTER_VALIDATE_INT, [ 'default' => 1, 'min_range' => 1, 'max_range' => 100]);
$studying = filter_input(INPUT_POST, 'gender', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^(similar|different|same)$/i']]);
This is simpler and more secure.
Each input should be properly validated.
Avoid using #.
Once you have the values, you can concatenate them in to your query, like so:
$types = '';
$values = [];
$query = 'SELECT * FROM users';
$where = [];
// empty tests for both null (no data in input) and false (invalid data)
if (!empty($gender)) {
$where[] = 'gender = ?';
$types .= 's';
$values[] = &$gender;
}
if (!empty($ageFrom)) {
$where[] = 'age >= ?';
$types .= 'i';
$values = &$ageFrom;
}
if (!empty($ageTo)) {
$where[] = 'age <= ?';
$types .= 'i';
$values = &$ageTo;
}
if (!empty($studying)) {
$field = 'user_bio';
// Get the $user_bio value of the current user from the database
// Change the $user_bio into a regular expression collection of words
$regexp = '('.str_replace(' ','|',$user_bio).')';
// Set up the where
switch ($studying) {
case 'same':
$comparison = '= ?';
break;
case 'different':
$comparison = 'NOT REGEXP (?)';
break;
case 'similar':
$comparison = 'REGEXP (?)';
break;
}
$where[] = $field.' '.$comparison;
$types .= 's';
$values[] = &$user_bio;
}
if (count($where) > 0) {
$query .= ' WHERE '.implode(' AND ',$where);
}
// new mysqli ( host,
$mysqli = new mysqli('localhost','root','','stuff');
$stmt = $mysqli->prepare($query);
// This allows you to use a variable number of arguments with the prepared statement
// Note the use of the ampersands on the array assignment, this ensures they are passed by reference
$params = array_merge([$types],$values);
call_user_func_array([$stmt,'bind_param'],$params);
$stmt->execute();
// Bind a variable for each column
$stmt->bind_result($user_name);
while ($stmt->fetch()) {
var_dump($result);
}
(I'm not sure why the answers already provided don't address your question sufficiently.)
I'd approach it like this. First, get rid of that first query execution to pull all users. Instead, use just a single query.
Dynamically prepare the SQL text. Start the statement with the "SELECT ... FROM users". (We'll handle appending an ORDER BY as the last step.
I'd conditionally check each "filter", to see if I need to append a condition to the WHERE clause or not.
At the start of the SQL, we'll include a "WHERE 1=1".
$sql = "SELECT ... FROM users u"
$sql .= " WHERE 1=1";
The "WHERE 1=1" is basically useless. The optimizer is going to throw that away. The reason we add it is just to make our code easier later. We can just append our next filter with " AND condition", and not worry about whether this is the first one, and we need to use WHERE instead of AND.
We'll initialize a string and an array, to hold our bind types string "sssis" whatever it needs to be, and an array of references to the values we want to pass in.
$bind_type = "";
$bind_vals = array();
The processing for each filter is going to be icky... but we can do it. Check if we need to append anything to the SQL. If we do, figure out what needs to be added, including any bind placeholders. And append the type of the bind parameter ("i", "s", whatever) to the $bind_type string, and push (the reference to) a value into our $bind_vals array.
if ( $refined_gender ) {
// figure out what that SQL text needs to look like
// append the string to the SQL text
$sql .= " AND u.gender = ?";
// append type to string, and push a reference to the value into array
$bind_types .= "s";
$bind_val[] = &$refined_gender;
}
Our code in there is going to be more complicated than that. That's just handling an equality comparison. We're just keeping things simple now, to illustrate the pattern.
We repeat the same kind of thing for each filter we might need to add. Check if it's needed, figure out what we need to append to the SQL text, append to the bind_types string and push (a reference to) the value into the bind_vals array.
For working this out, I'd start with working on just one condition, and get that working, to get the kinks worked out. When we add more filters, and things go awry, I know where to look for the problem. (I know what was working before.)
When I'm done with the WHERE clause, I append any ORDER BY and LIMIT that I need. This could be conditional, but in the end, we're going to wind up doing something like this:
$sql .= " ORDER BY u.id DESC LIMIT 50";
When I'm done with all that, I've got a string containing SQL text that looks something like this:
SELECT ...
FROM users u
WHERE 1=1
AND u.gender = ?
AND u.age_from >= ?
AND u.age_to <= ?
ORDER BY u.id DESC
LIMIT 50
(in this example, it contains three bind placeholders. If we've done it right,
we'll have a $bind_types string containing three characters, e.g. "sii"
And we'll have a $bind_vals array that contains references to three values.
Now, we can call mysqli_stmt_prepare. If there's not an error in our SQL, we should get back a statement handle.
$stmt = mysqli_prepare($conn,$sql);
(Check the return from the prepare.)
Now we just need to bind our parameters. And this is where mysqli makes things a little hairy. If we were using PDO (or Perl DBI), calling the "bind parameter/bind value" would be easy. Those would let us pass an array of the bind values. But not mysqli. He won't let us call mysqli_stmt_bind_param with an array as an argument.
We need to run a function call like this:
mysqli_stmt_bind_param($stmt, $bind_types, &$refined_gender, &$age_from, ... );
And our problem is that we have a variable number of arguments.
There is a workaround.
We can use the call_user_func_array function.
Because the code is using procedural style and not object oriented style, the handle to the prepared statement is the first argument, the second argument is the bind types string, followed by the bind values. The bind values are already in an array. We just need to get all of those into one hugh jass array.
The array_merge function seems to be custom designed for doing this.
// array_merge(array($stmt), array($bind_types), $bind_vals)
That will return us a single array. Which is exactly what we need for calling the call_user_func_array function. We aren't going to need that array anywhere else (unless we're debugging, and we want to print it out).
We only need to call mysqli_stmt_bind_param if we have at least one bind placeholder in our statement. So we can shortcut around this if our $bind_types string is empty. (And we know $bind_types won't be "0" because our code never appended a "0" to it.)
if ($bind_types) {
call_user_func_array('mysqli_stmt_bind_param', array_merge(array($stmt), array($bind_types), $bind_vals) );
}
The first argument (to call_user_func_array) is the name of the function we want to execute, and the second argument is the hugh jass array that we want converted into a list.
And the whole point of doing that is making it dynamic, we can pass in one, two, three, bind values.
At this point, we're ready to execute the statement, and fetch the results.
Again, important to point out: mysqli_stmt_bind_param expects the bind values to be passed by reference, not by value. And that's why we pushed references to the values into the bind_vals array.
I'm not sure what question you asked.
But definitely ditch that first call to mysqli_query. That's going to return all rows in the users table.
With one or two conditions, the approach of static SQL and static bind types, and listing out the bind values is workable.
But when we get three, four, five possible filters, and all the possible combinations, that's going to be unweildy.
So we go with a more dynamic approach, dynamically creating the query, and pushing our bind values on an array as go.
This Html page:
<form method="POST" action="">
<input type="radio" name="rbo_gender" value="male">Male
<input type="radio" name="rbo_gender" value="female">Female
Age From<select name="agefrom">
<?php
for($i=10;$i<50;$i++):
?>
<option value="<?php echo $i?>"><?php echo $i?></option>
<?php
endfor;
?>
</select>
Age To<select name="ageto">
<?php
for($i=10;$i<50;$i++):
?>
<option value="<?php echo $i?>"><?php echo $i?></option>
<?php
endfor;
?>
</select>
Studying:
<input type="radio" name="rbo_type" value="similar">Similar
<input type="radio" name="rbo_type" value="exact">Exactly same
<input type="radio" name="rbo_type" value="different">Different
<input type="submit" name="btnsearch" value="Search">
</form>
This is php part:
if($_POST["btnsearch"])
{
if(!empty($_POST["rbo_gender"]))
{
$gender = $_POST["rbo_gender"];
$cond .= " and gender = '".$gender."'";
}
if(!empty($_POST["agefrom"]))
{
$agefrom = $_POST["agefrom"];
$cond .= " and age >= '".$agefrom."'";
}
if(!empty($_POST["ageto"]))
{
$ageto = $_POST["ageto"];
$cond .= " and age <= '".$ageto."'";
}
if(!empty($_POST["rbo_type"]))
{
$user_type = $_POST["rbo_type"];
switch($_POST["rbo_type"])
{
case "similar": $cond .= " and user_bio like '%".$ageto."%'";
break;
case "exact": $cond .= " and user_bio = '".$ageto."'";
break;
case "different":$cond .= " and user_bio ! like '%".$ageto."%'";
break;
}
}
$query = "select * from users where 1 ".$cond;
}
Please update query as per mysqli() & use bind param. Also instead of use # try to use filter_input you can use REGEXP instead of like also. I have created the variable to use bind_param purpose.

PHP - building query for table filters

I need help with building a query, I have a form ( multiple dropdowns) and need to display a data table according to the user selection.
filters.php (on server side - ajax )
$criteria[0] = $_POST['Size'];
$criteria[1] = $_POST['Name'];
$criteria[2] = $_POST['Color'];
$criteria[3] = $_POST['Sku'];
$criteria[4] = $_POST['Features'];
$statement = $dbh->prepare("select * from table_name where Sku = :sku AND Size = :size AND Color = :color AND Features LIKE :features ");
$statement->execute(array(':name' => $criteria[1], ':Size'=> $criteria[0], ':color'=>$criteria[2], ':sku'=>$criteria[3], ':features'=> '%'.$features[4].'%'));
$statement->setFetchMode(PDO::FETCH_ASSOC);
$subrows = $statement->fetchAll();
// ect..
When all the criteria are selected ( all dropdowns have a value ) the call will return data , but what if the user only selects one, two or three dropdowns? It will return an empty table which is logic if using the query above.
I was thinking maybe I should try first to check if the variables are set, then perform for each scenario a query ? Im kind of lost how to implement it.
The idea is to have a small div with dropdowns next to the data table which allows filtering.
thanks
If statements dependent on the $_POST key may be the best way - and add the corresponding part to the query string:
i.e. :
$queryString = "select * from table_name where ";
if($_POST['SKU']){
$queryString .= "Sku = :sku";
}
Furthermore, to handle the "AND" parts :
$and = (count($_POST) > 1 ? 1 : 0);
and inside each if statement, the first line should be:
$queryString .= ($and ? "AND " : "");
Furthermore, I would suggest using an ORM, (Idiorm being my personal favorite, which I use in this example: )
$queryBuilder = ORM::for_table("table");
if($_POST['Sku'])
$queryBuilder = $queryBuilder->where("Sku", $_POST['Sku']);
and so on and so forth.
Check the desired variables to see if they contain data, if so you have to build the query. You can add to the array at the same time! For example:
$myarray = array();
if(!empty($criteria[0])){
$myarray["Size"] = $criteria[0];
}

prevent sql injection on query with variable (and large) number of columns

I have a sql query that is generated using php. It returns the surrogate key of any record that has fields matching the search term as well as any record that has related records in other tables matching the search term.
I join the tables into one then use a separate function to retrieve a list of the columns contained in the tables (I want to allow additions to tables without re-writing php code to lower ongoing maintenance).
Then use this code
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$qry_string.="UPPER(";
$qry_string.=$cur_col;
$qry_string.=") like '%";
$qry_string.=strtoupper($term_searching);
$qry_string.="%' or ";
}
}
To generate the rest of the query string
select tbl_sub_model.sub_model_sk from tbl_sub_model inner join [about 10 other tables]
where [much code removed] or UPPER(tbl_model.image_id) like '%HONDA%' or
UPPER(tbl_model.image_id) like '%ACCORD%' or UPPER(tbl_badge.sub_model_sk) like '%HONDA%'
or UPPER(tbl_badge.sub_model_sk) like '%ACCORD%' or UPPER(tbl_badge.badge) like '%HONDA%'
or UPPER(tbl_badge.badge) like '%ACCORD%' group by tbl_sub_model.sub_model_sk
It does what I want it to do however it is vulnerable to sql injection. I have been replacing my mysql_* code with pdo to prevent that but how I'm going to secure this one is beyond me.
So my question is, how do I search all these tables in a secure fashion?
Here is a solution that asks the database to uppercase the search terms and also to adorn them with '%' wildcards:
$parameters = array();
$conditions = array();
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$conditions[] = "UPPER( $cur_col ) LIKE CONCAT('%', UPPER(?), '%')";
$parameters[] = $term_searching;
}
}
$STH = $DBH->prepare('SELECT fields FROM tbl WHERE ' . implode(' OR ', $conditions));
$STH->execute($parameters);
Notes:
We let MySQL call UPPER() on the user's search term, rather than having PHP call strtoupper()
That should limit possible hilarious/confounding mismatched character set issues. All your normalization happens in one place, and as close as possible to the moment of use.
CONCAT() is MySQL-specific
However, as you tagged the question [mysql], that's probably not an issue.
This query, like your original query, will defy indexing.
Try something like this using an array to hold parameters. Notice % is added before and after term as LIKE %?% does not work in query string.PHP Manual
//Create array to hold $term_searching
$data = array();
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$item = "%".strtoupper($term_searching)."%";//LIKE %?% does not work
array_push($data,$item)
$qry_string.="UPPER(";
$qry_string.=$cur_col;
$qry_string.=") LIKE ? OR";
}
}
$qry_string = substr($qry_string, 0, -3);//Added to remove last OR
$STH = $DBH->prepare("SELECT fields FROM table WHERE ". $qry_string);//prepare added
$STH->execute($data);
EDIT
$qry_string = substr($qry_string, 0, -3) added to remove last occurrence of OR and prepare added to $STH = $DBH->prepare("SElECT fields FROM table WHERE". $qry_string)

MySQL where clause equals anything (SELECT * WHERE col = ANY_VALUE)

I'd like to create a query in MySQL that has an optional value. When the value is specified the query is filtered by that value, when the value is not all rows are returned. Here's the idea:
public function doQuery($item = 'ANY_VALUE') {
$query = "SELECT * FROM table WHERE item = ?";
db->fetchAll($query,array($item))
...
}
doQuery(); // Returns everything
doQuery($item='item1'); // Returns only rows where item = 'item1'
Is there an easy way to do this without creating two query strings depending on the value of $item?
As far as I know, no such "any" placeholder exists.
If you can use LIKE, you could do
SELECT * FROM table WHERE item LIKE '%'
if you can append a condition, you could nullify the item clause like this:
SELECT * FROM table WHERE item = ? OR 1=1
(won't work in your example though, because you are passing "item" as a parameter)
That's all the options I can see - it's probably easiest to work with two queries, removing the WHERE clause altogether in the second one.
This would probably work, but I*m not sure whether it's a good idea from a database point of view.
public function doQuery($item = 'ANY_VALUE') {
$query = "SELECT * FROM table WHERE item = ? OR 1 = ?";
db->fetchAll($query,array($item, ($item == 'ANY_VALUE' ? 1 : 0))
...
}
Better way to do this is first generate sql query from the parameter you need to bother on, and then execute.
function doQuery($params) {
$query = 'SELECT * FROM mytable ';
if (is_array($params) // or whatever your condition ) {
$query .= 'WHERE item = ' . $params[0];
}
$query .= ' ;';
// execute generated query
execute($query);
}
You cannot get distinct results without giving distinct query strings.
Using $q = "... WHERE item = '$item'" you DO create distinct query strings depending on the value of $item, so it is not that different from using
$q = "..." . ($item=='ANY_VALUE' ? something : s_th_else);.
That said I see two or three options:
use function doQuery($item = "%") { $query = "SELECT ... WHERE item LIKE '$item'"; ...}
But then callers to that function must know that they must escape a '%' or '_' character properly if they want to search for an item having this character literally (e.g. for item = "5% alcoholic solution", giving this as argument would also find "50-50 sunflower and olive oil non alcoholic solution".
use function doQuery($item = NULL) { $query = "SELECT ..."; if ($item !== NULL) $query .= " WHERE item = '$item' "; ...} (where I use NULL to allow any other string or numerical value as a valid "non-empty" argument; in case you also want to allow to search for NULL (without quotes) you must choose another "impossible" default value, e.g., [], and you must anyway use a distinct query without the single quotes which however are very important in the general case), or even:
use function doQuery($item = NULL) { if($item === NULL) $query = "SELECT ..."; else $query = "SELECT ... WHERE item = '$item' "; ...}, which is more to type but probably faster since it will avoid an additional string manipulation (concatenation of the first and second part).
I think the 2nd & 3rd options are better than the first one. You should explain why you want to avoid these better solutions.
PS: always take care of not forgetting the quotes in the SQL, and even to properly escape any special characters (quotes, ...) in arguments which can depend on user input, as to avoid SQL injections. You may be keen on finding shortest possible solutions (as I am), but neglecting such aspects is a no-no: it's not a valid solution, so it's not the shortest solution!

Categories