Skip to content

Instantly share code, notes, and snippets.

@andrerom
Last active November 10, 2023 14:52
Show Gist options
  • Save andrerom/a8573d45547875f6562b881b43bd1864 to your computer and use it in GitHub Desktop.
Save andrerom/a8573d45547875f6562b881b43bd1864 to your computer and use it in GitHub Desktop.
Lazy Collection using Generator in PHP (Example, script in bottom of file to test it)
<?php
/**
* Class GeneratorCollection, allows for lazy loaded arrays using Generators withouth it's limitations.
*
* This collection class takes Generator as argument, and allows user to threat it like any kind of array. It will take
* care about storing the values returned from generator so user can iterate over it several times.
*
* NOTE: In current form only works with generators that uses integers as keys (lists).
*/
class GeneratorCollection implements Iterator, ArrayAccess, Countable
{
private $position = 0;
private $array = [];
private $generator;
public function __construct(Generator $generator)
{
$this->generator = $generator;
}
public function rewind()
{
if (isset($this->generator)) {
// If position has already been increased a non complete loop has been made, so we need to load remaining
if ($this->position) {
$this->loadAll();
} else {
$this->generator->rewind();
}
}
$this->position = 0;
}
public function current()
{
if (isset($this->generator)) {
// As the generator is iterated, set the values on array here so we can also iterate on it later
return $this->array[$this->generator->key()] = $this->generator->current();
}
return $this->array[$this->position];
}
public function key()
{
if (isset($this->generator)) {
return $this->generator->key();
}
return $this->position;
}
public function next()
{
if (isset($this->generator)) {
$this->generator->next();
}
// Also incrememnt our own position even if generator is still set for use in rewind()
++$this->position;
}
public function valid()
{
if (isset($this->generator)) {
if ($this->generator->valid()) {
return true;
}
// If not valid we are at the end of the array, for future iteration we'll unset this and use array
unset($this->generator);
}
return isset($this->array[$this->position]);
}
public function offsetExists($offset)
{
if (isset($this->generator)) {
$this->loadAll();
}
return isset($this->array[$offset]);
}
public function offsetGet($offset)
{
if (isset($this->generator)) {
$this->loadAll();
}
return $this->array[$offset];
}
public function offsetSet($offset, $value)
{
// Read only collection, so not handled here (for making it writable it would have to loadAll first if needed)
}
public function offsetUnset($offset)
{
// Read only collection, so not handled here (for making it writable it would have to loadAll first if needed)
}
public function count()
{
if (isset($this->generator)) {
$this->loadAll();
}
return count($this->array);
}
private function loadAll()
{
// As we might have loaded some elements already we use array union to retain everything
$this->array = iterator_to_array($this->generator) + $this->array;
unset($this->generator);
}
}
function gen()
{
echo "(Generator started)".PHP_EOL;
yield from [1, 2, 3];
return 4;
}
$iterator = new GeneratorCollection(gen());
// uncomment this to see how generators behaves normally
//$iterator = gen();
echo "Generator should start after this line even if function is called above (aka should be lazy loaded)!\n";
foreach($iterator as $value) {
echo $value;
if ($value === 2) break;
}
echo "\n";
echo $iterator[2];
echo "\n";
foreach($iterator as $value) {
echo $value;
}
echo "\n";
foreach($iterator as $value) {
echo $value;
}
echo "\n";
echo "Count:" .count($iterator). "\n\n";
/* Expected output is:
Generator should start after this line even if function is called above (aka should be lazy loaded)!
Generator started!
12
3
123
123
Count:3
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment