Writing a PDO search query from a PHP array - php

I'm building an application using PHP 7 and a PDO connection to a MySQL database.
One part of the application contains a search form which allows a user to search for a training course by 3 different fields: the course category, the course name, and a date.
The types of elements on the form are:
Course category - dropdown, with numerical (int) ID's.
Course name - text input
Date - date picker (using HTML 5 type="date" parameter to get a calendar in the browser).
These fields can be used in conjunction, or on their own. This means a user could search, for example, just by (1), or (2 & 3), or all (1 & 2 & 3).
I've written the PHP to get the POST data and it's now in an array - for example:
$search_data = [
'category' => 3,
'name' => 'Hazard training',
'date' => ''
]
I want to use this within a PDO query but I don't know what the best way to write it is because (1) and (3) would be an = query condition, whereas (2) is a LIKE. My solution was going to be looping through the search terms and then trying to construct a query, e.g.
$sql = ' WHERE ';
foreach ($search_data as $key => $value) {
if ($key == 'category') {
$sql .= ' category = ' . $value;
}
if ($key == 'course_name') {
$sql .= ' course_name LIKE % ' . $value ' % ';
}
if ($key == 'date') {
$sql .= ' date = ' . $value;
}
}
The trouble with this is it doesn't work because of having to bind the parameters in PDO. It also doesn't work because I can't find a way to get the AND between each query (if there is a preceding statement).
I'm lost with this now and unsure what the best way to write this is.
Any help would be appreciated.
Edit: I realise that hardcoding the names, e.g. ($key == 'course_name') isn't ideal, but this is only being done because of the different query conditions (LIKE vs =). I assume that one could make $search_data multi-dimensional to say which type of query it was, but this is beyond my initial problem and probably another post.

Here`s a simple solution to your problem:
$sql = 'SELECT ..... FROM ... WHERE 1 ';
$where = '';
$pdoData = [];
foreach ($search_data as $key => $value) {
if(!$value) continue; // skip empty values
if ($key === 'category') {
$pdoData[':category'] = $value;
$where .= ' AND category = :category ';
}
if ($key === 'course_name') {
$pdoData[':course_name'] = '%'.$value.'%';
$where .= ' AND course_name LIKE (:course_name) ';
}
if ($key === 'date') {
$pdoData[':date'] = $value;
$where .= ' AND date = :date ';
}
}
$sql = $sql.$where;
$stmt = $this->ci->db->prepare($sql);
$stmt->execute($pdoData);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
And you have $pdoDate array holding the binded data.

Related

Combine two if conditions

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
}

PDO dynamic parameter binding where clause AND between dates

I have a search form with several input fields. 2 of these input fields are dates. If a users fills in the From/To date, all records from/untill that date should be shown; if a users gives 2 dates, all records between these 2 dates should be shown.
This is my working code for the other input fields, but I have no idea how to implement the date constraints. The parameter binding should stay dynamic since we don't know on how many variables the user will search.
<?php $this->databaseConnection();
$sql = 'SELECT * FROM trips t';
$searchTerms = array_filter($_GET);
$whereClause = array();
foreach($searchTerms as $key=>$value)
{
//Push values to array
array_push($whereClause, "t." . $key . "=:" . $key);
}
if(!empty($whereClause))
{
$sql .= " WHERE " . implode(" AND ", $whereClause);
}
if($this->databaseConnection()) {
$query_search_data = $this->db_connection->prepare($sql);
foreach($searchTerms as $key=>$value)
{
$query_search_data->bindValue(':' . $key, $value);
}
$query_search_data->execute();
$result = $query_search_data->fetchAll(); ?>
I found the solution.
Unset the date variables (e.g. unset($searchTerms['fromDate'], $searchTerms['toDate']);) and add them again at the end of the query.
if (isset($_GET['fromDate']) && !empty($_GET['fromDate'])) {
$sql .= " AND Date >=:from_date";
}
Hope this can help other people with dynamic parameter binding issues.

Loop through an array to create an SQL Query

I have an array like the following:
tod_house
tod_bung
tod_flat
tod_barnc
tod_farm
tod_small
tod_build
tod_devland
tod_farmland
If any of these have a value, I want to add it to an SQL query, if it doesnt, I ignore it.
Further, if one has a value it needs to be added as an AND and any subsequent ones need to be an OR (but there is no way of telling which is going to be the first to have a value!)
Ive used the following snippet to check on the first value and append the query as needed, but I dont want to copy-and-paste this 9 times; one for each of the items in the array.
$i = 0;
if (isset($_GET['tod_house'])){
if ($i == 0){
$i=1;
$query .= " AND ";
} else {
$query .= " OR ";
}
$query .= "tod_house = 1";
}
Is there a way to loop through the array changing the names so I only have to use this code once (please note that $_GET['tod_house'] on the first line and tod_house on the last line are not the same thing! - the first is the name of the checkbox that passes the value, and the second one is just a string to add to the query)
Solution
The answer is based heavily upon the accepted answer, but I will show exactly what worked in case anyone else stumbles across this question....
I didnt want the answer to be as suggested:
tod_bung = 1 AND (tod_barnc = 1 OR tod_small = 1)
rather I wanted it like:
AND (tod_bung = 1 OR tod_barnc = 1 OR tod_small = 1)
so it could be appended to an existing query. Therefore his answer has been altered to the following:
$qOR = array();
foreach ($list as $var) {
if (isset($_GET[$var])) {
$qOR[] = "$var = 1";
}
}
$qOR = implode(' OR ', $qOR);
$query .= " AND (" .$qOR . ")";
IE there is no need for two different arrays - just loop through as he suggests, if the value is set add it to the new qOR array, then implode with OR statements, surround with parenthesis, and append to the original query.
The only slight issue with this is that if only one item is set, the query looks like:
AND (tod_bung = 1)
There are parenthesis but no OR statements inside. Strictly speaking they arent needed, but im sure it wont alter the workings of it so no worries!!
$list = array('tod_house', 'tod_bung', 'tod_flat', 'tod_barnc', 'tod_farm', 'tod_small', 'tod_build', 'tod_devland', 'tod_farmland');
$qOR = array();
$qAND = array();
foreach ($list as $var) {
if (isset($_GET[$var])) {
if (!empty($qAND)) {
$qOR[] = "$var = 1";
} else {
$qAND[] = "$var = 1";
}
$values[] = $_GET[$var];
}
}
$qOR = implode(' OR ', $qOR);
if ($qOR != '') {
$qOR = '(' . $qOR . ')';
}
$qAND[] = $qOR;
$qAND = implode(' AND ', $qAND);
echo $qAND;
This will output something like tod_bung = 1 AND (tod_barnc = 1 OR tod_small = 1)
As the parameter passed to $_GET is a string, you should build an array of strings containing all the keys above, iterating it and passing the values like if (isset($_GET[$key])) { ...
You could then even take the key for appending to the SQL string.
Their are a lot of ways out their
$list = array('tod_house', 'tod_bung', 'tod_flat', 'tod_barnc', 'tod_farm', 'tod_small', 'tod_build', 'tod_devland', 'tod_farmland');
if($_GET){
$query = "";
foreach ($_GET as $key=>$value){
$query .= (! $query) ? " AND ":" OR ";
if(in_array($key,$list) && $value){
$query .= $key." = '".$value."'";
}
}
}
Sure you have to take care about XSS and SQL injection
If the array elements are tested on the same column you should use IN (...) rather than :
AND ( ... OR ... OR ... )
If the values are 1 or 0 this should do it :
// If you need to get the values.
$values = $_GET;
$tod = array();
foreach($values as $key => $value) {
// if you only want the ones with a key like 'tod_'
// otherwise remove if statement
if(strpos($key, 'tod_') !== FALSE) {
$tod[$key] = $value;
}
}
// If you already have the values.
$tod = array(
'tod_house' => 1,
'tod_bung' => 0,
'tod_flat' => 1,
'tod_barnc' => 0
);
// remove all array elements with a value of 0.
if(($key = array_search(0, $tod)) !== FALSE) {
unset($tod[$key]);
}
// discard values (only keep keys).
$tod = array_keys($tod);
// build query which returns : AND column IN ('tod_house','tod_flat')
$query = "AND column IN ('" . implode("','", $tod) . "')";

(PHP+MySQL) How can I echo the column name on a specific condition?

I am using PHP 5.4 with a MySQL database.
This database represents a media library. The table I'm dealing with has one column, "Title", with obvious contents, and then a series of boolean columns, representing the availability of that title on a given platform. So a row looks like
TITLE: "Curb Your Enthusiasm: The Game"
PS4: 0
Atari 2600: 1
Dreamcast: 0
And so on.
The PHP code I would like to write be, in pseudocode,
Echo row[0] (title)
Cycle through other cells in the row
If the cell is '0' or NULL, do nothing
But if the cell is '1', echo the name of that column
So the result would be the echoing of
Curb Your Enthusiasm: The Game (Atari 2600, WonderSwan, Saturn)
It's the fourth statement that I can't quite work out. It seems to require the function mysqli_fetch_field, but I'm not sure of the syntax, and nothing I try after googling quite works.
I'd really appreciate any advice or examples someone could offer!
$database = mysqli_connect(SERVER,USERNAME,PASSWORD,'games');
$query = mysqli_query($database,"SELECT * FROM games` WHERE NAME LIKE '%ZELDA%'");
while ($row = mysqli_fetch_row($query)) {
echo $row[0]; // Echo title
for ($i=0;$i<sizeof($row);$i++) {
if ($row[$i] === '1') {
// ???????
}
}
}
Here is some rough untested code that should hopefully get you going.
while ($row = mysqli_fetch_assoc($query)) {
$columns = array(); // this will track the additional columns we need to display
foreach($row AS $column => $value) {
if($column == "title") {
echo $value; // this is the title, just spit it out
continue;
}
if($value == 1) {
// We have a column to display!
$columns[] = $column;
}
}
if(count($columns)) {
// We have one or more column names to display
echo " (" . implode(", ",$columns) . ")";
}
}
Some things to point out:
Using mysqli_fetch_assoc will allow you access to column names along with the values, which is useful here.
Keep track of the columns you want to display in an array first, this makes it easier at the end of each loop to format the output.
Sounds like you can do something like this:
// Simulates DB fetch
$titles = array(
array(
'TITLE'=>'Curb Your Enthusiasm: The Game',
'PS4'=>0,
'Atari 2600'=>1,
'Dreamcast'=>0
),
array(
'TITLE'=>'Curb Your Enthusiasm: The Book',
'PS4'=>1,
'Atari 2600'=>1,
'Dreamcast'=>0
)
);
foreach($titles as $title){
// get supported platforms
$supportedPlatforms = array();
foreach($title as $titleAttribute=>$titleValue){
if($titleAttribute != 'TITLE' && $titleValue == 1)
$supportedPlatforms[] = $titleAttribute;
}
echo $title['TITLE'] . ' (' . implode(', ', $supportedPlatforms) . ')' . "<br>";
}
Try running it here: http://phpfiddle.org/lite/code/pr6-fwt

Query returns different results between PL/SQL Developer and PHP

I have made a small intranet website to collect and store data to be used to expedite our logistics processes. I'm now in the process of adding search functionality which, if records are found that match that criteria, will allow the user to quickly select parts of that data to pre-populate a new shipping request with data (e.g, the user types 'Mar' in the Recipient Name input textbox and '109' in the Street Address input textbox and the query returns two records: {"Mary Smith", "1090 South Central St"} and {"Mark Swanson", "109 E. 31st St."}).
At the moment, when search criteria is entered and submitted, the data returned from the query in PHP is 100% accurate if and only if a single criteria is entered (such as Recipient Name). When I attempt to use two different search criterias in PHP, the record results do not match the results when running the same query in Oracle PL/SQL Developer. If three different search criterias are used, the query ran in PHP will return 0 records. In all three of the aforementioned scenarios, the query is executed without error in Oracle PL/SQL Developer.
The following code is from my PHP search function. The input data to this function is an associate array of field names and the user inputted search criteria data for that field.
public function Search()
{
if($this->dbcon)
{
$query = "SELECT * FROM ship_request ";
$postCount = count($this->post_data);
$counter = 0;
if ($postCount > 0)
{
$query .= "WHERE ";
}
foreach ($this->post_data as $k => $v)
{
$counter++;
if (strlen($v) > 0)
{
if ($k == 'SR_DATE')
{
$query .= $k . " = :" . $k . " AND ";
} else {
$query .= "upper(" . $k . ") like upper(:" . $k . ") AND ";
}
}
}
if (substr($query,-4) == "AND ")
{
$query = substr($query, 0, strlen($query) - 4);
}
$stid = oci_parse($this->ifsdb, $query);
foreach ($this->post_data as $k => $v)
{
if (strlen($v) > 0)
{
if ($k == 'SR_DATE')
{
$this->post_data[$k] = date("d-M-y", strtotime($this->post_data[$k]));
$placeHolder = $this->post_data[$k];
} else {
$placeHolder = '%' . $this->post_data[$k] . '%';
}
oci_bind_by_name($stid, $k, $placeHolder);
}
}
oci_execute($stid);
$nrows = oci_fetch_all($stid, $recordsFound);
$recordsFound = json_encode($recordsFound);
oci_free_statement($stid);
echo $recordsFound;
} else {
die("Could not connect to database!");
}
}
}
I've done a var_dump on $query to see what my query actually looks like when I enter multiple search criteria values. This is an example of what I see:
select * from HOL_SHIP_REQUEST where upper(sr_shipper_name) like upper(:sr_shipper_name) and upper(sr_recipient_name) like upper(:sr_recipient_name) and sr_recipient_phone like upper(:sr_recipient_phone)
That query returns 0 records when I enter "a" for Shipper Name, "m" for Recipient Name, and "2" for Phone Number.
This query, when executed in Oracle PL/SQL Developer, however, returns 27 records.
select * from HOL_SHIP_REQUEST where upper(sr_shipper_name) like upper('%a%') and upper(sr_recipient_name) like upper('%m%') and sr_recipient_phone like upper('%2%')
Is there something wrong with the way that I'm trying to bind the parameters in PHP? Is there something different I have to do when using multiple like statements?
You've forgotten the % wildcard chars in your built query string. The DB interface libraries do NOT parse the query you're building, and do NOT look for LIKE clauses - it's not their job to guess what kind of match you're trying to do. e.g. are you doing
WHERE a LIKE 'b'
WHERE a LIKE 'b%'
WHERE a LIKE '%b'
WHERE a LIKE '%b%'
It's up to you to provide the appropriate wildcards, and since you're using placeholders, you'll have to do it yourself, e.g.
WHERE UPPER(sr_shipper_name) LIKE CONCAT('%', :sr_shipper_name, '%')
If you were to do it something like this:
$shipper = '%foo%';
WHERE ... LIKE :shipper
you'd end up with the equivalent of:
WHERE ... LIKE '\%foo\%'
The placeholder system also doesn't parse your provided text and try to figure out if you're really trying to use a wilcard or just passing in a literal % char. That's why you have to use the CONCAT hack to build a proper wildcarded construct.

Categories