skip to Main Content

I’m struggling with creating optional generator functions in PHP. Essentially, I’m trying to build a function that only turns into a generator if there are multiple values and behaves like a normal function if only a single value is present.

Per the official documentation:

Any function containing yield is a generator function.

A simple yet easy rule to follow by. However, with things I’ve recently been working on I’ve been having trouble with this. In particular, I haven’t found a clean way to implement generator functions in a way that allows me to only return a generator if necessary. A quick example:

function generatorTest() {
    if(false) {
        yield 0;
    } else {
        return 0;
    }
}

var_dump(generatorTest()); // object(Generator)

Looking at the code, one might expect to only get the value, but as it turns out, a functions is turned into a generator really anytime it contains the yield keyword. Which is in line with the documentation, but questionable. Even if the yield keyword is unreachable, the function is turned into a generator. A simple way around this would be to return a closure that itself is a generator, so the function itself isn’t turned into a generator:

function generatorTest() {
    if(false) {
        return (function() {
            yield 0;
        })();
    } else {
        return 0;
    }
}

var_dump(generatorTest()); // int(0)

And this, while not the cleanest workaround, actually works. However, with the application I’m currently working on, this is rather impractical, because I’m not just yielding/returning a simple value but instead iterate complex requests to an API. As a result I’d have two blocks that are rather similar and unnecessarily bloat the function.

I also looked into alternatives and came up with this:

function generatorTest() {
    $ret = (function() {
        if(false) {
            yield 0;
        } else {
            return 0;
        }
    })();
    
    try {
        return $ret->getReturn();
    } catch(Exception $e) {
        return $ret;
    }
}

var_dump(generatorTest()); // int(0)

Instead of returning a function that is a generator if necessary, it always creates a generator and checks if any return value is present. It doesn’t bloat the code because it always returns a function with only the return and yield actually branching off, everything else would have to be written only once. However it basically intentionally uses exceptions to check if it’s actually a generator or just a normal value that was returned. Which really is kinda ugly.

So here is the question, is there a better way to create optional generator functions, as in functions that are only generators if a condition applies and otherwise will just return as normal? Am I entirely off on my modeling and should look at a different approach? Or is my case so out-of-this-world that my ugly solution is pretty much the best for that situation?

Edit: Additional details on the apllication

The application in question is this MediaWiki bot. Specifically, I’m currently working on this function, which already uses the second method I mentioned in my example, but stores all data it loads before return the generator, which pretty much defeats the purpose of the generator and is neither efficient nor useful, because I have to wait like an hour before I know if it actually works when performing a large number of requests.

The function mentioned above is used to query revisions on a wiki and the idea was to provide different return types depending on what you want to query. Since overloading is not really possible in PHP, this is the result I got. When calling the function with a single id, it would look like this:

require_once("bottibott_v5/src/autoload.php");

$bot = new Bot(URL); // URL is the url to the api endpoint of the wiki
$revisions = new Revisions($bot);
$revisions->setIds("1");

var_dump($revisions->getRevisionsFromRevids()); // would output some kind of revision record, depending on the wiki

Meanwhile when calling the same function with multiple ids, the request would look like this:

require_once("bottibott_v5/src/autoload.php");

$bot = new Bot(URL); // URL is the url to the api endpoint of the wiki
$revisions = new Revisions($bot);
$revisions->setIds(range(0, 10));

foreach($revisions->getRevisionsFromRevids() as $revision) {
    var_dump($revision); // would output each revision record individually
}

2

Answers


  1. If you have 1 value, you could just convert this into an array, so that you always yield any value from the function. The main advantage is that you just foreach over the generator and do the same logic…

    function generatorTest(mixed $range)
    {
        if (!is_array($range)) {
            $range = [$range];
        }
        foreach ($range as $individual) {
            yield $individual;
        }
    }
    
    var_dump(generatorTest(1));
    // class Generator#1 (0) {
    // }
    
    var_dump(generatorTest(range(0, 10))); 
    // class Generator#1 (0) {
    // }
    
    Login or Signup to reply.
  2. Actually I agree with making generator in all conditions to make code more concise and simple to use!

    function generatorTest(mixed $arr): Generator
    {
        yield from is_array($arr)? $arr: [$arr];
    }
    
    

    But there is a way to do as you wish a little more cleaner

    Use two functions

    function makeGenerator(array $array):Generator
    {
        yield from $array;
    }
    
    function generatorTest(bool $generator = false):Generator|int
    {
        $array = [1, 2, 3];
        return $generator? makeGenerator($array): 5;
    }
    

    Which In your case and code would be like this from line 113 to 123:

    return $generator && count($revisions) == 1? $this->makeGenerator($revisions): reset($revisions);
    

    And a new function to make generator at line 125:

    protected function makeGenerator(array $revisions){
        yield from $revisions;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search