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
Related
I'm writing PHP method to pretty-print callstack with params. Reason for this is it will be used as an output of a public API (in debug mode) so it must not display everything and only display save information.
I would like to see something like this:
Config->saveToFile(resource: file) in config.php::456
Config->backup('config.bak') in config.php::123
But when I call debug_backtrace() and parse the args value, I cannot use methods gettype(), is_resource() and get_resource_type() because it always say the variable is of unknown type:
Config->saveToFile(Unknown type: Resource id #99) in config.php::456
Config->backup('config.bak') in config.php::123
Code used to parse args is:
public static function getTrace() {
$trace = debug_backtrace();
$output = [];
foreach ($trace as $call) {
$name = $call['class'] . $call['type'] . $call['function']
//actual code checks for various situations
$args = [];
foreach ($call['args'] as $arg) {
$args[] = self::toString($arg);
}
$name .= '(' . join(', ', $args) . ')';
$output[] = $name . ' in ' . basename($call['file']) . '::' . $call['line'];
}
return $output;
}
protected static function toString($mixed) {
//process known types - arrays, objects, strings, etc.
//...
if (is_resource($mixed)) {
return 'resource: ' . get_resource_type($mixed);
}
return gettype($mixed) . ': ' . $mixed;
}
Even when I use code by diz at ysagoon dot com listed under debug_backtrace documentation, which utilize gettype() and check for resource, in my case it returns Config->saveToFile(Unknown).
When I use the methods in code where the resource is created, it correctly returns its type.
Is there a limit or reason why resources are not identified from backtrace? Something I should enable in PHP configuration? I haven't found anything about this in PHP documentation nor Google.
System:
XAMPP 3.2.2
Apache/2.4.17 (Win32)
PHP/5.6.15
Windows 10 Pro x64 Anniversary edition 1607 (10.0.14393)
So the problem is that resources can be identified as a resource only while they are opened. After you close the resource, it is no more identified by methods gettype(), is_resource() and get_resource_type() as a resource and instead change to unknown type.
$f = fopen('tmp', 'w');
echo gettype($f); //= 'resource'
fclose($f);
echo gettype($f); //= 'Unknown type'
To print closed resources in backtrace I've created two methods to remember resources while they are still opened:
protected $resources = [];
public function traceResourceParams() {
$trace = debug_backtrace();
$args = [];
foreach ($trace as $call) {
foreach ($call['args'] as $arg) {
if (is_resource($arg) && !array_key_exists(intval($arg), $this->resources)) {
$this->resources[intval($arg)] = self::toString($arg);
}
}
}
}
public function traceNamedResource($resource, $name) {
if (is_resource($resource)) {
$this->resources[intval($resource)] = '{' . get_resource_type($resource) . ': ' . $name . '}';
}
}
And updated my toString method to check for stored resources:
protected static function toString($mixed) {
//process known types - arrays, objects, strings, etc.
//...
if (is_resource($mixed)) {
return 'resource: ' . get_resource_type($mixed);
}
//closed resources does not evaluate as resource
//but still convert to resource id using intval()
//so we can match them to previously evaluated resources
$val = intval($mixed);
if ($val && array_key_exists($val, self::getInstance()->resources)) {
return self::getInstance()->resources[$val];
}
return gettype($mixed) . ': ' . $mixed;
}
So now I can store the resource when it is created:
$f = fopen('tmp', 'w');
$debug->traceNamedResource($f, 'tmp');
fclose($f);
Or when it is passed as a parameter:
protected function saveToFile($file) {
$debug->traceResourceParams()
//... work with file
fclose($file);
}
What can i do if i want to something like following using extbase model query (Using Object Relational Model).
SELECT uid FROM table WHERE fIND_IN_SET('4',column_name);
OR something like
SELECT SUM(column_name) FROM table WHERE 1
Note : I don't want to use custom query using statement() method
If you specified why you don't "want" to use a custom statement you would realize that you are mistaken. Your situation is the exact reason why statement() exists in the first place. Trying to avoid using it at almost any cost is unreasonable as the SQL abilities of extbase are far from covering all use cases. So in summary: use extbase API where logical and an obvious good choice but don't shy away from statement() where it's use is the obvious best choice just because usign extbase API "looks nicer" or it seems like with the API you're "using the framework to it's full potential".
As of now, there is neither an equivalent for the FIND_IN_SET function, nor for aggregate functions.
You will only get around this only with a custom statement or by writing your own extension of the TYPO3\CMS\Extbase\Persistence\Generic\Query class. There are, of course, security considerations which you'll have to mind with a custom statement(). However, the same security implications apply with the class extension.
To be more specific on the FIND_IN_SET function: You can't just use the like() method. If you're searching for id 1, for example, you would find it in a set that consists of any of 10,11,12,13 and so on. Furthermore can't get around that problem either because the like() method only accepts property names and doesn't let you wrap column names in functions.
According to your question: I don't think there is any way to use mysql functions in extbase without using statement().
According to your example: You can try $query->like('columnName', "%{4}%").
This is possible but needs a bit effort. I have done that in my news extension. The code looks like that
$query = // your $query->execute();
$queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
list($hash, $parameters) = $queryParser->preparseQuery($query);
$statementParts = $queryParser->parseQuery($query);
// Limit and offset are not cached to allow caching of pagebrowser queries.
$statementParts['limit'] = ((int)$query->getLimit() ?: null);
$statementParts['offset'] = ((int)$query->getOffset() ?: null);
$tableNameForEscape = (reset($statementParts['tables']) ?: 'foo');
foreach ($parameters as $parameterPlaceholder => $parameter) {
if ($parameter instanceof LazyLoadingProxy) {
$parameter = $parameter->_loadRealInstance();
}
if ($parameter instanceof \DateTime) {
$parameter = $parameter->format('U');
} elseif ($parameter instanceof DomainObjectInterface) {
$parameter = (int)$parameter->getUid();
} elseif (is_array($parameter)) {
$subParameters = [];
foreach ($parameter as $subParameter) {
$subParameters[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($subParameter, $tableNameForEscape);
}
$parameter = implode(',', $subParameters);
} elseif ($parameter === null) {
$parameter = 'NULL';
} elseif (is_bool($parameter)) {
return ($parameter === true ? 1 : 0);
} else {
$parameter = $GLOBALS['TYPO3_DB']->fullQuoteStr((string)$parameter, $tableNameForEscape);
}
$statementParts['where'] = str_replace($parameterPlaceholder, $parameter, $statementParts['where']);
}
$statementParts = [
'selectFields' => implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']),
'fromTable' => implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']),
'whereClause' => (!empty($statementParts['where']) ? implode('', $statementParts['where']) : '1')
. (!empty($statementParts['additionalWhereClause'])
? ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'])
: ''
),
'orderBy' => (!empty($statementParts['orderings']) ? implode(', ', $statementParts['orderings']) : ''),
'limit' => ($statementParts['offset'] ? $statementParts['offset'] . ', ' : '')
. ($statementParts['limit'] ? $statementParts['limit'] : '')
];
$sql = $GLOBALS['TYPO3_DB']->SELECTquery(
$statementParts['selectFields'],
$statementParts['fromTable'],
$statementParts['whereClause'],
'',
$statementParts['orderBy'],
$statementParts['limit']
);
return $sql;
By using the DataMapper you can map the raw result back to models if you need that as well. The code for that looks like that
$dataMapper = $objectManager->get(DataMapper::class);
$records = $dataMapper->map($className, $rows);
I have two way for show result in/using html tags:
first way :(add html into function)
function _is_comments_($id,$type){
$db = mysqli_access::F("SELECT message,timestamp FROM " . COMMENTS . " WHERE pid = ? AND type = ? AND approved = 1 ", $id, $type);
foreach($db as $row){
$commentdata = '<p>'.$row['message'].'-'.$row['timestamp'].'</p>';
}
return $commentdata;
}
result:
echo _is_comments_('125','article');
second way:(separated html from function)
function _is_comments_($id,$type){
$db = mysqli_access::F("SELECT message,timestamp FROM " . COMMENTS . " WHERE pid = ? AND type = ? AND approved = 1 ", $id, $type);
foreach($db as $row){
$commentdata[] = $row;
}
return $commentdata;
}
in result(again foreach loop):
<?php
$comments_list = _is_comments_('125','article');
foreach($comments_list as $row){ ?>
<p><?php echo $row['message'].'-'.$row['timestamp'];?></p>
<?php
}
?>
which way is better and faster?
It is always a good design practice to keep concerns separated. So the second is the best option because it limits the the responsibility of the function to just accessing data.
With that in mind, you may consider passing a function as a third argument to the function that will carry the formatting. This is called the decorator pattern.
function _is_comments_($id,$type, $decorator){
$db = mysqli_access::F("SELECT message,timestamp FROM " . COMMENTS . " WHERE pid = ? AND type = ? AND approved = 1 ", $id, $type);
foreach($db as $row){
$commentdata[] = $decorator($row);
}
return $commentdata;
}
_is_comments_($id,$type, function($item){
return "<span>$item</span>";
});
You can create as many anonymous function decorator as you want and it makes the original function very powerful and extremely flexible.
Though both links above refer to objects, the same principles can, and are, applied to functions. I've used both patterns together in the past with great success.
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.