PHP: Search mysql database using multiple select dropdown lists? - php

I'm trying to search MYSQL database using multiple Dropdown lists on my page.
However, there is a small twist in this search function.
Basically, I need to make sure All the criteria (all the multiple select dropdonw values) match the items and if they do match then show the result!
At the moment, my code shows the results even if one of the dropdown values match the items which is not what i am trying to do.
This is my code:
$searchList = "";
$clause = " WHERE ";//Initial clause
$sql="SELECT *
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id";//Query stub
if(isset($_POST['keyword']) && !empty($_POST['keyword'])){
foreach($_POST['keyword'] as $c){
if(!empty($c)){
$currentproduct = $_POST['product'];
$cat = $_POST['cat'];
##NOPE##$sql .= $clause."`".$c."` LIKE '%{$c}%'";
$sql .= $clause . " (ATTRIBUTES.attr LIKE BINARY '$c') AND ATTRIBUTES.sub_cat_name='$currentproduct'";
$clause = " OR ";//Change to OR after 1st WHERE
}
}
$sql .= " GROUP BY product_details.id";
//print "SQL Query: $sql<br />"; //<-- Debug SQl syntax.
// Run query outside of foreach loop so it only runs one time.
$query = mysqli_query($db_conx, $sql);
I even tried to remove the isset and did if(!empty($_POST['keyword'])){ but i still get results even if one of the dropdown lists values match the items credentials.
I'm not sure what I am doing wrong here as I thought using if(!empty($_POST['keyword'])){ should solve this issue but it hasn't.
Could someone please advise on this issue?
any help would be appreciated.
EDIT: I changed the CODE to the following and it doesn't display anything:
$clause = " WHERE ";//Initial clause
$sql="SELECT *
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id";//Query stub
$currentproduct = $_POST['product'];
$cat = $_POST['cat'];
if(!empty($_POST['keyword'])){
foreach($_POST['keyword'] as $c){
if(!empty($c)){
##NOPE##$sql .= $clause."`".$c."` LIKE '%{$c}%'";
$sql .= $clause . " (ATTRIBUTES.attr LIKE BINARY '$c') AND ATTRIBUTES.sub_cat_name='$currentproduct'";
$clause = " AND ";//Change to OR after 1st WHERE
}
}
$sql .= " GROUP BY product_details.id";
//print "SQL Query: $sql<br />"; //<-- Debug SQl syntax.
// Run query outside of foreach loop so it only runs one time.
$query = mysqli_query($db_conx, $sql);
//var_dump($query); //<-- Debug query results.
// Check that the query ran fine.
if (!$query) {
print "ERROR: " . mysqli_error($db_conx);
}

$clause = " OR ";//Change to OR after 1st WHERE
The above OR operator will cause your where criteria to select a record even if 1 keyword matches the attr field. Change it to " AND " to expect all keywords to apply.
Furthermore, ... AND ATTRIBUTES.sub_cat_name='$currentproduct'" criterion seems to apply to all keywords, so this criterion should be added once, not at every iteration of the loop. $currentproduct = $_POST['product']; row should also be moved in fron of the loop.
EDIT: to reflect to changing the opreator to AND and not having any rows returned.
...ATTRIBUTES.attr LIKE BINARY '$c'...
If there are no wildcards in $c, then the above criterion will require the word to match the attr field as if = operator had been used, which is unlikely to happen. Wildcards must be included in the search: '%$c%'
Plus some protection from sql injection would also be nice.
EDIT2:
If each attribue is stored in its own record, then it complicates things a little bit, since the where criteria is evaluated against a single record, not a collection of them.
I'll give you a sample select command, but you will have to incorporate it into your php code.
select product_details.* FROM product_details INNER JOIN
(select product_details.id, count(ATTRIBUTES.id) as total
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id
WHERE ATTRIBUTES.attr in (...)
GROUP BY product_details.id
HAVING total=...) as t
on t.id=product_details.id
The subquery counts how many attributes were matched for a product and eliminates those, where the count does not equal to the number of parameters submitted via the form. The outer query gets the product details for those, where the count matched.
For the ... in the in() clause you need to provide a comma separated, ' enclosed list of the keywords, like: "'computer', 'apple'". Use implode() function in php and sztring concatenation to get the results.
For the ... in the having clause substitute the number of keywords in the $_POST['keyword'] array (you should check in the code if it's an array or just a single value, though).
Still, you should consider the impact of sql injection on your code.

Related

PHP search many inputs [duplicate]

This question already has answers here:
Search Form with One or More (Multiple) Parameters
(2 answers)
Closed 4 years ago.
I have search field in which user can specify a lot of values to search like price, surface, year, garden, balcony etc.
In my search there is not even one field required every one is optional so user can provide 0 inputs filled or all.
Basically all this info's are saved in my database but I don't really know how to structure my code.
At the moment I have PHP file which I call from front and in this file I'm checking which field was filled and I'm executing method from class which do select to db and return data. This is working fine for every input separately but when I combain for example 2 different fields like price and surface then none of methods will be executed.
Im basically asking about an idea for architecture of search where user can fullfill many different fields. Im not using any PHP framework.
I could do something like:
if(a & b & c & d & e & f) then execute method a
if(a & b & c & d & e) then execute method b
if(a & b & c & d) then execute method c
and so on.. where this letters(a, b, c etc...) are $_POST['something'] but I would have a lots of if's to check which POST (which inputs) user fullfill and sent. Later on I would need to create a lot of methods in class with different SELECTs to db basing on which POST we have... I don't think that's best solution because I would basically repeat my code.
Something like this
$sql = 'SELECT * FROM sometable';
$where = [];
$params = [];
if($a){
$where[] = 'a = :a';
$params[':a'] = $a;
}
if($b){
$where[] = 'b = :b';
$params[':b'] = $b;
}
if(!empty($where)){
$sql .= ' WHERE '.implode(' AND ', $where);
}
$stmt = $PDO->prepare($sql);
$res = $stmt->execute($params);
And so On.
It almost always preferable to use and array and implode for things like this instead of concatenation. Often concatenation will leave you with a hanging "separator" in this case " AND ". For example if we tried this with concatenation:
//if we put WHERE here and then nothing passes our conditions we wind up with:
//"SELECT * FROM sometable WHERE" which wont work
$sql = 'SELECT * FROM sometable ';
//we still need something like an array if we want to prepare our query.
//which is something we should always do
$params = [];
if($a){
//if we put WHERE here, then what if this condition doesn't pass
//do we put it in the next condition? How do we tell. .
$sql .= 'WHERE a = :a AND ';
$params[':a'] = $a;
}
if($b){
//again if the first condition didn't pass how do we know to put "WHERE" here.
//"SELECT * FROM sometable b = :b AND" which wont work
$sql .= 'b = :b AND ';
$params[':b'] = $b;
}
if($c){
//lets say the first 2 conditions passes but this last one failed
//"SELECT * FROM sometable WHERE a = :a AND b = :b AND" which wont work
$sql .= 'c = :c';
$params[':c'] = $c;
}
//we would need to do something like this to trim the last "AND" off
$sql = preg_replace('/\sAND\s$/', '', $sql);
//--------------------
//now if we were prepending "AND" instead of appending it, we're no better off.
//--------------------
//we can fix the where issue by using a string variable (and testing it latter)
$where = '';
if($a){
$where .= 'a = :a';
$params[':a'] = $a;
}
if($b){
//However lets say the first condition failed, we get this:
//"SELECT * FROM sometable WHERE AND b = :b" which wont work
$where .= ' AND b = :b';
$params[':b'] = $b;
//--------------------------
//so in every condition following we would have to test $where
//and if its not empty then we can prepend "AND"
if(!empty($where)) $where .= ' AND ';
$where .= 'b = :b';
$params[':b'] = $b;
}
if($c){
if(!empty($where)) $where .= ' AND ';
$where .= 'c = :c';
$params[':c'] = $c;
}
//finally to fix the "WHERE" issue we need to do something like this:
if(empty($where)) $sql .= ' WHERE '.$where;
//we could also try something like this in every condition:
if($d){
if(empty($where)) $where .= ' WHERE ';
//However, this breaks our fix for prepending "AND", because
//$where will never be empty when we test it.
//if(!empty($where)) $where .= ' AND ';
$where .= 'd = :d';
$params[':d'] = $d;
}
Hopefully that all makes sense. It's just so much easier to use an array and implode it later.
I just wanted to show that to help visualize the issues with concatenation. We wind writing more code, using the same number of variables and double the conditional logic. Or we can get into complicated things like Regex to trim the hanging AND off etc.
Hope that helps!
BECAUSE I mentioned it in the comments.
If you are using "OR" you can of course do the same thing with that, but typically "OR" will cause a full scan of the DB. It's just the way OR works. When we use "AND" the DB (basically) takes the return set and applies the next condition to that, because both have to pass. However, with "OR" rows that failed the first condition could still pass if the second condition passes. So the DB must scan the full record set for each or, as well as keep track of all the rows that passed in the previous conditions. It's just the way the logic works for "OR".
Now for improved "OR" performance we can use a sub-query that is a union. Like this:
$sql = 'SELECT * FROM sometable AS t';
$union = [];
$params = [];
if($a){
$union[] = 'SELECT id FROM sometable WHERE a = a:';
$params[':a'] = $a;
}
if($b){
$union[] = 'SELECT id FROM sometable WHERE b = b:';
$params[':b'] = $b;
}
if(!empty($union)){
$sql .= '
JOIN( '.
implode(' UNION ', $union).
' ) AS u ON t.id = u.id
}
What we wind up with is something like this query:
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable WHERE a = a:
UNION
SELECT id FROM sometable WHERE b = b:
) AS u ON t.id = u.id
When we use "OR" as our dataset grows the DB must store these results in temp table as well as search the entire dataset. Because we are pulling all the columns in the table, this dataset will quickly grow. Once it hits a certian size it will get swapped to Disc and our performance will take a big hit for that.
With the Union query, we also create a temp table. But because we are only concerned with pulling out the ids this temp table will be very small. Union unlike Union ALL will also automatically remove duplicate records further reducing our dataset. So we want to use Union and not Union ALL.
Then we join this back on the table in the outer query and use that to pull the all the columns from just the rows that we need.
Basically we are accepting the fact that we need a temp table and minimizing the impact of that.
This might not seem like it would be much faster, and in some cases it might not be (when no swapping happens). But for me, using a query like you describe where users can search on multiple fields, I was able to reduce the time it took from about 15 seconds to under 1 second. My query had several joins in it such as if a user put in a state, I had to join on participant then participants_addresses (junction table) and then addresses and then finally on states. But if they put in a phone I had to join participant > participants_phones > phone etc.
I can't guarantee this will work in every case and you should use Explain and SQL_NO_CACHE when benchmarking your queries. For example EXPLAIN SELECT SQL_NO_CACHE * FROM .... Explain will tell you how the indexes are working and No Cache prevents the DB from caching the query if you run it multiple times. Caching will make it look like it is fast when it's really not.
You can do something similar when sorting, which also kills performance.
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable WHERE a = a: ORDER BY date DESC
) AS u ON t.id = u.id
This has a similar effect of only sorting the id's in the temp table (instead of the whole dataset), and then when we join it, it actually keeps the order the ids are in. I forget if the order of the subquery vs the outer query matter.
For fun you can even combine the two with 2 nested sub-queries, with the Union as the deepest query (it's something like this).
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable AS t0 JOIN (
SELECT id FROM sometable WHERE a = a:
UNION
SELECT id FROM sometable WHERE b = b:
) AS u ON t0.id = u.id
ORDER BY t0.date DESC
) AS t1 ON t.id = t1.id
It can get pretty complicated though ... lol.
Anyway, I was bored and maybe, just maybe, it will work for someone like it did for me. (this is what happens when I don't get sleep) :)
UPDATE
IF you have problems with the parameters you can output the SQL with the values filled in by doing this:
echo str_replace(array_keys($params), $params, $sql)."\n";
But use this only for Debugging, not for putting the data into the query because that would defeat the purpose of using prepared statements and open you up to SQLInjection attacks. That said, it can make it easier to see if you are missing anything or have any spelling errors. I also use this when I just want to test the query in PHPMyAdmin, but am to lazy to cut an paste the data into it. Then I just copy the output put it in PHPMyAdmin and then I can rule out any issues with PHP or tweak the query if need be.
You can also have issues if you have to many elements in the array, AKA extra placeholders that are not in the query.
For that you can do
//count the number of : in the query
$num_placeholders = substr_count(':', $sql);
//count the elements in the array
$num_params = count($params);
if($num_placeholders > $num_params ) echo "to many placeholders\n";
else if($num_placeholders < $num_params ) echo "to many params\n";
One last thing to be mindful of when mixing "AND" and "OR" is stuff like this
SELECT * FROM foo WHERE arg1 = :arg1 OR arg2 = :arg2 AND arg3 = :arg3
The way it executes this is like this
SELECT * FROM foo WHERE arg1 = :arg1 OR (arg2 = :arg2 AND arg3 = :arg3)
This will return all rows that match arg1 regardless of the rest of the query.
Most of the time this would not be what you want. You would actually want it to do it this way:
SELECT * FROM foo WHERE (arg1 = :arg1 OR arg2 = :arg2) AND arg3 = :arg3
Which is called an "Exclusive OR". This will return all rows that match arg1 OR arg2 AND arg3
Hope that helps.
You could also create an wanted list of nesseccary items and Check If each Item is Set by the PHP function isset().

Loop through results of one query and insert them into another, joining with union all results of 2nd query

I have a query that I would really appreciate any bit of help on.
I have a first query that selects the job_types in one table (26 results). These job_type names all relate to a separate table that is the same name as them. Then, I need to use this result (the list of job_types) to call a new query on all the related job_type tables - looping through the results from the first query, inserting them in to the second query, and I am trying to UNION the results and output them into a table. But I'm stuck with the loop. My code so far is:
$sql = "";
$union = "";
$tables = [];
$q = "SELECT DISTINCT job_table
FROM job_type
WHERE job_type NOT IN (26, 28) AND status = 1";
$tables[] = $conn->query($q);
print_r($tables);
/*while ($table = $tables->fetch_assoc()) {
$sql .= "SELECT
DATE(call_time) AS `date`,
COUNT(*) AS `sub_total`,
'$table->job_table' AS `table`
FROM '$table->job_table'
WHERE table.show IS TRUE AND call_time BETWEEN $startDate AND $endDate
AND table.processing_complete IS TRUE
GROUP BY `date`";
}*/
foreach ($tables AS $table) {
$sql .= $union . "SELECT
DATE(call_time) AS `date`,
COUNT(*) AS `sub_total`,
'$table' AS `table`
FROM '$table'
WHERE table.show IS TRUE AND call_time BETWEEN $startDate AND $endDate
AND table.processing_complete IS TRUE
GROUP BY `date`";
$union = " UNION ";
}
I know I need to put in a loop, in order to union the tables and run through them until the final one, so the WHILE loop is something I was trying out, as is the FOREACH loop. I'm only learning (slowly) PHP and would much appreciate any assistance.
EDIT
I have added the following code to the end to try and get it to display.
$results = $conn->query($sql);
}
while ($row = $results->fetch_assoc()) {
echo"<TABLE><TR><TD>". $row["date"]. "</TD>";
echo"<TD>". $row["sub_total"]. "</TD>";
echo"<TD>". $row["table"] . "</TD></TR>";
}
However, then I run it, I am returning the following error - 'Catchable fatal error: Object of class mysqli_result could not be converted to string' on the '$table' AS table line.
The part you put in comments was the better attempt, so I will focus on that code block.
Some issues:
The query method returns a result object. You should not assign it to $tables[], which would make $tables an array with one result object. Just assign the result to $tables.
The fetch_assoc method makes your records available as associative records, so you should not use the object notation $table->, but brackets: $table[ ... ].
The FROM clause in SQL should not be followed by something in quotes, but by the name itself, so don't put single quotes there
When you embed expressions in a double quoted string, you need to wrap them in braces, like {$table[...]}. Without those braces you can only embed atomic variables, like $startDate.
In MySql SQL dates must be quoted (I assume $startDate and $endDate are strings)
In SQL the IS operator is really intended in combination with NULL, not with another value. What's more, checking equality with TRUE is often overkill, as you could just test the expression on the left of the equality
To join the different SQL snippets with UNION, you could first collect them in an array, and then apply implode to that. Also: UNION ALL has better performance, and makes no difference in your case.
Corrected code:
// *** query method returns a result object, not an array:
$tables = $conn->query($q);
while ($table = $tables->fetch_assoc()) {
// *** Use {$ ... } notation, with bracket access, not '->'.
// *** Quote dates. Do not quote tables. Omit "IS TRUE".
// *** Collect into array. The `UNION` can be added later.
$sql[] = "
SELECT
DATE(call_start_time) AS `date`,
COUNT(*) AS `sub_total`,
'{$table[job_processing_table]}' AS `table`
FROM {$table[job_processing_table]}
WHERE table.show
AND call_start_time BETWEEN '$startDate' AND '$endDate'
AND table.processing_complete
GROUP BY `date`";
}
// *** Add UNION:
$sql = implode(" UNION ALL ", $sql)

I need to nest 2 arrays so that I can echo order header as well as order item details

I have updated my original post based on what I learned from your comments below. It is a much simpler process than I originally thought.
require '../database.php';
$pdo = Database::connect();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "SELECT * FROM Orders WHERE id = 430";
$q = $pdo->prepare($sql);
$q->execute(array($id));
$data = $q->fetch(PDO::FETCH_ASSOC);
echo 'Order Num: ' . $data['id'] . '<br>';
$sql = "SELECT * FROM Order_items
JOIN Parts ON Parts.id = Order_Items.part_id
WHERE Order_Items.orders_id = 430";
$q = $pdo->prepare($sql);
$q->execute(array($line_item_id));
$data = $q->fetch(PDO::FETCH_ASSOC);
while ($data = $q->fetch(PDO::FETCH_ASSOC))
{
echo '- ' . $data['part_num'] . $data['qty'] . "<br>";
}
Database::disconnect();
Unfortunately, only my first query is producing results. The second query is producing the following ERROR LOG: "Base table or view not found: 1146 Table 'Order_items' doesn't exist" but I am expecting the following results.
Expected Results from Query 1:
Order Num: 430
Expected Results from Query 2:
- Screws 400
- Plates 35
- Clips 37
- Poles 7
- Zip ties 45
Now that I understand where you are coming from, let's explain a couple of things.
1.PDO and mysqli are two ways of accessing the database; they essentially do the same things, but the notation is different.
2.Arrays are variables with multiple "compartments". Most typical array has the compartments identified by a numerical index, like:
$array[0] = 'OR12345'; //order number
$array[1] = '2017-03-15'; //order date
$array[2] = 23; //id of a person/customer placing the order
etc. But this would require us to remember which number index means what. So in PHP there are associative arrays, which allow using text strings as indexes, and are used for fetching SQL query results.
3.The statement
$data = $q->fetch(PDO::FETCH_ASSOC)
or
$row = $result->fetch_assoc()
do exactly the same thing: put a record (row) from a query into an array, using field names as indexes. This way it's easy to use the data, because you can use field names (with a little bit around them) for displaying or manipulating the field values.
4.The
while ($row = $result->fetch_assoc())
does two things. It checks if there is a row still to fetch from the query results. and while there is one - it puts it into the array $row for you to use, and repeats (all the stuff between { and }).
So you fetch the row, display the results in whatever form you want, and then loop to fetch another row. If there are no more rows to fetch - the loop ends.
5.You should avoid using commas in the FROM clause in a query. This notation can be used only if the fields joining the tables are obvious (named the same), but it is bad practice anyway. The joins between tables should be specified explicitly. In the first query you want the header only, and there is no additional table needed in your example, so you should have just
SELECT *
FROM Orders
WHERE Orders.Order_ID = 12345
whereas in the second query I understand you have a table Parts, which contains descriptions of various parts that can be ordered? If so, then the second query should have:
SELECT *
FROM Order_items
JOIN Parts ON Parts.ID = Order_Items.Part_ID
WHEERE Order_Items.Order_ID = 12345
If in your Orders table you had a field for the ID of the supplier Supplier_ID, pointing to a Suppliers table, and an ID of the person placing the order Customer_ID, pointing to a Customers table, then the first query would look like this:
SELECT *
FROM Orders
JOIN Suppliers ON Suppliers.ID = Orders.Supplier_ID
JOIN Customers ON Customers.ID = Orders.Customer_ID
WHERE Orders.Order_ID = 12345
Hope this is enough for you to learn further on your own :).

SQL MATCH not Matching correctly

I have 3 tables, and i have joined them together, which works fine, it pulls the information. One of the tables as an array within a column, on every row like:
["Pets","Schools","Shops"]
I need, while selecting, the query to pull out when a MATCH AGAINST a var. Here is my code:
$searchRefine = '';
foreach( $refineAmen as $key => $val) {
$searchRefine = $searchRefine . " MATCH(pa.Property_Amenities) AGAINST ('".$val."' IN BOOLEAN MODE) OR ";
}
The above takes each var from the array from the row / column, and adds it to a sub-query string.
$searchRefine = substr($searchRefine, 0, -3);
The above takes out the last 3 (or the word 'OR') at the end as its not needed
$detail = "SELECT p.*, pi.*, pa.* FROM tbl_property p LEFT join tbl_property_images pi on p.Property_Id = pi.Property_Id LEFT join tbl_property_amenities pa on pi.Property_Id = pa.Property_Id WHERE (p.Property_Postcode='" . $_POST['cust_id'] . "' OR p.Property_City Like '%" . $_POST['cust_id'] . "%') AND ( " . $searchRefine. " ) GROUP BY pi.Property_Id";
The above takes the sub strings and adds it to the final for firing. I do not get any errors.
The issue is, it does not pull the records with any of these key works within the row, just One. I have tried a number of combinations of AND or OR, so the query understands. But still no luck. Any someone look at this, and see if they can see what I have done wrong.
Thanks
You should normalize your database structure instead of storing multiple values in one column. This way you would not have to compute a sub-query string for fulltext search with MATCH...AGAINST.
Can you show your database structure?
Beware of SQL injection. Once your query is working you should alter it to escape client data oder use prepared statements.

Search mySQL with PHP, using a WHERE wildcard or an IF statement?

I'm letting users search my database for data by city.
My query looks like:
$results = mysql_query("SELECT * FROM mydb WHERE City='".$city."' LIMIT 10");
I want a user to be able to search 'all cities', so I'd like to either remove the WHERE statement if $city=='all cities'; or use a wildcard for the WHERE statement that matches all cities in the db.
I used to have an IF statement that switched between two queries, but I want to add more filters like country/all countries, zipcode/all zipcodes, etc, So I'd rather keep one dynamic SQL query.
Well, you could still have just one query and build the where clause dynamically, as such:
$where = '';
// conditional statements (if/else, switch) for populating the where clause
$where .= " WHERE City = '{$city}'";
$where .= " AND Country = '{$country}'";
$results = mysql_query("SELECT * FROM mydb{$where} LIMIT 10");
One way would be a case statement:
WHERE City = case when '$city' = 'All cities' then City else '$city' end
If the user is searching for 'All cities', this turns the WHERE statement into:
WHERE City = City
Which is always true (at least for non-null cities ;))
P.S. Make sure you're running these queries using a read-only MySQL account. The user could enter funny stuff into the $city parameter!
You could try
WHERE City like '$city'
and permit the users to enter wildcards, if you think they'd be up to it.
although not PHP programmer, this pseudocode might offer an option... conditionally build out your where clause. Additionally, I would do it with parameterized queries instead of direct string building to prevent sql-injection attacks.
cYourSQL = "select * from YourTable where "
cAndRequired = ""
if city is NOT "all cities"
cYourSQL = cYourSQL + cAndRequired + " city = 'YourParameterValueProvided' "
cAndRequired = " AND "
endif
Now, always add your country selection
cYourSQL = cYourSQL + cAndRequired + " country = 'YourCountryValue' LIMIT 10 "
Run the query

Categories