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
Related
I'm designing a simple templating system for a CMS in PHP which internally currently uses something like:
require_once 'templates/template1.php`;
to import the desired template.
I would like every content {{field123}} in this PHP file to be automatically converted into <?php echo $row['field123']; ?> before being passed into require_once and executed by PHP.
Is there a way to activate a preprocessor (I know that PHP is already named after preprocessor) that does this replacement {{anything}} -> <?php echo $row['anything']; ?> before executing the PHP code template1.php? If not, what's the usual way to do this?
Having PHP code in templates - especially code with potential side-effects - can get dirty real quick. I would recommend using static templates, treating them as strings instead of executing them, then parsing them for tokens, with your main application compiling them and handling output.
Here is a rudimentary implementation that parses variables into tokens, and also handles mapped function calls in your templates. First, "fetching" our template (for a simple example):
$tpl = 'This is a sample template file.
It can have values like {{foo}} and {{bar}}.
It can also invoke mapped functions:
{{func:hello}} or {{func:world}}.
Hello user {{username}}. Have a good day!';
Then, the template parser:
function parse_template(string $tpl, array $vars): string {
// Catch function tokens, handle if handler exists:
$tpl = preg_replace_callback('~{{func:([a-z_]+)}}~', function($match) {
$func = 'handler_' . $match[1];
if(function_exists($func)) {
return $func();
}
return "!!!What is: {$match[1]}!!!";
}, $tpl);
// Generate tokens for your variable keys;
$keys = array_map(fn($key) => '{{' . $key . '}}', array_keys($vars));
// Substitute tokens:
$tpl = str_replace($keys, $vars, $tpl);
return $tpl;
}
These are our handler functions, with handler_X matching {{func:X}}.
function handler_hello() {
return 'HELLO THERE';
}
function handler_world() {
return '#Current World Population: ' . mt_rand();
}
Then, here are the variables you'd like to parse in:
$vars = [
'foo' => 'Food',
'bar' => 'Barnacle',
'username' => 'Herbert'
];
Now let's parse our template:
$parsed = parse_template($tpl, $vars);
echo $parsed;
This results in:
This is a sample template file.
It can have values like Food and Barnacle.
It can also invoke mapped functions:
HELLO THERE or #Current World Population: 1477098027.
Hello user Herbert. Have a good day!
Job done. You really don't need a complicated templating engine for something like this. You could easily extend this to allow the handlers to receive arguments defined in the template tokens -- however I'll leave that for your homework part. This should do to demonstrate the concept.
As mentioned in a comment and in How do I capture PHP output into a variable?, the use of output buffering can work:
<?php
ob_start();
?>
Hello
{{field123}} and {{field4}}
World
<?php // or require_once 'template1.php'; ?>
<?php
$s = ob_get_clean();
$a = array('field123' => 'test', 'field4' => 'test2');
$s = preg_replace_callback('/{{(.*?)}}/', function ($m) use ($a) { return isset($a[$m[1]]) ? $a[$m[1]] : $m[0]; }, $s);
echo $s;
?>
// Output:
// Hello
// test and test2
// World
Here we also used a method similar to Replace with dynamic variable in preg_replace to do the replacement.
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;
When I execute following code I am getting this error. Why is that? What is the proper use of callbacks?
CODE (simplified)
class NODE {
//...some other stuff
function create($tags, $callback=false) {
$temp = new NODE();
//...code and stuff
if($callback) $callback($temp); //fixed (from !$callback)
return $this;
}
}
$document = new NODE();
$document->create("<p>", function($parent) {
$parent->create("<i>");
});
ERROR
Fatal error: Function name must be a string in P:\htdocs\projects\nif\nif.php on line 36
$document->new NODE();
This is not valid syntax. The accepted format would be:
$document = new NODE();
In addition to this, if you use the unary operator (!) on a false, you get true. If you use it on a Callable, you get false. As such, if (!$callback) $callback() will throw the first error of your script.
As a side note, you are reinventing the wheel. I would strongly recommend you take a look at the DOMDocument family of classes, which are doing exactly what you are currently trying to implement, albeit with fewer callbacks.
if(!$callback) $callback($temp);
If $callback is false, for sure you won't be able to call it as a callback.
if(!$callback) $callback($temp);
should probably be
if($callback) $callback($temp);
And the instanciation:
$document = new NODE();
My 2c here, type hinting may be good to use here as well.
Ex: function create($tags, callable $callback = function())
To do such a thing in php you should use function pointers and tell php which function to execute.
Look at this code.
// This function uses a callback function.
function doIt($callback)
{
$data = acquireData();
$callback($data);
}
// This is a sample callback function for doIt().
function myCallback($data)
{
echo 'Data is: ', $data, "\n";
}
// Call doIt() and pass our sample callback function's name.
doIt('myCallback');
So as you seen you can only pass the name to the function and you should predefine the function..
Similar question: How do I implement a callback in PHP?
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
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