Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paging functionality to ResultSet #136

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
30 changes: 5 additions & 25 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down
94 changes: 90 additions & 4 deletions src/ResultSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,102 @@
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
*
* @return static
ncosta-ic marked this conversation as resolved.
Show resolved Hide resolved
*/
public static function fromQuery(Query $query)
{
return new static($query->yieldResults(), $query->getLimit());
return new static($query->yieldResults(), $query->getLimit(), $query->getOffset());
}

/**
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
80 changes: 80 additions & 0 deletions tests/ResultSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ipl\Tests\Orm;

use ArrayIterator;
use BadMethodCallException;
use ipl\Orm\ResultSet;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -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
);
}
}
Loading