Skip to content

Commit

Permalink
Introduce class ipl\Stdlib\CallbackFilterIterator
Browse files Browse the repository at this point in the history
This class stems from the motivation to have a filter iterator
that uses callbacks, which behaves like any other iterator.
Just like the SPL's `CallbackFilterIterator`, but that works
differently: It isn't valid unless explicitly rewound first.

Any other iterator, especially generators, doesn't need to be
rewound first. That's most obvious with a generator that yields
conditionally, just like a filter iterator would.

Any call to `valid()`, `key()` or `current()` should return the
same result as the very first iteration step would.
  • Loading branch information
nilmerg committed Mar 26, 2024
1 parent 8854c46 commit ed7cb0e
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
70 changes: 70 additions & 0 deletions src/CallbackFilterIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace ipl\Stdlib;

use CallbackFilterIterator as SplCallbackFilterIterator;

/**
* CallbackFilterIterator that behaves like generators in terms of validity before the first call to `rewind()`
*
* This class stems from the motivation to have a filter iterator
* that uses callbacks, which behaves like any other iterator.
* Just like the SPL's `CallbackFilterIterator`, but that works
* differently: It isn't valid unless explicitly rewound first.
*
* Any other iterator, especially generators, doesn't need to be
* rewound first. That's most obvious with a generator that yields
* conditionally, just like a filter iterator would.
*
* Any call to `valid()`, `key()` or `current()` should return the
* same result as the very first iteration step would.
*/
class CallbackFilterIterator extends SplCallbackFilterIterator
{
/** @var bool Whether iteration has started */
private $started = false;

public function rewind(): void
{
$this->started = true;

parent::rewind();
}

public function valid(): bool
{
if ($this->started) {
return parent::valid();
}

// As per php-src, \CallbackFilterIterator::rewind() forwards the iterator to the first valid element
// (https://github.com/php/php-src/blob/5cba2a3dc59ef2a0e432b05ab27f2b3ab4da48d0/ext/spl/spl_iterators.c#L1686)
$this->rewind();

return parent::valid();
}

#[\ReturnTypeWillChange]
public function key()

Check failure on line 48 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.2 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::key() is not compatible with return type mixed of method FilterIterator::key().

Check failure on line 48 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.3 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::key() is not compatible with return type mixed of method FilterIterator::key().

Check failure on line 48 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.4 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::key() is not covariant with return type mixed of method FilterIterator::key().
{
if ($this->started) {
return parent::key();
}

$this->rewind();

return parent::key();
}

#[\ReturnTypeWillChange]
public function current()

Check failure on line 60 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.2 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::current() is not compatible with return type mixed of method FilterIterator::current().

Check failure on line 60 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.3 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::current() is not compatible with return type mixed of method FilterIterator::current().

Check failure on line 60 in src/CallbackFilterIterator.php

View workflow job for this annotation

GitHub Actions / Static analysis for php 7.4 on ubuntu-latest

Return type mixed of method ipl\Stdlib\CallbackFilterIterator::current() is not covariant with return type mixed of method FilterIterator::current().
{
if ($this->started) {
return parent::current();
}

$this->rewind();

return parent::current();
}
}
70 changes: 70 additions & 0 deletions tests/CallbackFilterIteratorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace ipl\Tests\Stdlib;

use ipl\Stdlib\CallbackFilterIterator;

class CallbackFilterIteratorTest extends TestCase
{
public function testFirstIterationStep()
{
$iterator = new CallbackFilterIterator(new \ArrayIterator([1, 2, 3]), function (int $i) {
return $i === 2;
});

foreach ($iterator as $k => $v) {
$this->assertSame(1, $k);
$this->assertSame(2, $v);
}
}

public function testValidBeforeRewind()
{
$iterator = new CallbackFilterIterator(new \ArrayIterator([1, 2, 3]), function (int $i) {
return $i === 2;
});

$this->assertTrue($iterator->valid());

$iterator->rewind();

$this->assertTrue($iterator->valid());
}

public function testKeyBeforeRewind()
{
$iterator = new CallbackFilterIterator(new \ArrayIterator([1, 2, 3]), function (int $i) {
return $i === 2;
});

$this->assertSame(1, $iterator->key());

$iterator->rewind();

$this->assertSame(1, $iterator->key());
}

public function testCurrentBeforeRewind()
{
$iterator = new CallbackFilterIterator(new \ArrayIterator([1, 2, 3]), function (int $i) {
return $i === 2;
});

$this->assertSame(2, $iterator->current());

$iterator->rewind();

$this->assertSame(2, $iterator->current());
}

public function testInvalidBeforeRewind()
{
$iterator = new CallbackFilterIterator(new \ArrayIterator([1, 2, 3]), function (int $i) {
return $i === 4;
});

$this->assertFalse($iterator->valid());
$this->assertNull($iterator->key());
$this->assertNull($iterator->current());
}
}

0 comments on commit ed7cb0e

Please sign in to comment.