diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3fc5ac7..ce70d7a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -756,12 +756,12 @@ parameters: path: src/Resolver.php - - message: "#^Class ipl\\\\Orm\\\\ResultSet implements generic interface Iterator but does not specify its types\\: TKey, TValue$#" + message: "#^Cannot assign offset mixed to ArrayIterator\\.$#" count: 1 path: src/ResultSet.php - - message: "#^Method ipl\\\\Orm\\\\ResultSet\\:\\:__construct\\(\\) has parameter \\$limit with no type specified\\.$#" + message: "#^Class ipl\\\\Orm\\\\ResultSet implements generic interface Iterator but does not specify its types\\: TKey, TValue$#" count: 1 path: src/ResultSet.php @@ -776,17 +776,7 @@ parameters: path: src/ResultSet.php - - message: "#^Method ipl\\\\Orm\\\\ResultSet\\:\\:hasMore\\(\\) has no return type specified\\.$#" - count: 1 - path: src/ResultSet.php - - - - message: "#^Method ipl\\\\Orm\\\\ResultSet\\:\\:hasResult\\(\\) has no return type specified\\.$#" - count: 1 - path: src/ResultSet.php - - - - message: "#^Method ipl\\\\Orm\\\\ResultSet\\:\\:yieldTraversable\\(\\) has no return type specified\\.$#" + message: "#^Method ipl\\\\Orm\\\\ResultSet\\:\\:key\\(\\) should return int\\|null but returns mixed\\.$#" count: 1 path: src/ResultSet.php @@ -796,22 +786,12 @@ parameters: path: src/ResultSet.php - - message: "#^Property ipl\\\\Orm\\\\ResultSet\\:\\:\\$cache has no type specified\\.$#" - count: 1 - path: src/ResultSet.php - - - - message: "#^Property ipl\\\\Orm\\\\ResultSet\\:\\:\\$generator has no type specified\\.$#" - count: 1 - path: src/ResultSet.php - - - - message: "#^Property ipl\\\\Orm\\\\ResultSet\\:\\:\\$limit has no type specified\\.$#" + message: "#^Parameter \\#1 \\$offset of method ArrayIterator\\<\\(int\\|string\\),mixed\\>\\:\\:seek\\(\\) expects int, mixed given\\.$#" count: 1 path: src/ResultSet.php - - message: "#^Property ipl\\\\Orm\\\\ResultSet\\:\\:\\$position has no type specified\\.$#" + message: "#^Property ipl\\\\Orm\\\\ResultSet\\:\\:\\$cache with generic class ArrayIterator does not specify its types\\: TKey, TValue$#" count: 1 path: src/ResultSet.php diff --git a/src/ResultSet.php b/src/ResultSet.php index 05117a5..0e633fd 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -3,31 +3,94 @@ namespace ipl\Orm; use ArrayIterator; +use BadMethodCallException; +use Generator; use Iterator; use Traversable; +/** + * Dataset containing database rows + */ class ResultSet implements Iterator { + /** @var ArrayIterator */ protected $cache; /** @var bool Whether cache is disabled */ protected $isCacheDisabled = false; + /** @var Generator */ protected $generator; + /** @var ?int */ protected $limit; + /** @var ?int */ protected $position; - public function __construct(Traversable $traversable, $limit = null) + /** @var ?int */ + protected $offset; + + /** @var ?int */ + protected $pageSize; + + /** + * Construct the ResultSet object + * + * @param Traversable $traversable + * @param ?int $limit + * @param ?int $offset + */ + public function __construct(Traversable $traversable, $limit = null, $offset = null) { $this->cache = new ArrayIterator(); $this->generator = $this->yieldTraversable($traversable); $this->limit = $limit; + $this->offset = $offset; + } + + /** + * Get the current page number + * + * Returns the current page, calculated by the {@see self::$position position}, {@see self::$offset offset} + * and the {@see self::$pageSize page size} + * + * @return int page + * @throws BadMethodCallException if no {@see self::$pageSize page size} has been provided + */ + public function getCurrentPage(): int + { + if ($this->pageSize) { + $offset = $this->offset ?: 0; + $position = ($this->position ?: 0) + 1; + if (($position + $offset) > $this->pageSize) { + // we are not on the first page anymore, calculating proper page + return intval(ceil(($position + $offset) / $this->pageSize)); + } + + // still on the first page + return 1; + } + + throw new BadMethodCallException("The 'pageSize' property has not been set. Cannot calculate pages."); + } + + /** + * Set the amount of entries a page should contain (needed for pagination) + * + * @param ?int $size entries per page + * + * @return $this + */ + public function setPageSize(?int $size) + { + $this->pageSize = $size; + + return $this; } /** - * Create a new result set from the given query + * Create a new result set from the given {@see Query query} * * @param Query $query * @@ -35,7 +98,7 @@ public function __construct(Traversable $traversable, $limit = null) */ public static function fromQuery(Query $query) { - return new static($query->yieldResults(), $query->getLimit()); + return new static($query->yieldResults(), $query->getLimit(), $query->getOffset()); } /** @@ -52,11 +115,21 @@ public function disableCache() return $this; } + /** + * Check if dataset has more entries + * + * @return bool + */ public function hasMore() { return $this->generator->valid(); } + /** + * Check if dataset has a result + * + * @return bool + */ public function hasResult() { return $this->generator->valid(); @@ -86,7 +159,13 @@ public function next(): void } } - public function key(): int + /** + * Return the current item's key + * + * @return ?int + */ + #[\ReturnTypeWillChange] + public function key() { if ($this->position === null) { $this->advance(); @@ -137,6 +216,13 @@ protected function advance() } } + /** + * Yield entry from dataset + * + * @param Traversable $traversable + * + * @return Generator + */ protected function yieldTraversable(Traversable $traversable) { foreach ($traversable as $key => $value) { diff --git a/tests/ResultSetTest.php b/tests/ResultSetTest.php index c0c1413..1baba56 100644 --- a/tests/ResultSetTest.php +++ b/tests/ResultSetTest.php @@ -3,6 +3,7 @@ namespace ipl\Tests\Orm; use ArrayIterator; +use BadMethodCallException; use ipl\Orm\ResultSet; use PHPUnit\Framework\TestCase; @@ -81,4 +82,83 @@ public function testResultWithCacheEnabledWithLimit() ['a', 'b', 'a', 'b'] ); } + + public function testResultPaging() + { + $set = (new ResultSet(new ArrayIterator(['a', 'b', 'c', 'd', 'e', 'f', 'g']))) + ->setPageSize(2); + + $count = 0; + foreach ($set as $item) { + ++$count; + + if ($count > 2) { + if ($count % 2 === 0) { + // a multiple of two, page should equal to count / 2 + $this->assertEquals( + $set->getCurrentPage(), + $count / 2 + ); + } elseif ($count % 2 === 1) { + $this->assertEquals( + $set->getCurrentPage(), + intval(ceil($count / 2)) + ); + } + } else { + $this->assertEquals( + $set->getCurrentPage(), + 1 + ); + } + } + } + + public function testResultPagingWithoutPageSize() + { + $this->expectException(BadMethodCallException::class); + + $set = (new ResultSet(new ArrayIterator(['a', 'b', 'c', 'd', 'e', 'f', 'g']))); + + foreach ($set as $_) { + // this raises an exception as no page size has been set + $set->getCurrentPage(); + } + } + + public function testResultPagingWithOffset() + { + $set = (new ResultSet(new ArrayIterator(['d', 'e', 'f', 'g', 'h', 'i', 'j']), null, 3)) + ->setPageSize(2); + + $count = 0; + foreach ($set as $_) { + ++$count; + + $offsetCount = $count + 3; + if ($offsetCount % 2 === 0) { + // a multiple of two, page should equal to offsetCount / 2 + $this->assertEquals( + $set->getCurrentPage(), + $offsetCount / 2 + ); + } elseif ($offsetCount % 2 === 1) { + $this->assertEquals( + $set->getCurrentPage(), + intval(ceil($offsetCount / 2)) + ); + } + } + } + + public function testResultPagingBeforeIteration() + { + $set = (new ResultSet(new ArrayIterator(['a', 'b', 'c', 'd', 'e', 'f', 'g']))) + ->setPageSize(2); + + $this->assertEquals( + $set->getCurrentPage(), + 1 + ); + } }