stckvrw
stckvrw

Reputation: 1801

How to convert PHP string as name of a function and call the function

Here is my PHP code:

<?php
function getItems($arg) {
    $products = array();
    $products[] = (object) array('id' => '0', 'title' => 'Product1');
    $products[] = (object) array('id' => '1', 'title' => 'Product2');
    return $products;
}

$jsonContent = '{"myKey":["loop", "getItems(\"Arg\")", "<div><h3>{$item->title}</h3></div>"]}';

$jsonObj = json_decode($jsonContent);

$options = $jsonObj->myKey;

if($options[0] == 'loop') {

    $html = [];

    foreach($options[1] as $item) {

        $html[] = $options[2];
    }

    echo implode('', $html);
}
?>

Of course, I get the warning

Invalid argument supplied for foreach() ...

because the argument for the foreach() is not an array but a string.

So how to convert a string as name of a function and call the function?

And how to use the third element of the $options array as a wrapper with dynamic data from the loop (I hope you will understand me by the code)

I need to get the following result:

<div><h3>Product1</h3></div>
<div><h3>Product2</h3></div>

Upvotes: 0

Views: 542

Answers (6)

AbraCadaver
AbraCadaver

Reputation: 79024

This is hackish and uses eval which can be dangerous on untrusted data, but for fun:

$arg = 'something';

if($options[0] == 'loop') {
    $func = rtrim($options[1], '()');

    foreach($func($arg) as $item) {
        eval("echo \"{$options[2]}\";");
    }
}

Or a slightly different approach without an argument:

if($options[0] == 'loop') {
    eval("\$items = {$options[1]};");

    foreach($items as $item) {
        eval("echo \"{$options[2]}\";");
    }
}

Upvotes: 0

Andreas
Andreas

Reputation: 23968

The basic idea of it is to skip the foreach and just "run it" and input options[2] as argument, however since the string has () this had to be removed with substr.

$html = [];


$html[] =substr($options[1],0,-2)($options[2]);

var_dump($html);

https://3v4l.org/sE2bT


I see you asked of a different approach without the eval.

You can use str_replace in a loop in your function.
Because products are undefined when the string is created then the "" will not concert it to products value in the function.

But this will (I assume) give you the output you want.

function getItems($item) {
    $products = array();
    $products[] = (object) array('id' => '0', 'title' => 'Product1');
    $products[] = (object) array('id' => '1', 'title' => 'Product2');
    $return = "";
    foreach($products as $prod){
        $return  .= str_replace('$products->title',$prod->title, $item);
    }
    return $return;
}

$jsonContent = '{"myKey":["loop", "getItems()", "<div><h3>{$products->title}</h3></div>", "<div><h1>{$products->title}</h1></div>"]}';

$jsonObj = json_decode($jsonContent);

$options = $jsonObj->myKey;

if($options[0] == 'loop') {

    $html = [];
    $func = substr($options[1],0,-2);
    foreach(array_slice($options,2) as $arg){
        $html[] = $func($arg);
    }

    var_dump($html);
}

/*
array(2) {
  [0]=>
  string(60) "<div><h3>{Product1}</h3></div><div><h3>{Product2}</h3></div>"
  [1]=>
  string(60) "<div><h1>{Product1}</h1></div><div><h1>{Product2}</h1></div>"
}
*/

https://3v4l.org/66Kv7


If you can't change the code in function then do the same processing in the "front".

function getItems() {
    $products = array();
    $products[] = (object) array('id' => '0', 'title' => 'Product1');
    $products[] = (object) array('id' => '1', 'title' => 'Product2');
    return $products;
}

$jsonContent = '{"myKey":["loop", "getItems()", "<div><h3>{$products->title}</h3></div>", "<div><h1>{$products->title}</h1></div>"]}';

$jsonObj = json_decode($jsonContent);

$options = $jsonObj->myKey;

if($options[0] == 'loop') {

    $html = [];
    $func = substr($options[1],0,-2);
    $products = $func();

    foreach(array_slice($options,2) as $arg){ 
        foreach($products as $prod){
            $html[] = str_replace('$products->title',$prod->title, $arg);
        }       
    }

    var_dump($html);
}

/*
array(4) {
  [0]=>
  string(30) "<div><h3>{Product1}</h3></div>"
  [1]=>
  string(30) "<div><h3>{Product2}</h3></div>"
  [2]=>
  string(30) "<div><h1>{Product1}</h1></div>"
  [3]=>
  string(30) "<div><h1>{Product2}</h1></div>"
}
*/

https://3v4l.org/1jT48

Upvotes: 1

Don&#39;t Panic
Don&#39;t Panic

Reputation: 41820

I think this would be more workable with a couple of changes to the JSON. First, I think it would make the code more readable if options was an object rather than an array, but this same approach should still work with an array if you prefer that. Second, I think the function arguments and any object properties used in the template should be separate options to avoid use of eval().

{
    "myKey": {
        "type": "loop",
        "function": "getItems",
        "args": [
            "Arg"
        ],
        "template": "<div><h3>%s<\/h3><\/div>",
        "properties": [
            "title"
        ]
    }
}

The code for processing options after decoding the JSON:

if ($options->type == 'loop') {
    $html = [];
    foreach (($options->function)(...$options->args) as $item) {
        $html[] = vsprintf($options->template, array_map(function ($property) use ($item) {
            return $item->$property;
        }, $options->properties));
    }
    echo implode('', $html);
}

I would not recommend using this if the JSON would ever come from any external source. It seems like a good way to let people execute arbitrary code on your system, even without eval. Even if you're only using it yourself, I think it would still be prudent to at least validate the function name against a list of allowed functions.


Code for options array rather than object:

JSON:

{"myKey":["loop","getItems",["Arg"],"<div><h3>%s<\/h3><\/div>",["title"]]}

PHP:

if ($options[0] == 'loop') {
    $html = [];
    foreach (($options[1])(...$options[2]) as $item) {
        $html[] = vsprintf($options[3], array_map(function ($property) use ($item) {
            return $item->$property;
        }, $options[4]));
    }
    echo implode('', $html);
}

Upvotes: 0

Nigel Ren
Nigel Ren

Reputation: 57141

With the extra complication of adding in the value to the HTML, I can only think of the evil eval. But for the rest, just separate the function name from the parameters and create them as an array in the JSON. These can then be passed in using the Argument unpacking ... operator. I've amended the function to show how the passed in arguments could work.

In the JSON I have

["Product", 3]

which maps onto

function getItems( $prefix, $start) {

So to put this together...

function getItems( $prefix, $start) {
    $products = array();
    $products[] = (object) array('id' => '0', 'title' => $prefix.$start);
    $products[] = (object) array('id' => '1', 'title' => $prefix.($start+1));
    return $products;
}

$jsonContent = '{"myKey":["loop", "getItems", ["Product", 3], 
            "<div><h3>{$item->title}</h3></div>"]}';

$jsonObj = json_decode($jsonContent);

$options = $jsonObj->myKey;

if($options[0] == 'loop') {

    $html = [];

    foreach($options[1](...$options[2]) as $item) {
        $html[] = eval("return \"{$options[3]}\";");
    }

    echo implode('', $html);
}

which returns

<div><h3>Product3</h3></div><div><h3>Product4</h3></div>

Upvotes: 1

nessb
nessb

Reputation: 136

Use call_user_func to call a function from a string, and as pointed out by Andreas, use separate array positions for function and argument(s)

But I wanna point out that this is UTTERLY complicated and maybe not necessary... are there any other ways you can uncomplicate it ?

EDIT:

Why do you need to pass an arg that you don't even use anyway ?

function getItems($arg) {
    $products = array();
    $products[] = (object) array('id' => '0', 'title' => 'Product1');
    $products[] = (object) array('id' => '1', 'title' => 'Product2');
    return $products;
}

$jsonContent = '{"myKey":["loop", "getItems", "Arg1,Arg2", "<div><h3>{$item->title}</h3></div>"]}';

$jsonObj = json_decode($jsonContent);

$options = $jsonObj->myKey;

if($options[0] == 'loop') {

    $html = [];

    $args = explode(',', $options[2]);

    foreach($args as $item) {
        $html[] = call_user_func ($options[1], $item);
    }

    echo implode('', $html);
}
?>````

Upvotes: 0

Harvey Fletcher
Harvey Fletcher

Reputation: 104

In order to call a string as your function, you need to remove the () from your string.

$jsonContent = '{"myKey":["loop", "getItems", "<div><h3>{$item->title}</h3></div>"]}';

Then, on your foreach() loop, add them again after $options[0] like this:

 foreach( $options[1]() as $item) {
    $html[] = $options[2];
 }

Upvotes: 0

Related Questions