phpy icon indicating copy to clipboard operation
phpy copied to clipboard

[ Feature ] Support for PHP generators

Open TheCelavi opened this issue 11 months ago • 1 comments

Is it possible to add support for PHP generators to be passed to Python?

Per example, pandas dataframe object could work with generators for large datasets...

TheCelavi avatar Feb 17 '25 13:02 TheCelavi

You can implement a Python iterator class on the PHP side like this:

declare(strict_types=1);

namespace YourNamespace;

use Closure;
use Exception;
use Generator;

#[\PyInherit('Iterator', 'typing')]
class Iterator extends \PyClass
{
    protected Generator $generator;

    /**
     * @param Generator|Closure $generator
     * @throws Exception
     */
    public function __construct(
        Generator|Closure $generator
    )
    {
        parent::__construct();
        if ($generator instanceof Closure) {
            $generator = $generator();
            if (!$generator instanceof Generator) {
                throw new \InvalidArgumentException('Closure must return a Generator. ');
            }
        }
        $this->generator = $generator;
    }

    public function __iter__()
    {
        return $this->self();
    }

    public function __next__()
    {
        if (!$this->generator->valid()) {
            return null;
        }
        $value = $this->generator->current();
        $this->generator->next();
        return $value;
    }

    public function __invoke(): ?\PyIter
    {
        $res = $this->self();
        return $res instanceof \PyIter ? $res : null;
    }
}
\PyClass::setProxyPath(__DIR__);

Then, make a slight tweak to the proxy generated by phpy—specifically, in the implementation of next, add logic to throw a StopIteration:

import typing

class workbunny_phporc_iterator(typing.Iterator):
  def __init__(self, _this):
    self.__this = _this
    _this.set('_super', super())
    _this.set('_self', self)
    _this.call('__init')

  def __iter__(self, ):
    return self.__this.call('__iter__', )

  def __next__(self, ):
    res = self.__this.call('__next__', )
    if res == None:
      raise StopIteration
    return res

  def __init(self, ):
    return self.__this.call('__init', )

I've implemented this approach in the php-orc project, so feel free to check it out.

chaz6chez avatar Apr 01 '25 02:04 chaz6chez