I would like to create dinamically in my php class a where clause from a array where are defined search fields.
$search = array('brand' => 'something', 'model' => 'something');
$myclass->testarr($search);
CLASS
public function testarr($search){
if (!empty($search)){
foreach ($search as $key => $value) {
$where = $key . " = " . $value;
}
$clause = !empty($where) ? 'WHERE' : '';
$result = $this->db->mysqli->query
("SELECT * FROM tb1 $clause $where");
}
}
My problem is to manage a clause with more than one field by entering the suffix AND. How could I do that? Thanks
I would advice to do this:
$where = array();
if (!empty($search) && is_array($search)) {
foreach ($search as $key => $value) {
$where[] = $key . " = " . $value;
}
}
if (!empty($where))
$query = sprintf('SELECT * FROM tb1 WHERE %s', implode('AND ', $where));
else
$query = 'SELECT * FROM tb1';
Using implode makes things easier.
Beware however of escaping issues, as your code is prone to security issues.
There is one flaw with your code: $where = $key . " = " . $value; will overwrite $where in each iteration, you need to use .= to concatenate. Then this could be done e.g. the following way
$where = "";
foreach ($search as $key=>$value) {
if (!empty($where)) $where .= " AND ";
$where .= $key . " = " . $value;
}
$clause = !empty($where) ? 'WHERE '.$where : '';
This will add a AND before every condition, starting from the second (because for the first the if will fail).
I suggest researching prepared statements, these will make your code alot more secure and once you understood the concept, they become quite easy to handle (imo). Because if that is most of your code at the moment, you are quite vulnerable to SQL injections.
Related
I'm working with an API to retrieve records based on user input.
There are filters and filter groups that will be concatenated, however, I do not want the last AND or OR in the nested for each loop to be concatenated to the string.
I would like to have an if statement that looks for the last item in the nested foreach loop and concatenates a different string to the end, like this:
if($i == $lastItem) {
$conditions .= 'WHERE ' . $type . ' = '. "'". $tag . "'";
}
else {
$conditions .= 'WHERE ' . $type . ' = '. "'". $tag . "'" . ' ' . $condition . " ";
}
What is the best practice for finding the last item of a nested for each loop using PHP?
Here is the reference code:
$conditions = "";
$filters = "";
foreach($request->filters as $key=>$filter) {
foreach($filter as $item) {
if($item['tag']) {
$type = $item['code'];
$tag = $item['tag'];
$condition = $item['condition'];
$conditions .= 'WHERE ' . $type . ' = '. "'". $tag . "'" . ' ' . $condition . " ";
}
}
$groupCondition = $request->conditions[$key]['condition'];
$filters .= '('.$conditions.') ' . $groupCondition . " ";
}
Here is an example of a request, the two objects are combined in the foreach loop:
filter: {
0 {
0 {
code: 'gl_code',
condition: 'OR',
tag: '10-140-4700-0401'
}
1 {
0 {
code: 'ty_letter_no',
condition: 'AND',
tag: 'AM123'
},
1 {
code: 'gl_code',
condition: 'OR',
tag: '10-140-4700-0401'
}
}
groupConditions: {
0 {
condition: 'OR'
}
1 {
condition: 'AND'
}
}
Here an example of the current output:
"(WHERE ty_letter_no = 'AM123' AND )
OR
(WHERE ty_letter_no = 'AM123' AND WHERE solicit_code = '19-10NL' AND WHERE ty_letter_no = 'AU' AND )
AND
(WHERE ty_letter_no = 'AM123' AND WHERE solicit_code = '19-10NL' AND WHERE ty_letter_no = 'AU' AND WHERE solicit_code = '19-04HRGOLF' AND )
AND "
I would like it to output:
"(WHERE ty_letter_no = 'AM123')
OR
(WHERE ty_letter_no = 'AM123' AND WHERE solicit_code = '19-10NL' AND WHERE ty_letter_no = 'AU')
AND
(WHERE ty_letter_no = 'AM123' AND WHERE solicit_code = '19-10NL' AND WHERE ty_letter_no = 'AU' AND WHERE solicit_code = '19-04HRGOLF')"
I shall work under these assumptions:
the request comes from a "LEGO model" builder, so that the condition entry is always present, but sometimes not significant (specifically, when it is the last of its group).
the resulting code has to be a valid SQL condition.
the string values do not need escaping and have been proofed against SQL injection.
If this is so, then you can use a limited state machine to achieve your result:
$completeCondition = '';
$groupjoin = '';
foreach ($request->filter as $index => $conditions) {
$conditionjoin = '';
$partialCondition = '';
foreach ($conditions as $triplet) {
$partialCondition .= "{$conditionjoin}{$triplet->code} = '{$triplet->tag}'";
$conditionjoin = " {$triplet->condition} ";
}
$completeCondition .= "{$groupjoin}({$partialCondition})";
$groupjoin = " {$request->groupConditions[$index]->condition} ";
}
if (!empty($completeCondition)) {
$completeCondition = " WHERE {$completeCondition}";
}
Using this version of your request,
$request = json_decode('{
"filter": [
[
{ "code": "gl_code", "condition": "OR", "tag": "10-140-4700-0401" }
],
[
{ "code": "ty_letter_no", "condition": "AND", "tag": "AM123" },
{ "code": "gl_code", "condition": "OR", "tag": "10-140-4700-0401" }
]
],
"groupConditions": [ { "condition": "OR" }, { "condition": "AND" } ]
}');
the result is the following, valid SQL:
WHERE (gl_code = '10-140-4700-0401')
OR (ty_letter_no = 'AM123' AND gl_code = '10-140-4700-0401')
(if the destination language is not SQL then the code can be changed slightly, of course).
a more refined result (PDO support)
Normally you do not want to include the request strings in your SQL code as is, because this allows the user to arbitrarily alter the SQL code you will execute. For example if I were to send along a tag of
'||SLEEP(60)||'
the code above would happily encode it as, say, gl_code = ''||SLEEP(60)||'', which is a valid SQL request and will be executed, halting your thread for sixty seconds. If I know you're using MySQL, I can perform some tricks with the LOCK() function and try to exhaust the internal metadata memory. If you're really unlucky and composite queries have not been disabled (they usually are!), then I very much fear I can own your SQL server. Even if they are, there are several dirty tricks that can be done with LEFT JOINs, UNIONs and SELECT 'code' INTO DUMPFILE '/var/www/nasty.php', and not all installations are hardened against all of them.
To avoid this we use PDO and SQL parameterization. This requires sending the query in two parts, a prepared query like
`...AND gl_code = :value1`
and a binding list containing ...':value1' => 'AL-1234-56'....
$completeCondition = '';
$groupjoin = '';
$bindingList = array();
foreach ($request->filter as $index => $conditions) {
$conditionjoin = '';
$partialCondition = '';
foreach ($conditions as $triplet) {
$bind = ':b' . count($bindingList);
$partialCondition .= "{$conditionjoin}{$triplet->code} = {$bind}";
$conditionjoin = " {$triplet->condition} ";
$bindingList[$bind] = $triplet->tag;
}
$completeCondition .= "{$groupjoin}({$partialCondition})";
$groupjoin = " {$request->groupConditions[$index]->condition} ";
}
if (!empty($completeCondition)) {
$completeCondition = " WHERE {$completeCondition}";
}
// Now we could safely do (supposing $pdo is my PDO DB object)
$stmt = $pdo->prepare($completeCondition);
$stmt->execute($bindingList);
while ($row = $stmt->fetch()) {
// Do something with the row.
}
Just an idea: you can always understand if the current iteration is the first (by using a flag).
In this way, you can:
skip the first iteration
save at each iteration the current item, to be used at the next iteration
Update the query at the Nth iteration appending information related to the (N-1)th iteration
Outside the loop append the information related to the last iteration applying the modifications you need
Something like
$conditions = "";
$filters = "";
$isFirst = true;
foreach($request->filters as $key=>$filter) {
foreach($filter as $item) {
if($isFirst)
{
$isFirst = false;
}
else
{ if($previousItem['tag']) {
$type = $previousItem['code'];
$tag = $previousItem['tag'];
$condition = $previousItem['condition'];
$conditions .= 'WHERE ' . $type . ' = '. "'". $tag . "'" . ' ' . $condition . " ";
}
}
$previousItem = $item;
}
$groupCondition = $request->conditions[$key]['condition'];
$filters .= '('.$conditions.') ' . $groupCondition . " ";
}
if(isset($previousItem))
{
$type = $previousItem['code'];
$tag = $previousItem['tag'];
$conditions .= 'WHERE ' . $type . ' = '. "'". $tag . "'"
}
You could try adapting this solution to your needs.
To strictly answer your question, can you see if you're working on the last iteration by checking the current iteration (in your case, the key) against the length (count()) of the array. You can avoid doing something on that iteration by making sure that if( $iteration < $length - 1 ), then you're not in the last iteration. If you want to check that you are in the last, if( $iteration == $length - 1 ).
To address the larger scope of the question:
When programmatically building queries, I've often found it easier to make use of the WHERE 1=1 to start, since it allows you to implode() an array using AND as a glue (and some DBMS will not even parse the 1=1).
$filters = "";
$length = count($request->filters);
foreach($request->filters as $key => $filter) {
$conditions = array( "WHERE 1=1" );
foreach($filter as $item){
$tag = $item['tag'];
$type = $item['code'];
$condition = $item['condition'];
$conditions[] = "{$type} = '{$tag}'";
}
// Build the conditions
$filters .= sprintf( '(%s)', implode(' AND ', $conditions ) );
// If this isn't the last item, add the group conditions
$filters .= ( $key < $length-1 ) ? " {$request->conditions[$key]['condition']} " : '';
}
The last line checks to make sure it's not the last item (by making sure the current index is less than the $length-1 before appending that last condition. Using the above, you'd end up with a filter like:
(WHERE 1=1 AND gl_code = '10-140-4700-0401') OR (WHERE 1=1 AND ty_letter_no = 'AM123' AND gl_code = '10-140-4700-0401')
If the service you're sending that to doesn't like the 1=1 (it shouldn't have any effect at all, but if it does) you can do $filters = str_replace( '1=1 AND ', '', $filters ); at the end.
(WHERE gl_code = '10-140-4700-0401') OR (WHERE ty_letter_no = 'AM123' AND gl_code = '10-140-4700-0401')
Also note that this doesn't output strictly valid SQL, just the format you requested (I'm assuming the service wants the filters in this format for their own reasons)
Here's a quick example
am currently trying to do a search in php and can pass single values through and get results. Now my challenge is that am trying to pass an array of values into a forEach but my loop only goes once through then end's with only the last value in the array. Can someone point out the mistake am making currently? I should note that the search is in two different tables so am using an alias for both.
if (!empty($postData->searchTxt)) {
$nameArray = preg_split ("/[\s,]+/", $postData->searchTxt);
$countNames =count($nameArray);
foreach($nameArray as $index => $code ) {
$whereClausePrefix === ' where ';
$conditions = $whereClausePrefix." (ex.name like '%".$code."%') or ex.surname like '%".$code."%' or u.user_email like '%".$code."%' ";
if ($whereClausePrefix === ' where ') {
$whereClausePrefix === ' and ';
}
}
echo $conditions;
}
you don't append conditions. you reset it in every iteration.
if (!empty($postData->searchTxt)) {
$nameArray = preg_split ("/[\s,]+/", $postData->searchTxt);
$whereClausePrefix === ' where ';
$countNames =count($nameArray);
foreach($nameArray as $index => $code ) {
$conditions = $conditions ." ". $whereClausePrefix." (ex.name like '%".$code."%') or ex.surname like '%".$code."%' or u.user_email like '%".$code."%' ";
if ($whereClausePrefix === ' where ') {
$whereClausePrefix === ' and ';
}
}
echo $conditions;
}
you also need to move $whereClause variable to outside of your loop. because you reinitialize it to 'where' after set it to 'and'
Please, follow below code and add or/and condition depend on your requirement in
if ($conditions != '') {
$conditions = $conditions . " or ";
}
Modified code at below and you will try:
Where try to add all conditions with Or and at last concatenation with where clause.
if (!empty($postData->searchTxt)) {
$nameArray = preg_split ("/[\s,]+/", $postData->searchTxt);
$countNames =count($nameArray);
foreach($nameArray as $index => $code ) {
if ($conditions != '') {
$conditions = $conditions . " or ";
}
$conditions = $conditions . " ((ex.name like '%".$code."%') or ex.surname like '%".$code."%' or u.user_email like '%".$code."%') ";
}
if ($conditions != '') {
$conditions = ' where ' . $conditions;
}
echo $conditions;
}
Just to add to the previous answer. Adding the $conditions again allowed the text to be loop through depending on the number of values in the array. In addition the if statement checking if the $conditions is not now was reset to and to help add more additional statements to my first array values search parameter. Find below the updated code. Hope this helps others out there too.
if (!empty($postData->searchTxt)) {
$nameArray = preg_split ("/[\s,]+/", $postData->searchTxt);
foreach($nameArray as $index => $code ) {
if ($conditions != '') {
$whereClausePrefix = ' and ';
}
$conditions = $conditions ." ". $whereClausePrefix." (ex.name like '%".$code."%' or ex.surname like '%".$code."%' or u.user_email like '%".$code."%') ";
}
}
echo $conditions;
I have an HTML-table, where various selections can be made. The selected variables that contain the respective values, build the array $data[]. Now, with this array I would like to make an SQL request where all selected criteria should be met. This means, that I need the following request:
SELECT * FROM fruitgroups
WHERE $selection[1] = $value[1]
AND $selection[2] = $value[2]
AND $selection[3] = $value[3]
etc ...
Can anybody please help me with the loop that generates exactly the string:
...
$selection[1] = $value[1]
AND $selection[2] = $value[2]
AND $selection[3] = $value[3]
... etc ...
...that I need for the request?
Thank you in advance!
You can make a SQL request like this:
$selection = array("one", "two", "three");
$value = array("Tone", "Ttwo", "Tthree");
$concat = array();
foreach($selection as $key => $var){
$new = $selection[$key] . " = " . $value[$key];
array_push($concat, $new);
}
$concat = implode(" AND ", $concat);
$request = 'SELECT * FROM fruitgroups WHERE ' . $concat . ';';
echo $request;
Run example
Similar to the answer above, but keep it simple and don't forget the single quotes around the values:
$clauses = [];
foreach ($values as $i => $value) {
$conditions[] = "{$selection[$i]} = '$value'";
}
$query = "SELECT * FROM fruitgroups WHERE " . implode(' AND ', $conditions);
Even better, use an ORM like Eloquent:
$conditions = [];
foreach ($values as $i => $value) {
$conditions[$i] = $value;
}
$result = App\FruitGroups::where($conditions)->get();
I'm assuming you are sanitizing your inputs first, of course.
Here is my function.
I want to simplify this function.
Any one help me please?
public function showData($table,$fields,$values)
{
$first = true;
$whereClause=null;
foreach($fields as $key => $value)
{
if($first)
{
$whereClause .= " WHERE $value = '$values[$key]'";
$first = false;
}
else
{
$whereClause .= " AND $value = '$values[$key]'";
}
}
$sql = "SELECT * FROM $table $whereClause";
$q = $this->conn->prepare($sql) or die("failed!");
$q->execute();
while ($r = $q->fetch(PDO::FETCH_ASSOC))
{
$data[] = $r;
}
return $data;
}
foreach($ob->showData($tablenm,$field,$val) as $roleval)
{
//Do Something
}
Any other way to simplify this function.
Help me please.
public function query($sql, $params = NULL)
{
$stmt = $this->conn->prepare($sql);
$stmt->execute($params)
return $stmt;
}
$data = $ob->query("SELECT * FROM table WHERE foo = ? AND bar = ?", [$foo, $bar]);
foreach($data as $roleval)
{
//Do Something
}
This function is way more simpler, way more powerful and way more flexible than yours. Put aside that yours is essentially and irrecoverably prone to SQL injection, just mocking a prepared statement but not using it really.
You have to understand that keywords in SQL serve for the great purpose of readability, makes whole statement readable and unambiguous, comprehensible by the every programmer in the world. And so you can tell that your initial idea to save yourself typing of SELECT or WHERE turns to be not that brilliant.
Besides, PDO supports dozens of different return formats, while you are limiting yourself with only one.
You may read further in my article Your first database wrapper's childhood diseases
function showData($table, $fields, $values) {
if(!(is_array($fields) && is_array($values) ) || count($fields) !== count($values))
throw new Exception('Arguments error: "fields" and "values" must be arrays with equal number of elements.');
foreach ($fields as $key => &$field)
$field = '`' . str_replace('`', '``', $field) . '` = ' . $this->conn->quote($values[$key]);
return 'SELECT * FROM `' . str_replace('`', '``', $table) . (empty($fields) ? '`' : '` WHERE ' . implode(' AND ', $fields)) . ';';
}
test case:
echo showData('table`name', ['col`1', 'col\`2', 'col\\`3'], ["Tom's cat 1", "Tom's cat 2", "Tom's cat 3"]);
#output: SELECT * FROM `table``name` WHERE `col``1` = 'Tom\'s cat 1' AND `col\``2` = 'Tom\'s cat 2' AND `col\``3` = 'Tom\'s cat 3';
Of course you will execute the SQL instead of returning it as test output.
So I'm trying to create a function that generates a SQL query string based on a multi dimensional array.
Example:
function createQueryString($arrayToSelect, $table, $conditionalArray) {
$queryStr = "SELECT ".implode(", ", $arrayToSelect)." FROM ".$table." WHERE ";
$queryStr = $queryStr.implode(" AND ",$conditionalArray); /*NEED HELP HERE*/
return $queryStr;
}
$columnsToSelect = array('ID','username');
$table = 'table';
$conditions = array('lastname'=>'doe','zipcode'=>'12345');
echo createQueryString($columnsToSelect, $table, $conditions); /*will result in incorrect SQL syntax*/
as you can see I need help with the 3rd line as it's currently printing
SELECT ID, username FROM table WHERE
lastname AND zipcode
but it should be printing
SELECT ID, username FROM table WHERE
lastname = 'doe' AND zipcode = '12345'
You're not actually imploding a multidimensional array. $conditions is an associative array.
Just use a foreach loop inside your function createQueryString(). Something like this should work, note it's untested.:
$terms = count($conditionalArray);
foreach ($conditionalArray as $field => $value)
{
$terms--;
$queryStr .= $field . ' = ' . $value;
if ($terms)
{
$queryStr .= ' AND ';
}
}
Note: To prevent SQL injection, the values should be escaped and/or quoted as appropriate/necessary for the DB employed. Don't just copy and paste; think!
function implodeItem(&$item, $key) // Note the &$item
{
$item = $key . "=" . $item;
}
[...]
$conditionals = array(
"foo" => "bar"
);
array_walk($conditionals, "implodeItem");
implode(' AND ', $conditionals);
Untested, but something like this should work. This way you can also check if $item is an array and use IN for those cases.
You will have to write another function to process the $conditionalArray, i.e. processing the $key => $value and handling the types, e.g. applying quotes if they're string.
Are you just dealing with = condition? What about LIKE, <, >?
Forgive me if its not too sexy !
$data = array('name'=>'xzy',
'zip'=>'3432',
'city'=>'NYK',
'state'=>'Alaska');
$x=preg_replace('/^(.*)$/e', ' "$1=\'". $data["$1"]."\'" ',array_flip($data));
$x=implode(' AND ' , $x);
So the output will be sth like :
name='xzy' AND zip='3432' AND city='NYK' AND state='Alaska'
I'd advise against automated conditionals creation.
Your case is too local, while there can be many other operators - LIKE, IN, BETWEEN, <, > etc.
Some logic including several ANDs and ORs.
The best way is manual way.
I am always doing such things this way
if (!empty($_GET['rooms'])) $w[]="rooms='".mesc($_GET['rooms'])."'";
if (!empty($_GET['space'])) $w[]="space='".mesc($_GET['space'])."'";
if (!empty($_GET['max_price'])) $w[]="price < '".mesc($_GET['max_price'])."'";
Though if you still want it with this simple array, just iterate it using
foreach ($conditions as $fieldname => $value)...
and then combine these variables in the way you need. you have 2 options: make another array of this with field='value' pairs and then implode it, or just concatenate, and substr trailing AND at the end.
I use a variation of this:
function implode_assoc($glue,$sep,$arr)
{
if (empty($glue)) {$glue='; ';}
if (empty($sep)) {$sep=' = ';}
if (is_array($arr))
{
foreach ($arr as $k=>$v)
{
$str .= $k.$sep.$v.$glue;
}
return $str;
} else {
return false;
}
};
It's rough but works.
Here is a working version:
//use: implode_assoc($v,"="," / ")
//changed: argument order, when passing to function, and in function
//output: $_FILES array ... name=order_btn.jpg / type=image/jpeg / tmp_name=G:\wamp\tmp\phpBDC9.tmp / error=0 / size=0 /
function implode_assoc($arr,$glue,$sep){
$str = '';
if (empty($glue)) {$glue='; ';}
if (empty($sep)) {$sep=' = ';}
if (is_array($arr))
{
foreach ($arr as $key=>$value)
{
$str .= $key.$glue.$value.$sep;
}
return $str;
} else {
return false;
}
}
I know this is for the case of a pdo mysql type.. but what i do is build pdo wrapper methods, and in this case i do this function that helps to build the string, since we work with keys, there is no possible way to mysql inject, since i know the keys i define / accept manually.
imagine this data:
$data=array(
"name"=>$_GET["name"],
"email"=>$_GET["email"]
);
you defined utils methods...
public static function serialize_type($obj,$mode){
$d2="";
if($mode=="insert"){
$d2.=" (".implode(",",array_keys($obj)).") ";
$d2.=" VALUES(";
foreach ($obj as $key=>$item){$d2.=":".$key.",";}
$d2=rtrim($d2,",").")";}
if($mode=="update"){
foreach ($obj as $key=>$item){$d2.=$key."=:".$key.",";}
}
return rtrim($d2,",");
}
then the query bind array builder ( i could use direct array reference but lets simplify):
public static function bind_build($array){
$query_array=$array;
foreach ($query_array as $key => $value) { $query_array[":".$key] = $query_array[$key]; unset($query_array[$key]); } //auto prepair array for PDO
return $query_array; }
then you execute...
$query ="insert into table_x ".self::serialize_type( $data, "insert" );
$me->statement = #$me->dbh->prepare( $query );
$me->result=$me->statement->execute( self::bind_build($data) );
You could also go for an update easy with...
$query ="update table_x set ".self::serialize_type( $data, "update" )." where id=:id";
$me->statement = #$me->dbh->prepare( $query );
$data["id"]="123"; //add the id
$me->result=$me->statement->execute( self::bind_build($data) );
But the most important part here is the serialize_type function
Try this
function GeraSQL($funcao, $tabela, $chave, $valor, $campos) {
$SQL = '';
if ($funcao == 'UPDATE') :
//Formata SQL UPDATE
$SQL = "UPDATE $tabela SET ";
foreach ($campos as $campo => $valor) :
$SQL .= "$campo = '$valor', ";
endforeach;
$SQL = substr($SQL, 0, -2);
$SQL .= " WHERE $chave = '$valor' ";
elseif ($funcao == 'INSERT') :
//Formata SQL INSERT
$SQL = "INSERT INTO $tabela ";
$SQL .= "(" . implode(", ", array_keys($campos) ) . ")";
$SQL .= " VALUES ('" . implode("', '", $campos) . "')";
endif;
return $SQL;
}
//Use
$data = array('NAME' => 'JOHN', 'EMAIL' => 'J#GMAIL.COM');
GeraSQL('INSERT', 'Customers', 'CustID', 1000, $data);