I've got a form with several different fields used for a search function in my application. There can be any combination for the search parameters, all could be filled or just 1 (or any other).
As of now, the relevant part of the PHP code looks like this (which actually works - but I fear and thinks is a terribly bad design/implementation):
$whereClause = "";
if (!empty($_POST['searchAlias'])) {
$searchAlias = "%{$_POST['searchAlias']}%";
$whereClause .= " AND (users.alias LIKE ? OR users.alias LIKE ?)";
} else {
$searchAlias = "";
$whereClause .= " AND (users.alias LIKE ? OR users.alias LIKE ?)";
}
if (!empty($_POST['searchLink'])) {
$searchLink = "%{$_POST['searchLink']}%";
$whereClause .= " AND (posts.homepage LIKE ? OR posts.homepage LIKE ?)";
} else {
$searchLink = "";
$whereClause .= " AND (posts.homepage LIKE ? OR ? = '')";
}
if (!empty($_POST['searchComment'])) {
$searchComment = "%{$_POST['searchComment']}%";
$whereClause .= " AND (posts.comment LIKE ? OR posts.comment LIKE ?)";
} else {
$searchComment = "";
$whereClause .= " AND (posts.comment LIKE ? OR ? = '')";
}
if (!empty($_POST['searchCategory'])) {
$searchCategory = $_POST['searchCategory'];
$whereClause .= " AND (posts.category LIKE ? OR posts.category LIKE ?)";
} else {
$searchCategory = "";
$whereClause .= " AND (posts.category LIKE ? OR ? = '')";
}
$stmt = $connection->prepare("SELECT users.alias, posts.postId,
categories.category, posts.homepage, posts.comment, posts.timestamp,
posts.upvotes FROM ((posts INNER JOIN categories ON posts.category =
categories.id) INNER JOIN users ON posts.userId = users.userId)
WHERE 1=1 $whereClause ORDER BY postId DESC LIMIT 300");
$stmt->bind_param("ssssssss", $searchAlias,$searchAlias,
$searchLink,$searchLink, $searchComment,$searchComment,
$searchCategory,$searchCategory);
I should add that I also have 2 date input fields (from and to date) which can be included in the search as well, they are not added in the current php code since I haven't fixed them yet.
As you can see I make sure that the parameters set from the form data are appended to the whereClause - always with two ? to make sure the bind_param types and variables can remain the same all the time.
So, is this an "acceptable" solution or is there any way to make this a lot cleaner and simpler? I've tried searching for different solutions but im either not good enough searching, or not smart enough understanding the solutions.
Any help or idea is appreciated!
Kind regards,
Eken
Using PDO with named parameters would make this task a bit easier to maintain if/when additional search parameters are added. It would also accommodate search parameters where the value is compared to multiple columns, or require type casting of the search value. You may want to consider adding '%' before and after the values for LIKE conditions. This could be done by adding the $_POST values to an array and binding the array values instead of the $_POST values.
EDIT
I've modified my first posting to search for a matching date, and to allow adding wildcards to like comparisons. The code could also be easily modified to allow for two value checks like between dates.
$postVals = array();
foreach($_POST as $varname => $varval) {
$postVals[$varname] = $varval;
}
$wherePieces = array();
$tests = array(
"searchAlias" => array("where" => "user.alias LIKE :searchAlias","addWildCard" => false),
"searchLink" => array("where" => "posts.homepage LIKE :searchLink","addWildCard" => true),
"searchComment" => array("where" => "posts.comment LIKE :searchComment","addWildCard" => true),
"searchCategory" => array("where" => "posts.category LIKE :searchCategory","addWildCard" => false),
"searchTimestamp" => array("where" => "CAST(posts.timestamp AS DATE) = CAST(:searchTimestamp AS DATE)","addWildCard" => false)
);
foreach($tests as $postname => $wherePart) {
if(isset($_POST[$postname]) && $_POST[$postname] != '') {
$wherePieces[] = $wherePart['where'];
}
}
$whereClause = "WHERE " . implode(" AND ",$wherePieces);
$qstr = "SELECT
users.alias,
posts.postId,
categories.category,
posts.homepage,
posts.comment,
posts.timestamp,
posts.upvotes
FROM posts
INNER JOIN categories
ON posts.category = categories.id
INNER JOIN users
ON posts.userId = users.userId)
$whereClause
ORDER BY postId DESC
LIMIT 300";
$stmt = $connection->prepare($qstr);
foreach($tests as $postname => $wherePart) {
if(isset($_POST[$postname]) && $_POST[$postname] != '') {
if($wherePart["addWildCard"]) {
$postVals[$postname] = '%'.$postVals[$postname].'%';
}
$stmt->bindParam(':'.$postname,$postVals[$postname]);
}
}
$stmt->execute();
NOTE: Not tested, could have typos.
Related
Im a new and just learning php. I have a data table with search boxes with this code.
$condition = '';
if(isset($_REQUEST['username']) and $_REQUEST['username']!="") {
$condition .= ' AND username LIKE "%'.$_REQUEST['username'].'%" ';
}
if(isset($_REQUEST['useremail']) and $_REQUEST['useremail']!=""){
$condition .= ' AND useremail LIKE "%'.$_REQUEST['useremail'].'%" ';
}
What I need is to search with both username AND useremail. I have attempted everything I know and spent a few hours searching for a solution but with no success.
You could write a complicated set of IF's with equals and not equals tests all over the place, but as the list of test gets bigger the IF's get almost impossible to maintain or understand. So it might be simpler to just build and array of things to AND in the query
$condition = '';
$and = []; #init the array
if(isset($_REQUEST['username']) and $_REQUEST['username']!="") {
$and[] = ['name' => 'username', 'value' => $_REQUEST['username'] ];
}
if(isset($_REQUEST['useremail']) and $_REQUEST['useremail']!=""){
$and[] = [''name' => 'useremail', 'value' => $_REQUEST['useremail'] ];
}
#now build the condition string
foreach ($and as $i => $andMe) {
if ( $i != 0 ){
// AND required here
$condition .= ' AND ';
}
$condition .= $andMe['name'] . ' = ' . $andMe['value'];
}
Also I have replaced the LIKE with an = as it seems more appropriate, I assume you dont ask people to enter something a bit like there user name and email, but in fact ask for the actual username or email
Of course that would still be susceptible to SQL Injection Attack So a better solution would be
#now build the condition string
foreach ($and as $i => $andMe) {
if ( $i != 0 ){
// AND required here
$condition .= ' AND ';
}
$condition .= $andMe['name'] . ' = ?';
}
And then prepare the query and use the value part to bind to the parameters.
Issue is you have leading AND in your query.
push condition to array then join conditions.
like that
$condition_array = [];
if(isset($_REQUEST['username']) and $_REQUEST['username']!="") {
$condition_array[] = 'username LIKE "%'.$_REQUEST['username'].'%" ';
}
if(isset($_REQUEST['useremail']) and $_REQUEST['useremail']!=""){
$condition_array[] = 'useremail LIKE "%'.$_REQUEST['useremail'].'%" ';
}
$condition = implode(" AND ",$condition_array);
You can create an array of all the keys that are to be searched. Then, create a new array and collect all conditions. Implode them in the end with AND as the glue. This way, query is made correctly without needing to add 100 different if conditions.
Use PDO objects to avoid SQL injection attacks. In the below snippet, if you ever need to add 1 more column for search, just add it in the below $keys array and rest works as usual without needing any further refactoring.
Snippet:
<?php
$keys = ['username', 'useremail'];
$conditions = [];
$placeholders = [];
foreach($keys as $key){
if(!empty($_REQUEST[ $key ])){
$conditions = " $key LIKE ?";
$placeholders[] = '%' . $_REQUEST[ $key ] . '%';
}
}
// you need to create $mysqli object here
if(count($conditions) === 0){
$stmt = $mysqli->prepare('select * from table');
$stmt->execute();
// rest of your code
}else{
$stmt = $mysqli->prepare('select * from table where '. implode(" AND ", $conditions));
$stmt->bind_param(str_repeat('s', count($placeholders)), ...$placeholders);
$stmt->execute();
// rest of your code
}
I have the following array
$exclude = array(1 => array(1,2,3,4), 2 => array(1,2,3,4));
I need to exclude the values inside the inner array when the key validates. I'm not sure if I should be using CASE or just AND and OR.
Here is the start to what I attempted, but I am missing something, as this doesn't work as intended.. I am appending the query to the end of a WHERE clause, so that is the only part I have access to.
foreach($exclude as $blogID => $post_ids) {
$ids = implode(',', $post_ids);
if($blogID == 1) {
$where .= " AND table.BLOG_ID = {$blogID} AND table.ID NOT IN ($ids)";
} else {
$where .= " OR table.BLOG_ID = {$blogID} AND table.ID NOT IN ($ids)";
}
}
I'm not sure without having the rest of the query to experiment with, but it may help to add some parentheses around all the clauses to make sure all the ANDs and ORs interact as intended. Something like this:
if ($exclude) {
$where .= ' AND ('; // Enclose all the ORs in one set of parentheses
$or = '';
foreach($exclude as $blogID => $post_ids) {
$ids = implode(',', $post_ids);
// Each individual OR part is enclosed in parentheses
$where .= "$or(table.BLOG_ID = {$blogID} AND table.ID NOT IN ($ids))";
$or = ' OR ';
}
$where .= ")";
}
I am working on a site that allows users to list boats and yachts for sale. There is a mysql database that has a table "yachts" and among other fields ther are "make" and "model".
When people come to the site to look for boats for sale there is a search form, one of the options is to enter the make and/or model into a text field. The relevant where clause on the results page is the following
WHERE ( make LIKE '%$yacht_make%' OR model LIKE '%$yacht_make%')
This is working if someone enters either the make or model but not if they enter both.
For example, if someone enters "Jeanneau", the make, it finds the boat with that make, or if they enter "Sun Odyssey", the model, it finds the boat of that model, but if they enter "Jeanneau Sun Odyssey" it comes up empty.
Is there is a way to write a query where all three ways of entering the above search criteria would find the boat?
Here is the site http://yachtsoffered.com/
Thanks,
Rob Fenwick
Edit:
The query is built with a php script here is the script
if(!empty($yacht_id)) {
$where = " WHERE yacht_id = $yacht_id ";
} else {
$where = " WHERE ( make LIKE '%$yacht_make%' OR model LIKE '%$yacht_make%') ";
if(!empty($year_from) && !empty($year_to)){
$where .= "AND ( year BETWEEN $year_from AND $year_to ) ";
}
if(!empty($length_from) && !empty($length_to)){
$where .= "AND ( length_ft BETWEEN $length_from AND $length_to ) ";
}
if(!empty($price_from) && !empty($price_to)){
$where .= "AND ( price BETWEEN $price_from AND $price_to ) ";
}
if ($sail_power != 2){
$where .= "AND ( sail_power = $sail_power ) ";
}
if (count($material_arr) > 0){
$material = 'AND (';
foreach ($material_arr as $value) {
$material .= ' material LIKE \'%' . $value . '%\' OR';
}
$material = substr_replace ( $material , ') ' , -2 );
$where .= $material;
}
if (count($engine_arr) > 0){
$engine = 'AND (';
foreach ($engine_arr as $value) {
$engine .= ' engine LIKE \'%' . $value . '%\' OR';
}
$engine = substr_replace ( $engine , ') ' , -2 );
$where .= $engine;
}
if (count($type_arr) > 0){
$type = 'AND (';
foreach ($type_arr as $value) {
$type .= ' type LIKE \'' . $value . '\' OR';
}
$type = substr_replace ( $type , ') ' , -2 );
$where .= $type;
}
if (count($region_arr) > 0){
$region = 'AND (';
foreach ($region_arr as $value) {
$region .= ' region LIKE \'' . $value . '\' OR';
}
$region = substr_replace ( $region , ') ' , -2 );
$where .= $region;
}
$where .= 'AND ( active = 1 ) ORDER BY yacht_id DESC';
}
$sql = "SELECT * FROM $tbl_name $where LIMIT $start, $limit";
$result = mysql_query($sql);
There are many ways to do it, the easiest one in my opinion is:
$search = preg_replace('/\s+/','|', $yacht_make);
$sql = "select * from yacht where concat_ws(' ',make,model) rlike '$search'";
This replaces all whitespace with |, that is used as OR in regexp-powered-like query on concatenation of all searchable fields. The speed of it may be questionable in heavy trafic sites but is quite compact and easy to add more fields.
Your problem is that in your query you are searching for the exact substring "Jeanneau Sun Odyssey", which is neither a make nor a model.
The easiest solution is to use to separate input boxes for make and model. But if you really need to use a single input box, your best bet would be to split on spaces and add clauses for each separate word, so your query will end up looking something like
WHERE make like '%sun%' OR model like '%sun%'
OR make like '%odyssey%' OR model like '%odyssey%'
OR make like '%Jeanneau%' OR model like '%Jeanneau%'
Thanks everyone I ended up using dev-null-dweller's solution above
I revised my code as following,
to get the value and escape here is the code,
if (isset($_GET['yacht_make'])) {
$yacht_make = cleanString($_GET['yacht_make']);
$yacht_make = preg_replace('/\s+/','|', $yacht_make);
} else {
$yacht_make = NULL;
}
And I revised the first few lines of the php query building code I posted above to read,
if(!empty($yacht_id)) {
$where = " WHERE yacht_id = $yacht_id ";
} else {
$where = " WHERE ( yacht_id LIKE '%' )";
if(!empty($yacht_make)){
$where .= "AND ( CONCAT_WS(' ',make,model) RLIKE '$yacht_make') ";
}
It is working nicely, although it brings up more results than I would like for "Jeanneau Sun Odyssey" as it brings up "Bayliner Sunbridge", I assume because it is matching "Sun".
But it is a big improvement from what I had.
Thanks All
Rob Fenwick
You could also use:
WHERE make LIKE '%$yacht_make%'
OR model LIKE '%$yacht_make%'
OR '$yacht_make' LIKE CONCAT('%', make, '%', model, '%')
OR '$yacht_make' LIKE CONCAT('%', model, '%', make, '%')
It will not be very efficient and still not catch all possibilities, e.g. if the user provide the make and a part of the model name, like 'Jeanneau Odyssey' or in wrong order: Sun Jeanneau Odyssey.
After checking out your site it appears you are only taking input from one text field and searching for that string in each field.
So, you can either use full text searches (linkie) or you could split your string by spaces and generate a WHERE cause on the fly. Here is a rough example:
$searchterms = explode (' ', $input);
$sql .= "... WHERE"; /* obviously need the rest of your query */
foreach ($searchterms as $term) {
if ((substr ($string, -5) != 'WHERE') &&
(substr ($string, -3) == ' ||') { $sql .= " ||"; }
$sql .= " make LIKE '%$term%' || model LIKE '%$term%'";
}
I'm writing a query that uses input from a search form where Brand, Type and Price are optional input fields:
SELECT * FROM `database` WHERE `brand` LIKE "%' . $brand . '%" AND `type` LIKE "%' . $type. '%" AND `price` LIKE "%' . $price . '%"
I am wondering if there is a way to say 'all' if nothing is entered into one of the fields. For example if they do not enter a value in the price field is there a way to tell SQL to just say ignore that section, eg:
AND `price` LIKE "*";
So the reuslts are still filtered by Brand and Type but can have any Price.
Any advice on this is appreciated! Thanks
As Ariel mentioned, it would be better to have PHP do the filtering as you build the query. Here's a code sample for doing it that way:
<?php
$sql = 'SELECT * FROM `database`';
$where = array();
if ($brand !== '') $where[] = '`brand` LIKE "%'.$brand.'%"';
if ($type !== '') $where[] = '`type` LIKE "%'.$type.'%"';
if ($price !== '') $where[] = '`price` LIKE "%'.$price.'%"';
if (count($where) > 0) {
$sql .= ' WHERE '.implode(' AND ', $where);
} else {
// Error out; must specify at least one!
}
// Run $sql
NOTE: Please, please, please make sure that the $brand, $type, and $price variable contents are sanitized before you use them this way or you make yourself vulnerable to SQL injection attacks (ideally you should be using the PHP PDO database connector with prepared statements to sanitize the input).
Normally you do that in the front end language, not SQL.
But price LIKE '%' does, in fact, mean all (except for NULLs). So you are probably fine.
If you have your form fields organized, you can do something like:
<?php
$fields = array(
// Form // SQL
'brand' => 'brand',
'type' => 'type',
'price' => 'price',
);
$sql = 'SELECT * FROM `database`';
$comb = ' WHERE ';
foreach($fields as $form => $sqlfield)
{
if (!isset($_POST[$form]))
continue;
if (empty($_POST[$form]))
continue;
// You can complicate your $fields structure and e.g. use an array
// with both sql field name and "acceptable regexp" to check input
// ...
// This uses the obsolete form for mysql_*
$sql .= $comb . $sqlfield . ' LIKE "%'
. mysql_real_escape_string($_POST[$form])
. '"';
/* To use PDO, you would do something like
$sql .= $comb . $sqlfield . 'LIKE ?';
$par[] = $_POST[$form];
*/
$comb = ' AND ';
}
// Other SQL to go here
$sql .= " ORDER BY brand;";
/* In PDO, after preparing query, you would bind parameters
- $par[0] is value for parameter 1 and so on.
foreach($par as $n => $value)
bindParam($n+1, '%'.$value.'%');
*/
Based on user selections, I need to filter results in my query, but I'm stuck on how to correctly implement dynamic OR statements after my AND. I'm currently using Active Record in CodeIgniter, but this may have to change.
I essentially need to create the following snippet: "WHERE city.id = 9 AND (eventType = 8 or eventType = 9)"
if there are no OR statements, then I don't need the AND
there could be 1-n OR statements
My code currently is as follows:
$this->db->where('city.id =', $cityID);
if ($eventTypes != NULL){
foreach ($eventTypes as $item){
$eventTypeID = intval($item);
$this->db->or_where('eventtype.id =', $eventTypeID);
}
}
This produces: WHERE city.id = 13 OR eventtype.id = 6 OR eventtype.id = 8 ... so I need the AND (
Codeigniter's "ActiveRecord" (airquotes...) Class is fairly limited and doesn't do well with more advanced AND/OR scoping, and points you back to raw sql for "more advanced" queries.
I would formulate your own WHERE string to build the specific query you are wanting and then just use
$this->db->where($sql)
example...
$where = "city.id = $cityID ";
if ($eventTypes != NULL)
{
$where .= "AND ( ";
foreach ($eventTypes as $i => $type)
{
$eventTypeID = intval($type);
// if it's not the first element
// (assumes $eventTypes is non-associative)
if ($i !== 0)
{
$where .= "OR ";
}
$where .= "eventtype.id = $eventTypeID ";
}
$where .= " ) ";
}
$this->db->where($where);