For PHP testing scripts I need a function report() that will report results of some expressions, like:
report(in_array(null, $array));
report(in_array(false, $array));
# etc...
The output should look like:
in_array(null, $array) => false
in_array(false, $array) => true
So I want to print the expression along with the result. Thus in the report function I need some means how to print the expression which was given by the caller:
function report($expr)
{
SOME_FUNCTION($expr)
# function I'm looking for!!
# function which would write the string 'in_array(null, $array)' to output!
echo " => ";
echo $expr;
echo "<br>";
}
Is there any such function that would dump the expression as given by the caller?
I know this can't be "normal" function, this would need to be somehow bound to PHP internals. But if there are magic things like debug_print_backtrace(), __FUNCTION__ or __LINE__, then I think there still can be some chance...
Directly, no there is no clean method to do what you want.
With that said, you could use debug_backtrace to get the stack. Then, all you need to do is walk the stack back one (go to the 2nd array element), and you have the file and line information. Then, you'd need to parse that line to extract the function name and the inputs.
It wouldn't be clean. It wouldn't likely be easy. And it wouldn't be 100% reliable. But it should work for most cases...
here's a simple case of a utility function to do that. Just call it with the function name, and it'll give you the literal caller of the function:
function getArgs($func) {
$d = debug_backtrace();
$call = $d[1];
$file = file($call['file']);
$line = $file[$call['line'] - 1];
if (preg_match('(' . preg_quote($func, '(') . '\((.*)\);)', $line, $match)) {
return $match[1];
}
return $line;
}
And here's an example:
function doSomething($arg) {
$call = getArgs(__FUNCTION__);
echo $call . ' - ' . $arg;
}
doSomething(strlen('foo'));
Would output:
strlen('foo') - 3
Related
What is the best way to parse a string containing a function call with parameters in PHP so that I have the function name and the parameters with their correct types. Example:
$string = "ask('Do you want to continue?', ['yes', 'no'])";
I don't want to directly call that function, so eval is not an option. I want to do something based on the function name and use the parameters with their correct types. Is there an easy way in PHP?
I expect something like this as a result:
$name = 'ask';
$parameters = ['Do you want to continue?', ['yes', 'no']];
Assuming that you want the arguments to be parsed to an array structure, you would still need to use eval (with all the precautions taken to ensure that the content is safe).
This code also assumes the format is as expected, i.e. it represents a valid function call, and the closing parenthesis is the final non-blank character:
$string = "ask('Do you want to continue?', ['yes', 'no'])";
$parts = array_map("trim", explode("(", substr(trim($string), 0, -1), 2));
$parts[1] = eval("return [$parts[1]];");
$parts will be:
[
"ask",
[
"Do you want to continue?",
["yes", "no"]
]
]
I think you should use one good library to parse PHP code.
that is some example of that kind of library
use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
$code = <<<'CODE'
<?php
function test($foo)
{
var_dump($foo);
}
CODE;
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
$ast = $parser->parse($code);
} catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return;
}
$dumper = new NodeDumper;
echo $dumper->dump($ast) . "\n";
https://github.com/nikic/PHP-Parser
I have written a very simple translation class that is supposed to return the meaning associated with the phrase that I give to it. Under the hood, it loads translations from a csv upon construction into an associative array. Upon translation request, it checks the array. If the phrase is there as a key in the array, returns its value, which is its translation. If the phrase does not exist as a key, it loads the array from the file again (as there might be new translations), checks for the key again. If it does not find the key again, the phrase will be returned as is.
<?php
class Translate{
function __construct() {
$this->loadTranslations();
}
public function get($message, $lang = "de"): string{
if(key_exists($message, self::$de)){
return self::$de[$message];
}
else {
//Load translations again
$this->loadTranslations();
if(isset(self::$de[$message])){
return self::$de[$message];
}
else {
return $message;
}
}
}
protected static $de = [];
protected function loadTranslations() {
$file = fopen(__DIR__ . "/../data/de.csv", "r");
if($file){
while($line = fgets($file)){
$en_de = explode(":", $line);
self::$de[array_shift($en_de)] = array_shift($en_de);
}
}
fclose($file);
}
}
$t = new Translate();
echo $t->get("Hello") . PHP_EOL;
Content of de.csv is like this:
"Hi": "Hallo"
"Hello": "Hallo"
The problem is when asked for a translation, the class always returns the given phrase. When I dump the array, the phrase is there as a key, but there is no success in accessing $array[$phrase] as PHP does not find the key in the array!
The problem is that in your CSV file, you have quotes round the text, so although Hello exists, it's actually stored in the translation array as "Hello" so will not match.
You could either redo your translation file to not have the quotes, or you could use the functionality of fgetcsv() to read it and strip out any surrounding quotes (use : as the separator)...
protected function loadTranslations() {
$file = fopen(__DIR__ . "/a.csv", "r");
if($file){
while([$key, $trans] = fgetcsv($file, null, ":", '"')){
self::$de[$key] = $trans;
}
}
fclose($file);
}
Just looking at the code to fetch the translation, you could shorten it. First check that the translations are loaded, then return the translation - using ?? to say if it's not found, then return the original message...
public function get($message, $lang = "de"): string{
if(!isset(self::$de)){
$this->loadTranslations();
}
return self::$de[$message] ?? $message;
}
Your csv looks more like json to me.
I'd probably adjust the file to be json permanently, but until then, just convert it into a json string manually, then decode it to create your key-value pairs.
self::$de = json_decode(
'{' . implode(',', file(__DIR__ . "/a.csv")) . '}',
true
);
In other words, make all of your language files valid json. This way you can instantly cal json_decode() on the entire file contents and the array is ready. Keeping your file in the current format means individually isolating each line of text in the file and calling a function to parse it -- this is waaaaay too much work to be done each time.
Please consistently write your class variables at the top of your class.
$de should not be a variable name -- I am assuming it is referring to a specific language. $lang() should be used to specify the user's desired language and search for the appropriate filename.
Edit:
I really can't overstate how beneficial it is to convert your files to valid json -- it just makes everything cleaner. Here is a re-write of your code. I don't agree with the use of a static class variable, nor the constructor that that loads a language without know what is going to be used. And as previously mentioned there should be no variable that refers to a specific language ($de). The class variable $translations should be an associative array containing subarrays so that you can permanently load and access multiple translations at the same time.
Untested suggestion:
class Translate{
protected $translations = [];
protected function loadTranslations($lang)
{
$filePath = __DIR__ . '/' . $lang . '.json';
if (file_exists($filePath)) {
$this->translations[$lang] = json_decode(file_get_contents($filePath), true);
}
}
public function get($message, $lang = "de"): string
{
if (!isset($this->translations[$lang])) {
$this->loadTranslations($lang);
}
return $this->translations[$lang][$message] ?? $message;
}
// e.g. $newTrans = ['Good Day' => 'Guten Tag', ...]
public function set($lang, $newTrans)
{
if (!isset($this->translations[$lang])) {
$this->loadTranslations($lang);
}
$this->translations[$lang] += $newTrans; // insert or overwrite key-value pair(s)
file_put_contents(__DIR__ . '/' . $lang . '.json', json_encode($this->translations[$lang])); // commit to file
}
}
$t = new Translate();
echo $t->get("Hello") . PHP_EOL;
Suppose I have the following PHP code:
class Foo {
function getBar() {
return 1;
}
}
function check( Foo $foo ) {
if ( $foo->getBar() == 1 ) {
// here could be more code ...
return 'Oh no, there was an error in class' .
get_class( $foo ) . ', method ' .
'getBar';
}
}
The last string in check bothers me because if Foo::bar gets renamed by a refactoring tool, the error message will be wrong. Is there any way to get around this without using a string somewhere?
You can use __METHOD__ to get the name of the current method.
But to get reference to other method that would allow you some kind of automatic refactoring - no, it's not possible in php.
Can be done by using method_exists()
class Foo {
function getBar() {
return 1;
}
}
function check( Foo $foo , $method = 'getBar') {
if (!method_exists($foo, $method) ) {
// here could be more code ...
return 'Oh no, there was an error in class' .
get_class( $foo ) . ', method ' .
$method;
}
}
It is not possible in PHP per se, but you can implement such a feature. One possible implementation would work as follows: somewhere the file path, class name, method name and some kind of a description of where and what should match what. Your new feature whenever triggered would check the given files, check whether some values changed, fix whatever needs to be fixed and log a report about the task. It would not be simple to implement something like this, but, important to note is that there is a solution.
How do I execute the transaction(123) function?
The response via API is: transaction(123)
I store this in the $response varible.
<?php
function transaction($orderid) {
return $orderid;
}
//api response
$response = "transaction(123)";
try {
$orderid = call_user_func($response);
echo $orderid;
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
?>
According to the manual page call_user_func() should be called with two parameters in your use case.
$orderid = call_user_func('transaction', 123);
This means you must extract the function and parameter separately from your $response variable:
preg_match('/([\w\_\d]+)\(([\w\W]*)\)/', $response, $matches);
Would result in the $matches array containing the function name at index 1 and the parameter at index 2.
So you would then do:
$orderid = call_user_func($matches[1], $matches[2]);
Obviously you need to be very careful with the values if they are coming from an untrusted source.
The bad way to do it, is to use the eval() function. It's very bad in your use-case because the API may very well return things you don't want to execute.
The good way to do it is to parse your string, validate its contents, and map the call and its arguments accordingly.
You can parse the return string using a regular expression:
preg_match("/^(.+?)\((.*?)\)$/", $answer, $match);
var_dump($match[1]); // method
var_dump(explode(',', $match[2])); // arguments
You must sanitize/validate the above.
Call call_user_func this way:
$orderid = call_user_func('transaction', 123);
Additionally, take a look at http://es.php.net/manual/en/function.call-user-func.php
How are callbacks written in PHP?
The manual uses the terms "callback" and "callable" interchangeably, however, "callback" traditionally refers to a string or array value that acts like a function pointer, referencing a function or class method for future invocation. This has allowed some elements of functional programming since PHP 4. The flavors are:
$cb1 = 'someGlobalFunction';
$cb2 = ['ClassName', 'someStaticMethod'];
$cb3 = [$object, 'somePublicMethod'];
// this syntax is callable since PHP 5.2.3 but a string containing it
// cannot be called directly
$cb2 = 'ClassName::someStaticMethod';
$cb2(); // fatal error
// legacy syntax for PHP 4
$cb3 = array(&$object, 'somePublicMethod');
This is a safe way to use callable values in general:
if (is_callable($cb2)) {
// Autoloading will be invoked to load the class "ClassName" if it's not
// yet defined, and PHP will check that the class has a method
// "someStaticMethod". Note that is_callable() will NOT verify that the
// method can safely be executed in static context.
$returnValue = call_user_func($cb2, $arg1, $arg2);
}
Modern PHP versions allow the first three formats above to be invoked directly as $cb(). call_user_func and call_user_func_array support all the above.
See: http://php.net/manual/en/language.types.callable.php
Notes/Caveats:
If the function/class is namespaced, the string must contain the fully-qualified name. E.g. ['Vendor\Package\Foo', 'method']
call_user_func does not support passing non-objects by reference, so you can either use call_user_func_array or, in later PHP versions, save the callback to a var and use the direct syntax: $cb();
Objects with an __invoke() method (including anonymous functions) fall under the category "callable" and can be used the same way, but I personally don't associate these with the legacy "callback" term.
The legacy create_function() creates a global function and returns its name. It's a wrapper for eval() and anonymous functions should be used instead.
With PHP 5.3, you can now do this:
function doIt($callback) { $callback(); }
doIt(function() {
// this will be done
});
Finally a nice way to do it. A great addition to PHP, because callbacks are awesome.
Implementation of a callback is done like so
// This function uses a callback function.
function doIt($callback)
{
$data = "this is my data";
$callback($data);
}
// This is a sample callback function for doIt().
function myCallback($data)
{
print 'Data is: ' . $data . "\n";
}
// Call doIt() and pass our sample callback function's name.
doIt('myCallback');
Displays: Data is: this is my data
One nifty trick that I've recently found is to use PHP's create_function() to create an anonymous/lambda function for one-shot use. It's useful for PHP functions like array_map(), preg_replace_callback(), or usort() that use callbacks for custom processing. It looks pretty much like it does an eval() under the covers, but it's still a nice functional-style way to use PHP.
well... with 5.3 on the horizon, all will be better, because with 5.3, we'll get closures and with them anonymous functions
http://wiki.php.net/rfc/closures
You will want to verify whatever your calling is valid. For example, in the case of a specific function, you will want to check and see if the function exists:
function doIt($callback) {
if(function_exists($callback)) {
$callback();
} else {
// some error handling
}
}
create_function did not work for me inside a class. I had to use call_user_func.
<?php
class Dispatcher {
//Added explicit callback declaration.
var $callback;
public function Dispatcher( $callback ){
$this->callback = $callback;
}
public function asynchronous_method(){
//do asynch stuff, like fwrite...then, fire callback.
if ( isset( $this->callback ) ) {
if (function_exists( $this->callback )) call_user_func( $this->callback, "File done!" );
}
}
}
Then, to use:
<?php
include_once('Dispatcher.php');
$d = new Dispatcher( 'do_callback' );
$d->asynchronous_method();
function do_callback( $data ){
print 'Data is: ' . $data . "\n";
}
?>
[Edit]
Added a missing parenthesis.
Also, added the callback declaration, I prefer it that way.
For those who don't care about breaking compatibility with PHP < 5.4, I'd suggest using type hinting to make a cleaner implementation.
function call_with_hello_and_append_world( callable $callback )
{
// No need to check $closure because of the type hint
return $callback( "hello" )."world";
}
function append_space( $string )
{
return $string." ";
}
$output1 = call_with_hello_and_append_world( function( $string ) { return $string." "; } );
var_dump( $output1 ); // string(11) "hello world"
$output2 = call_with_hello_and_append_world( "append_space" );
var_dump( $output2 ); // string(11) "hello world"
$old_lambda = create_function( '$string', 'return $string." ";' );
$output3 = call_with_hello_and_append_world( $old_lambda );
var_dump( $output3 ); // string(11) "hello world"
I cringe every time I use create_function() in php.
Parameters are a coma separated string, the whole function body in a string... Argh... I think they could not have made it uglier even if they tried.
Unfortunately, it is the only choice when creating a named function is not worth the trouble.