What is the best way to pass a variable number of arguments to a php function? I mean, suppose i have the following:
function my_func($a, $b, $c) {
$q = 'SELECT ' . $a . ' FROM ' . $b . ' WHERE status IS NULL';
}
my_func('id', 'table');
my_func('id', 'table', ' AND x = 1');
I've read about func_get_arg(), but if i call func_get_arg(2) in the first situation, i will get a, Argument 2 not passed to function error.
IMPORTANT: this query is not performed with user-passed arguments, so no injection hazzards! It is performed with controlled arguments given by me and its function is to check if that value is valid within a combination of foreign keys! So please no sarcastic 'injection paradise' comments, thank you.
Well i do not know if it's best, but i like to pass the array as argument and then work with it in my function. Here is one example:
function my_query($query = array())
{
// select and from are required to exist
if(!empty($query) && array_key_exists('select', $query) && array_key_exists('from', $query))
{
$q = "select {$query['select']}";
$q .= " from {$query['from']}";
foreach($query as $key => $val)
{
// Don't want to include select and from once again (also do not unset before in case need to run all of this once again)
if($key != 'select' && $key != 'from')
{
// Search if key has underscore and replace it with space for valid query
if(strpos($key, '_') !== false)
$key = str_replace('_', ' ', $key);
// Build query with spaces and all
$q .= " " . $key . " " . $val;
}
}
// Run query here using $q
}
}
And you can pass in array as you like:
$query = array(
'select' => '*',
'from' => 'users',
'where' => 'age > 25',
'order by' => 'id'
);
// Or
$query = array();
$query['select'] = '*';
$query['from'] = 'users';
$query['where'] = 'age > 25';
$query['order_by'] = 'id';
my_query($query);
// Would return us something like this
string(46) "select * from users where age > 25 order by id"
But using this you have to maintain right order in your array or write ordering and validation code in your function.
Since you have mentioned that your function does not deal with user-passed arguments.. I am suggesting this..
FYI : I just used an echo inside that for demonstration purposes.. you can change that later.
<?php
function my_func() {
echo $q = 'SELECT ' . func_get_arg(0) . ' FROM ' . func_get_arg(1) . ' WHERE status IS NULL';
}
my_func('id', 'table');
The above displays...
SELECT id FROM table WHERE status IS NULL
The arguments start from 0 index, so you should probably do.. func_get_arg(1) to get the second argument.
I have a function that builds a MySQL query from the supplied arguments. My current code is:
($args can be an empty array or up to a set of 5 field_names=>array_of_ids ...)
if( !(empty( $args )) )
{
$flag = 0;
$sql_append = '';
foreach( $args as $field_name => $id_array )
{
if( $flag == 0 )
{
$where_connector = " WHERE ";
$flag = 1;
}
else
{
$where_connector = " AND ";
}
${ $field_name . '_string'} = join(',',${ $field_name . '_ids'});
$sql_append .= $where_connector . 'link_id IN ($ids)";
}
}
I'm self-taught and so constantly worry about best practices. I seem to remember some sort of function that handles arguments, perhaps in a way that can be applied here more efficiently. Any ideas?
To neatly construct your WHERE $fieldname IN($ids) clauses from a $fieldname=>$id_array array, you can try this :)
function buildWhereIns(array $args)
{
if(!is_array($args) || empty($args)) return "";
$ids = array_map(function($item){
return implode(',',$item);
}, array_values($args));
$fields = array_map(function($item,$id){
return $item.' IN('.$id.') ';
}, array_keys($args),$ids);
return = count($fields) > 0 ? count($fields) > 1 ? " WHERE " . implode(' AND ', $fields) : " WHERE " . array_shift($fields) : "";
}
I would say that the larger issue here is that you should be using some sort of technique to protect your code against SQL injection. For example, PHP's built-in PDO classes provide a really easy way to do this: http://www.php.net/manual/en/pdo.prepared-statements.php.
In general, though, if you want a loop that behaves differently on the first or last iteration, your method isn't bad. The other obvious method is to just do the first (or last) iteration outside the loop, and then perform the iterations which are the same inside the loop body.
I managed to print a string using __toString() magic method, but in this string I see placeholders (for conditions params), and it doesn't work as SQL query.
I checked documentation of this object, and also looked in google, but couldn't find a working answer.
Basing on question's comments (thanks #Scuzzy for inspiration) I wrote some simple piece of code to convert SelectQuery object:
class ExportableSelectQuery {
public static function toSql(SelectQuery $obj) {
$_string = $obj->__toString();
$_conditions = $obj->conditions();
$_tables = $obj->getTables();
$_fields = $obj->getFields();
foreach($_tables as $k => $t) {
if(!empty($t['alias'])) {
$_string = str_replace('{' . $t['table'] . '}', $t['table'] . ' as', $_string);
}
else {
$_string = str_replace('{' . $t['table'] . '}', $t['table'], $_string);
}
}
foreach($_conditions as $k => $c) {
if(is_int($c['value'])) {
$_string = str_replace(':db_condition_placeholder_' . $k, $c['value'], $_string);
}
else {
$_string = str_replace(':db_condition_placeholder_' . $k, "'" . $c['value'] . "'", $_string);
}
}
//echo('<pre>');
//var_dump($_fields);
//var_dump($_conditions);
//var_dump($_tables);
//var_dump($_string);
//echo('</pre>');
//die();
return $_string;
}
}
Usage of this code is now simple (if you only have SelectQuery object somewhere):
die(ExportableSelectQuery::toSql($query));
I was thinking about extending original SelectQuery object, and provide method to get SQL code, but Drupal's db_select function returns SelectQuery, so I will have to either change db_select function or cast returned object to ExportableSelectQuery.
Also this is not probably best solution I could write, but assuming limit of time and purpose it solved my problem just fine.
If you wish to get SQL from for example "EntityFieldQyery", you may use something like this
Add tag to query
$query->entityCondition('entity_type', 'node')
->entityCondition('bundle', 'page')
->addTag('EFQDumper'); //<=== TAG
Implement hook "query_TAG_alter"
function YOURMODULE_query_EFQDumper_alter(QueryAlterableInterface $query)
{
//echo ExportableSelectQuery::toSql($query);
//die();
}
The solution based on Carlos comment
I am using a dataHandler library to handle all of my db inserts / updates, etc.
The library has the following functions:
function prepareValue($value, $connection){
$preparedValue = $value;
if(is_null($value)){
$preparedValue = 'NULL';
}
else{
$preparedValue = '\''.mysql_real_escape_string($value, $connection).'\'';
}
return $preparedValue;
}
function parseParams($params, $type, $connection){
$fields = "";
$values = "";
if ($type == "UPDATE"){
$return = "";
foreach ($params as $key => $value){
if ($return == ""){
if (preg_match("/\)$/", $value)){
$return = $key."=".$value;
}
else{
$return = $key."=".$this->prepareValue($value, $connection);
}
}
else{
if (preg_match("/\)$/", $value)){
$return = $return.", ".$key."=".$value;
}
else{
$return = $return.", ".$key."=".$this->prepareValue($value,
$connection);
}
}
}
return $return;
/* rest of function contains similar but for "INSERT", etc.
}
These functions are then used to build queries using sprintf, as in:
$query = sprintf("UPDATE table SET " .
$this->parseParams($params, "UPDATE", $conn) .
" WHERE fieldValue = %s;", $this->prepareValue($thesis_id, $conn));
$params is an associative array: array("db_field_name"=>$value, "db_field_name2"=>$value2, etc.)
I am now running into problems when I want to do an update or insert of a string that ends in ")" because the parseParams function does not put these values in quotes.
My question is this:
Why would this library NOT call prepareValue on strings that end in a closed parenthesis? Would calling mysql_real_escape_string() on this value cause any problems? I could easily modify the library, but I am assuming there is a reason the author handled this particular regex this way. I just can't figure out what that reason is! And I'm hesitant to make any modifications until I understand the reasoning behind what is here.
Thanks for your help!
Please note that inside prepareValue not only mysql_real_escape_string is applied to the value but it is also put inside '. With this in mind, we could suspect that author assumed all strings ending with ) to be mysql function calls, ie:
$params = array(
'field1' => "John Doe",
'field2' => "CONCAT('John',' ','Doe')",
'field3' => "NOW()"
);
Thats the only reasonable answer that comes to mind.
I've the following method which allows me to protect MySQL entities:
public function Tick($string)
{
$string = explode('.', str_replace('`', '', $string));
foreach ($string as $key => $value)
{
if ($value != '*')
{
$string[$key] = '`' . trim($value) . '`';
}
}
return implode('.', $string);
}
This works fairly well for the use that I make of it.
It protects database, table, field names and even the * operator, however now I also want it to protect function calls, ie:
AVG(database.employees.salary)
Should become:
AVG(`database`.`employees`.`salary`) and not `AVG(database`.`employees`.`salary)`
How should I go about this? Should I use regular expressions?
Also, how can I support more advanced stuff, from:
MAX(AVG(database.table.field1), MAX(database.table.field2))
To:
MAX(AVG(`database`.`table`.`field1`), MAX(`database`.`table`.`field2`))
Please keep in mind that I want to keep this method as simple/fast as possible, since it pretty much iterates over all the entity names in my database.
If this is quoting parts of an SQL statement, and they have only complexity that you descibe, a RegEx is a great approach. On the other hand, if you need to do this to full SQL statements, or simply more complicated components of statements (such as "MAX(AVG(val),MAX(val2))"), you will need to tokenize or parse the string and have a more sophisticated understanding of it to do this quoting accurately.
Given the regular expression approach, you may find it easier to break the function name out as one step, and then use your current code to quote the database/table/column names. This can be done in one RE, but it will be tricker to get right.
Either way, I'd highly recommend writing a few unit test cases. In fact, this is an ideal situation for this approach: it's easy to write the tests, you have some existing cases that work (which you don't want to break), and you have just one more case to add.
Your test can start as simply as:
assert '`ticked`' == Tick('ticked');
assert '`table`.`ticked`' == Tick('table.ticked');
assert 'db`.`table`.`ticked`' == Tick('db.table.ticked');
And then add:
assert 'FN(`ticked`)' == Tick('FN(ticked)');
etc.
Using the test case ndp gave I created a regex to do the hard work for you. The following regex will replace all word boundaries around words that are not followed by an opening parenthesis.
\b(\w+)\b(?!\()
The Tick() functionality would then be implemented in PHP as follows:
function Tick($string)
{
return preg_replace( '/\b(\w+)\b(?!\()/', '`\1`', $string );
}
It's generally a bad idea to pass the whole SQL to the function. That way, you'll always find a case when it doesn't work, unless you fully parse the SQL syntax.
Put the ticks to the names on some previous abstraction level, which makes up the SQL.
Before you explode your string on periods, check if the last character is a parenthesis. If so, this call is a function.
<?php
$string = str_replace('`', '', $string)
$function = "";
if (substr($string,-1) == ")") {
// Strip off function call first
$opening = strpos($string, "(");
$function = substr($string, 0, $opening+1);
$string = substr($string, $opening+1, -1);
}
// Do your existing parsing to $string
if ($function == "") {
// Put function back on string
$string = $function . $string . ")";
}
?>
If you need to cover more advanced situations, like using nested functions, or multiple functions in sequence in one "$string" variable, this would become a much more advanced function, and you'd best ask yourself why these elements aren't being properly ticked in the first place, and not need any further parsing.
EDIT: Updating for nested functions, as per original post edit
To have the above function deal with multiple nested functions, you likely need something that will 'unwrap' your nested functions. I haven't tested this, but the following function might get you on the right track.
<?php
function unwrap($str) {
$pos = strpos($str, "(");
if ($pos === false) return $str; // There's no function call here
$last_close = 0;
$cur_offset = 0; // Start at the beginning
while ($cur_offset <= strlen($str)) {
$first_close = strpos($str, ")", $offset); // Find first deep function
$pos = strrpos($str, "(", $first_close-1); // Find associated opening
if ($pos > $last_close) {
// This function is entirely after the previous function
$ticked = Tick(substr($str, $pos+1, $first_close-$pos)); // Tick the string inside
$str = substr($str, 0, $pos)."{".$ticked."}".substr($str,$first_close); // Replace parenthesis by curly braces temporarily
$first_close += strlen($ticked)-($first_close-$pos); // Shift parenthesis location due to new ticks being added
} else {
// This function wraps other functions; don't tick it
$str = substr($str, 0, $pos)."{".substr($str,$pos+1, $first_close-$pos)."}".substr($str,$first_close);
}
$last_close = $first_close;
$offset = $first_close+1;
}
// Replace the curly braces with parenthesis again
$str = str_replace(array("{","}"), array("(",")"), $str);
}
If you are adding the function calls in your code, as opposed to passing them in through a string-only interface, you can replace the string parsing with type checking:
function Tick($value) {
if (is_object($value)) {
$result = $value->value;
} else {
$result = '`'.str_replace(array('`', '.'), array('', '`.`'), $value).'`';
}
return $result;
}
class SqlFunction {
var $value;
function SqlFunction($function, $params) {
$sane = implode(', ', array_map('Tick', $params));
$this->value = "$function($sane)";
}
}
function Maximum($column) {
return new SqlFunction('MAX', array($column));
}
function Avg($column) {
return new SqlFunction('AVG', array($column));
}
function Greatest() {
$params = func_get_args();
return new SqlFunction('GREATEST', $params);
}
$cases = array(
"'simple'" => Tick('simple'),
"'table.field'" => Tick('table.field'),
"'table.*'" => Tick('table.*'),
"'evil`hack'" => Tick('evil`hack'),
"Avg('database.table.field')" => Tick(Avg('database.table.field')),
"Greatest(Avg('table.field1'), Maximum('table.field2'))" => Tick(Greatest(Avg('table.field1'), Maximum('table.field2'))),
);
echo "<table>";
foreach ($cases as $case => $result) {
echo "<tr><td>$case</td><td>$result</td></tr>";
}
echo "</table>";
This avoids any possible SQL injection while remaining legible to future readers of your code.
You could use preg_replace_callback() in conjunction with your Tick() method to skip at least one level of parens:
public function tick($str)
{
return preg_replace_callback('/[^()]*/', array($this, '_tick_replace_callback'), $str);
}
protected function _tick_replace_callback($str) {
$string = explode('.', str_replace('`', '', $string));
foreach ($string as $key => $value)
{
if ($value != '*')
{
$string[$key] = '`' . trim($value) . '`';
}
}
return implode('.', $string);
}
Are you generating the SQL Query or is it being passed to you? If you generating the query I wouldn't pass the whole query string just the parms/values you want to wrap in the backticks or what ever else you need.
EXAMPLE:
function addTick($var) {
return '`' . $var . '`';
}
$condition = addTick($condition);
$SQL = 'SELECT' . $what . '
FROM ' . $table . '
WHERE ' . $condition . ' = ' . $constraint;
This is just a mock but you get the idea that you can pass or loop through your code and build the query string rather than parsing the query string and adding your backticks.