feat(shared): implement pagination value objects and tests
This commit is contained in:
parent
c28ce7c488
commit
0ebeafeaf1
11 changed files with 608 additions and 378 deletions
|
|
@ -1,122 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Helpers;
|
||||
|
||||
/**
|
||||
* Immutable value object representing a pagination range.
|
||||
*/
|
||||
final readonly class Pagination implements \Stringable
|
||||
{
|
||||
public const int MAX_PAGE_SIZE = 10000;
|
||||
|
||||
private function __construct(
|
||||
private int $from,
|
||||
private int $to,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of the pagination range.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'Pagination{from=%d, to=%d}',
|
||||
$this->from,
|
||||
$this->to,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the from value of the pagination.
|
||||
*/
|
||||
public function from(): int
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the to value of the pagination.
|
||||
*/
|
||||
public function to(): int
|
||||
{
|
||||
return $this->to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and get the page size of the pagination.
|
||||
*/
|
||||
public function pageSize(): int
|
||||
{
|
||||
return $this->to - $this->from + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and get the page number of the pagination.
|
||||
*/
|
||||
public function pageNumber(): int
|
||||
{
|
||||
return (int) ($this->from / $this->pageSize()) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and get the indexes of the pagination.
|
||||
*/
|
||||
public static function ofIndexes(int $from, int $to): self
|
||||
{
|
||||
$from = max(0, $from);
|
||||
|
||||
$to = min(
|
||||
max($to, $from),
|
||||
$from + self::MAX_PAGE_SIZE - 1,
|
||||
);
|
||||
|
||||
return new self($from, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and get the indexes of the pagination.
|
||||
*/
|
||||
public static function ofPage(int $pageNumber, int $pageSize): self
|
||||
{
|
||||
$pageNumber = max(1, $pageNumber);
|
||||
|
||||
$pageSize = min(
|
||||
max(1, $pageSize),
|
||||
self::MAX_PAGE_SIZE,
|
||||
);
|
||||
|
||||
$fromIndex = ($pageNumber - 1) * $pageSize;
|
||||
$toIndex = $fromIndex + $pageSize - 1;
|
||||
|
||||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and get the offset/limit of the pagination.
|
||||
*/
|
||||
public static function ofOffsetAndLimit(int $offset, int $limit): self
|
||||
{
|
||||
$offset = max(0, $offset);
|
||||
|
||||
$limit = min(
|
||||
max(1, $limit),
|
||||
self::MAX_PAGE_SIZE,
|
||||
);
|
||||
|
||||
$fromIndex = $offset;
|
||||
$toIndex = $offset + $limit - 1;
|
||||
|
||||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first page of the pagination.
|
||||
*/
|
||||
public static function firstPage(int $pageSize = 25): self
|
||||
{
|
||||
return self::ofPage(1, $pageSize);
|
||||
}
|
||||
}
|
||||
53
src/Shared/ValueObjects/Pagination/PageNumber.php
Normal file
53
src/Shared/ValueObjects/Pagination/PageNumber.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
/**
|
||||
* Value object representing a page number in pagination (1-indexed).
|
||||
*/
|
||||
final readonly class PageNumber implements \JsonSerializable, \Stringable, Equatable
|
||||
{
|
||||
public const int DEFAULT_PAGE = 1;
|
||||
|
||||
public function __construct(
|
||||
public int $value = self::DEFAULT_PAGE,
|
||||
) {
|
||||
if ($this->value <= 0) {
|
||||
throw new \InvalidArgumentException('Page number must be greater than 0');
|
||||
}
|
||||
}
|
||||
|
||||
public static function of(int $value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return new self(self::DEFAULT_PAGE);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
60
src/Shared/ValueObjects/Pagination/PageSize.php
Normal file
60
src/Shared/ValueObjects/Pagination/PageSize.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
/**
|
||||
* Value object representing a page size in pagination.
|
||||
*/
|
||||
final readonly class PageSize implements \JsonSerializable, \Stringable, Equatable
|
||||
{
|
||||
public const int DEFAULT_SIZE = 25;
|
||||
|
||||
/** ERR_TOO_MANY_RECORDS fixed to 10k */
|
||||
public const int MAX_PAGE_SIZE = 10000;
|
||||
|
||||
public function __construct(
|
||||
public int $value = self::DEFAULT_SIZE,
|
||||
) {
|
||||
if ($this->value <= 0) {
|
||||
throw new \InvalidArgumentException('Page size must be greater than 0');
|
||||
}
|
||||
|
||||
if ($this->value > self::MAX_PAGE_SIZE) {
|
||||
throw new \InvalidArgumentException('Page size must be less than or equal to ' . self::MAX_PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
public static function of(int $value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return new self(self::DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->value === $other->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,20 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\Helpers;
|
||||
namespace Shared\ValueObjects\Pagination;
|
||||
|
||||
/** @template T of object */
|
||||
final readonly class Paginated
|
||||
{
|
||||
/** @param T[] $items */
|
||||
public function __construct(
|
||||
private Pagination $pagination,
|
||||
private readonly Pagination $pagination,
|
||||
private array $items,
|
||||
private int $total
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get items of the list.
|
||||
*
|
||||
* @return T[]
|
||||
*/
|
||||
/** @return T[] */
|
||||
public function getItems(): array
|
||||
{
|
||||
$items = [];
|
||||
|
|
@ -32,17 +28,11 @@ final readonly class Paginated
|
|||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pagination of the list.
|
||||
*/
|
||||
public function getPagination(): Pagination
|
||||
{
|
||||
return $this->pagination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total of the list.
|
||||
*/
|
||||
public function getTotal(): int
|
||||
{
|
||||
return $this->total;
|
||||
94
src/Shared/ValueObjects/Pagination/Pagination.php
Normal file
94
src/Shared/ValueObjects/Pagination/Pagination.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\DDD\Models\Equatable;
|
||||
|
||||
/**
|
||||
* Immutable value object representing a pagination range.
|
||||
*/
|
||||
final readonly class Pagination implements Equatable
|
||||
{
|
||||
/** ERR_TOO_MANY_RECORDS fixed to 10k */
|
||||
public const int MAX_PAGE_SIZE = PageSize::MAX_PAGE_SIZE;
|
||||
|
||||
private function __construct(
|
||||
private int $from,
|
||||
private int $to,
|
||||
) {
|
||||
}
|
||||
|
||||
public function from(): int
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function to(): int
|
||||
{
|
||||
return $this->to;
|
||||
}
|
||||
|
||||
public function pageSize(): int
|
||||
{
|
||||
return $this->to - $this->from + 1;
|
||||
}
|
||||
|
||||
public function pageNumber(): int
|
||||
{
|
||||
return (int) ($this->from / $this->pageSize()) + 1;
|
||||
}
|
||||
|
||||
public function pageSizeVo(): PageSize
|
||||
{
|
||||
return new PageSize($this->pageSize());
|
||||
}
|
||||
|
||||
public function pageNumberVo(): PageNumber
|
||||
{
|
||||
return new PageNumber($this->pageNumber());
|
||||
}
|
||||
|
||||
public function equals(mixed $other): bool
|
||||
{
|
||||
return $other instanceof self && $this->from === $other->from && $this->to === $other->to;
|
||||
}
|
||||
|
||||
public static function ofIndexes(int $from, int $to): self
|
||||
{
|
||||
$from = max(0, $from);
|
||||
$to = clamp($to, $from + 1, $from + self::MAX_PAGE_SIZE);
|
||||
|
||||
return new self($from, $to);
|
||||
}
|
||||
|
||||
public static function ofPage(PageNumber $pageNumber, PageSize $pageSize = new PageSize()): self
|
||||
{
|
||||
$fromIndex = ($pageNumber->value - 1) * $pageSize->value;
|
||||
$toIndex = $fromIndex + $pageSize->value - 1;
|
||||
|
||||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
||||
public static function ofOffsetAndLimit(int $offset, int $limit): self
|
||||
{
|
||||
$offset = max(0, $offset);
|
||||
$limit = clamp($limit, 1, self::MAX_PAGE_SIZE);
|
||||
|
||||
$fromIndex = $offset;
|
||||
$toIndex = $offset + $limit - 1;
|
||||
|
||||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
||||
public static function firstPage(PageSize $pageSize = new PageSize()): self
|
||||
{
|
||||
return self::ofPage(new PageNumber(1), $pageSize);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('Pagination{from=%d, to=%d}', $this->from, $this->to);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Helpers;
|
||||
|
||||
use Shared\Helpers\Paginated;
|
||||
use Shared\Helpers\Pagination;
|
||||
|
||||
describe('paginated', function (): void {
|
||||
it('returns its pagination', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage(12);
|
||||
|
||||
// WHEN
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
|
||||
// THEN
|
||||
expect($paginated->getPagination())
|
||||
->toBe($pagination);
|
||||
});
|
||||
|
||||
it('returns its total', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage(12);
|
||||
|
||||
// WHEN
|
||||
$paginated = new Paginated($pagination, [], 42);
|
||||
|
||||
// THEN
|
||||
expect($paginated->getTotal())->toBe(42);
|
||||
});
|
||||
|
||||
it('returns an empty list when there are no items', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage(12);
|
||||
|
||||
// WHEN
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
|
||||
// THEN
|
||||
expect($paginated->getItems())->toBe([])
|
||||
->and($paginated->getTotal())->toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\Helpers;
|
||||
|
||||
use Shared\Helpers\Pagination;
|
||||
|
||||
describe('pagination', function (): void {
|
||||
it('creates a pagination from indexes', function (): void {
|
||||
// GIVEN
|
||||
$from = 10;
|
||||
$to = 19;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofIndexes($from, $to);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(10)
|
||||
->and($pagination->to())->toBe(19)
|
||||
->and($pagination->pageSize())->toBe(10);
|
||||
});
|
||||
|
||||
it('normalizes a negative from index', function (): void {
|
||||
// GIVEN
|
||||
$from = -10;
|
||||
$to = 19;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofIndexes($from, $to);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(19);
|
||||
});
|
||||
|
||||
it('creates a pagination from page number and page size', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 1;
|
||||
$pageSize = 25;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(24)
|
||||
->and($pagination->pageSize())->toBe(25)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates the second page correctly', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 2;
|
||||
$pageSize = 25;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(25)
|
||||
->and($pagination->to())->toBe(49)
|
||||
->and($pagination->pageSize())->toBe(25)
|
||||
->and($pagination->pageNumber())->toBe(2);
|
||||
});
|
||||
|
||||
it('normalizes a page number below one', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 0;
|
||||
$pageSize = 25;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(24)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('normalizes a negative page number', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = -5;
|
||||
$pageSize = 25;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(24)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('normalizes a page size below one', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 1;
|
||||
$pageSize = 0;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->pageSize())->toBe(1)
|
||||
->and($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(0);
|
||||
});
|
||||
|
||||
it('limits the page size to the maximum page size', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 1;
|
||||
$pageSize = Pagination::MAX_PAGE_SIZE + 100;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->pageSize())
|
||||
->toBe(Pagination::MAX_PAGE_SIZE);
|
||||
});
|
||||
|
||||
it('creates a pagination from offset and limit', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 20;
|
||||
$pageSize = 10;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofOffsetAndLimit($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(20)
|
||||
->and($pagination->to())->toBe(29)
|
||||
->and($pagination->pageSize())->toBe(10);
|
||||
});
|
||||
|
||||
it('normalizes a negative offset', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = -10;
|
||||
$pageSize = 10;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofOffsetAndLimit($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(9);
|
||||
});
|
||||
|
||||
it('normalizes a limit below one', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = 10;
|
||||
$pageSize = 0;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofOffsetAndLimit($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(10)
|
||||
->and($pagination->to())->toBe(10)
|
||||
->and($pagination->pageSize())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates the first page with the default page size', function (): void {
|
||||
// GIVEN & WHEN
|
||||
$pagination = Pagination::firstPage();
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(24)
|
||||
->and($pagination->pageSize())->toBe(25)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates the first page with a custom page size', function (): void {
|
||||
// GIVEN
|
||||
$pageSize = 50;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::firstPage($pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(49)
|
||||
->and($pagination->pageSize())->toBe(50);
|
||||
});
|
||||
|
||||
it('returns its string representation', function (): void {
|
||||
// GIVEN
|
||||
$from = 10;
|
||||
$to = 19;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofIndexes($from, $to);
|
||||
|
||||
// THEN
|
||||
expect((string) $pagination)
|
||||
->toBe('Pagination{from=10, to=19}');
|
||||
});
|
||||
});
|
||||
87
tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php
Normal file
87
tests/Unit/Shared/ValueObjects/Pagination/PageNumberTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\ValueObjects\Pagination\PageNumber;
|
||||
|
||||
covers(PageNumber::class);
|
||||
|
||||
describe('PageNumber Value Object Unit Tests', function (): void {
|
||||
it('creates page number with default value', function (): void {
|
||||
// WHEN
|
||||
$page = new PageNumber();
|
||||
|
||||
// THEN
|
||||
expect($page->value)->toBe(PageNumber::DEFAULT_PAGE)
|
||||
->and($page->value())->toBe(PageNumber::DEFAULT_PAGE);
|
||||
});
|
||||
|
||||
it('creates page number with explicit value', function (): void {
|
||||
// WHEN
|
||||
$page = new PageNumber(5);
|
||||
|
||||
// THEN
|
||||
expect($page->value)->toBe(5)
|
||||
->and($page->value())->toBe(5);
|
||||
});
|
||||
|
||||
it('creates page number via of factory', function (): void {
|
||||
// WHEN
|
||||
$page = PageNumber::of(3);
|
||||
|
||||
// THEN
|
||||
expect($page->value)->toBe(3)
|
||||
->and($page->value())->toBe(3);
|
||||
});
|
||||
|
||||
it('creates page number via default factory', function (): void {
|
||||
// WHEN
|
||||
$page = PageNumber::default();
|
||||
|
||||
// THEN
|
||||
expect($page->value)->toBe(PageNumber::DEFAULT_PAGE);
|
||||
});
|
||||
|
||||
it('rejects page number less than or equal to zero', function (int $invalidValue): void {
|
||||
// WHEN / THEN
|
||||
expect(fn () => new PageNumber($invalidValue))
|
||||
->toThrow(\InvalidArgumentException::class, 'Page number must be greater than 0');
|
||||
})->with([
|
||||
'zero' => [0],
|
||||
'negative' => [-1],
|
||||
'large negative' => [-99],
|
||||
]);
|
||||
|
||||
it('implements equals correctly', function (): void {
|
||||
// GIVEN
|
||||
$page1 = new PageNumber(2);
|
||||
$page2 = new PageNumber(2);
|
||||
$page3 = new PageNumber(3);
|
||||
|
||||
// THEN
|
||||
expect($page1->equals($page2))->toBeTrue()
|
||||
->and($page1->equals($page3))->toBeFalse()
|
||||
->and($page1->equals(2))->toBeFalse()
|
||||
->and($page1->equals('2'))->toBeFalse()
|
||||
->and($page1->equals(null))->toBeFalse();
|
||||
});
|
||||
|
||||
it('converts to string via __toString', function (): void {
|
||||
// GIVEN
|
||||
$page = new PageNumber(4);
|
||||
|
||||
// THEN
|
||||
expect((string) $page)->toBe('4');
|
||||
});
|
||||
|
||||
it('serializes to json as integer', function (): void {
|
||||
// GIVEN
|
||||
$page = new PageNumber(7);
|
||||
|
||||
// THEN
|
||||
expect($page->jsonSerialize())->toBe(7)
|
||||
->and(json_encode($page))->toBe('7');
|
||||
});
|
||||
});
|
||||
101
tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php
Normal file
101
tests/Unit/Shared/ValueObjects/Pagination/PageSizeTest.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\ValueObjects\Pagination\PageSize;
|
||||
|
||||
covers(PageSize::class);
|
||||
|
||||
describe('PageSize Value Object Unit Tests', function (): void {
|
||||
it('creates page size with default value', function (): void {
|
||||
// WHEN
|
||||
$size = new PageSize();
|
||||
|
||||
// THEN
|
||||
expect($size->value)->toBe(PageSize::DEFAULT_SIZE)
|
||||
->and($size->value())->toBe(PageSize::DEFAULT_SIZE);
|
||||
});
|
||||
|
||||
it('creates page size with explicit value', function (): void {
|
||||
// WHEN
|
||||
$size = new PageSize(50);
|
||||
|
||||
// THEN
|
||||
expect($size->value)->toBe(50)
|
||||
->and($size->value())->toBe(50);
|
||||
});
|
||||
|
||||
it('creates page size via of factory', function (): void {
|
||||
// WHEN
|
||||
$size = PageSize::of(10);
|
||||
|
||||
// THEN
|
||||
expect($size->value)->toBe(10)
|
||||
->and($size->value())->toBe(10);
|
||||
});
|
||||
|
||||
it('creates page size via default factory', function (): void {
|
||||
// WHEN
|
||||
$size = PageSize::default();
|
||||
|
||||
// THEN
|
||||
expect($size->value)->toBe(PageSize::DEFAULT_SIZE);
|
||||
});
|
||||
|
||||
it('allows maximum page size', function (): void {
|
||||
// WHEN
|
||||
$size = new PageSize(PageSize::MAX_PAGE_SIZE);
|
||||
|
||||
// THEN
|
||||
expect($size->value)->toBe(PageSize::MAX_PAGE_SIZE);
|
||||
});
|
||||
|
||||
it('rejects page size less than or equal to zero', function (int $invalidValue): void {
|
||||
// WHEN / THEN
|
||||
expect(fn () => new PageSize($invalidValue))
|
||||
->toThrow(\InvalidArgumentException::class, 'Page size must be greater than 0');
|
||||
})->with([
|
||||
'zero' => [0],
|
||||
'negative' => [-1],
|
||||
'large negative' => [-100],
|
||||
]);
|
||||
|
||||
it('rejects page size exceeding maximum page size', function (): void {
|
||||
// WHEN / THEN
|
||||
expect(fn () => new PageSize(PageSize::MAX_PAGE_SIZE + 1))
|
||||
->toThrow(\InvalidArgumentException::class, 'Page size must be less than or equal to 10000');
|
||||
});
|
||||
|
||||
it('implements equals correctly', function (): void {
|
||||
// GIVEN
|
||||
$size1 = new PageSize(25);
|
||||
$size2 = new PageSize(25);
|
||||
$size3 = new PageSize(50);
|
||||
|
||||
// THEN
|
||||
expect($size1->equals($size2))->toBeTrue()
|
||||
->and($size1->equals($size3))->toBeFalse()
|
||||
->and($size1->equals(25))->toBeFalse()
|
||||
->and($size1->equals('25'))->toBeFalse()
|
||||
->and($size1->equals(null))->toBeFalse();
|
||||
});
|
||||
|
||||
it('converts to string via __toString', function (): void {
|
||||
// GIVEN
|
||||
$size = new PageSize(15);
|
||||
|
||||
// THEN
|
||||
expect((string) $size)->toBe('15');
|
||||
});
|
||||
|
||||
it('serializes to json as integer', function (): void {
|
||||
// GIVEN
|
||||
$size = new PageSize(30);
|
||||
|
||||
// THEN
|
||||
expect($size->jsonSerialize())->toBe(30)
|
||||
->and(json_encode($size))->toBe('30');
|
||||
});
|
||||
});
|
||||
59
tests/Unit/Shared/ValueObjects/Pagination/PaginatedTest.php
Normal file
59
tests/Unit/Shared/ValueObjects/Pagination/PaginatedTest.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\ValueObjects\Pagination\Paginated;
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
|
||||
covers(Paginated::class);
|
||||
|
||||
describe('Paginated Unit Tests', function (): void {
|
||||
it('stores and retrieves pagination, items, and total', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$item1 = (object) ['id' => 1, 'name' => 'First'];
|
||||
$item2 = (object) ['id' => 2, 'name' => 'Second'];
|
||||
$items = [$item1, $item2];
|
||||
$total = 42;
|
||||
|
||||
// WHEN
|
||||
$paginated = new Paginated($pagination, $items, $total);
|
||||
|
||||
// THEN
|
||||
expect($paginated->getPagination())->toBe($pagination)
|
||||
->and($paginated->getTotal())->toBe(42)
|
||||
->and($paginated->getItems())->toHaveCount(2)
|
||||
->and($paginated->getItems()[0]->id)->toBe(1)
|
||||
->and($paginated->getItems()[1]->id)->toBe(2);
|
||||
});
|
||||
|
||||
it('returns cloned items so original objects are not mutated externally', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
$original = (object) ['name' => 'Original'];
|
||||
$paginated = new Paginated($pagination, [$original], 1);
|
||||
|
||||
// WHEN
|
||||
$retrieved = $paginated->getItems();
|
||||
$retrieved[0]->name = 'Mutated';
|
||||
|
||||
// THEN
|
||||
expect($paginated->getItems()[0]->name)->toBe('Original')
|
||||
->and($retrieved[0])->not->toBe($paginated->getItems()[0]);
|
||||
});
|
||||
|
||||
it('handles empty items array', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::firstPage();
|
||||
|
||||
// WHEN
|
||||
$paginated = new Paginated($pagination, [], 0);
|
||||
|
||||
// THEN
|
||||
expect($paginated->getItems())->toBeEmpty()
|
||||
->and($paginated->getTotal())->toBe(0)
|
||||
->and($paginated->getPagination())->toBe($pagination);
|
||||
});
|
||||
});
|
||||
151
tests/Unit/Shared/ValueObjects/Pagination/PaginationTest.php
Normal file
151
tests/Unit/Shared/ValueObjects/Pagination/PaginationTest.php
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\Shared\ValueObjects\Pagination;
|
||||
|
||||
use Shared\ValueObjects\Pagination\Pagination;
|
||||
use Shared\ValueObjects\Pagination\PageNumber;
|
||||
use Shared\ValueObjects\Pagination\PageSize;
|
||||
|
||||
covers(Pagination::class);
|
||||
|
||||
describe('Pagination Value Object Unit Tests', function (): void {
|
||||
it('can be created from page and page size', function (): void {
|
||||
// GIVEN
|
||||
$page = new PageNumber(2);
|
||||
$size = new PageSize(20);
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($page, $size);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(20)
|
||||
->and($pagination->to())->toBe(39)
|
||||
->and($pagination->pageSize())->toBe(20)
|
||||
->and($pagination->pageNumber())->toBe(2);
|
||||
});
|
||||
|
||||
it('can be created from offset and limit', function (): void {
|
||||
// GIVEN
|
||||
$offset = 20;
|
||||
$limit = 10;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofOffsetAndLimit($offset, $limit);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(20)
|
||||
->and($pagination->to())->toBe(29)
|
||||
->and($pagination->pageSize())->toBe(10)
|
||||
->and($pagination->pageNumber())->toBe(3);
|
||||
});
|
||||
|
||||
it('can be created from explicit indexes using ofIndexes', function (): void {
|
||||
// GIVEN
|
||||
$from = 5;
|
||||
$to = 14;
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofIndexes($from, $to);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(5)
|
||||
->and($pagination->to())->toBe(14)
|
||||
->and($pagination->pageSize())->toBe(10);
|
||||
});
|
||||
|
||||
it('can be created using first page helper with default size', function (): void {
|
||||
// WHEN
|
||||
$pagination = Pagination::firstPage();
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(24)
|
||||
->and($pagination->pageSize())->toBe(25)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('can be created using first page helper with explicit size', function (): void {
|
||||
// GIVEN
|
||||
$pageSize = new PageSize(15);
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::firstPage($pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(0)
|
||||
->and($pagination->to())->toBe(14)
|
||||
->and($pagination->pageSize())->toBe(15)
|
||||
->and($pagination->pageNumber())->toBe(1);
|
||||
});
|
||||
|
||||
it('formats pagination as string representation via __toString', function (): void {
|
||||
// GIVEN
|
||||
$pagination = Pagination::ofIndexes(0, 9);
|
||||
|
||||
// WHEN
|
||||
$stringRepresentation = (string) $pagination;
|
||||
|
||||
// THEN
|
||||
expect($stringRepresentation)->toBe('Pagination{from=0, to=9}');
|
||||
});
|
||||
|
||||
it('clamps invalid constructor-level values via ofIndexes', function (int $from, int $to, int $expectedFrom, int $expectedTo): void {
|
||||
// GIVEN / WHEN
|
||||
$pagination = Pagination::ofIndexes($from, $to);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe($expectedFrom)
|
||||
->and($pagination->to())->toBe($expectedTo);
|
||||
})->with([
|
||||
'negative from is clamped to 0' => [-1, 10, 0, 10],
|
||||
'to below from+1 is clamped to from+1' => [10, 5, 10, 11],
|
||||
'to equal from is clamped to from+1' => [10, 10, 10, 11],
|
||||
'oversized range is clamped to max' => [0, 10001, 0, 10000],
|
||||
]);
|
||||
|
||||
it('clamps invalid ofOffsetAndLimit parameters', function (int $offset, int $limit, int $expectedFrom, int $expectedSize): void {
|
||||
// GIVEN / WHEN
|
||||
$pagination = Pagination::ofOffsetAndLimit($offset, $limit);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe($expectedFrom)
|
||||
->and($pagination->pageSize())->toBe($expectedSize);
|
||||
})->with([
|
||||
'negative offset clamped to 0' => [-1, 10, 0, 10],
|
||||
'limit 0 clamped to 1' => [0, 0, 0, 1],
|
||||
'negative limit clamped to 1' => [0, -5, 0, 1],
|
||||
'oversized limit clamped' => [0, 20000, 0, 10000],
|
||||
]);
|
||||
|
||||
it('creates pagination using ofPage with Value Objects', function (): void {
|
||||
// GIVEN
|
||||
$pageNumber = new PageNumber(3);
|
||||
$pageSize = new PageSize(15);
|
||||
|
||||
// WHEN
|
||||
$pagination = Pagination::ofPage($pageNumber, $pageSize);
|
||||
|
||||
// THEN
|
||||
expect($pagination->from())->toBe(30)
|
||||
->and($pagination->to())->toBe(44)
|
||||
->and($pagination->pageSize())->toBe(15)
|
||||
->and($pagination->pageNumber())->toBe(3)
|
||||
->and($pagination->pageNumberVo())->toEqual($pageNumber)
|
||||
->and($pagination->pageSizeVo())->toEqual($pageSize);
|
||||
});
|
||||
|
||||
it('checks equality with another Pagination instance', function (): void {
|
||||
// GIVEN
|
||||
$p1 = Pagination::ofPage(new PageNumber(1), new PageSize(20));
|
||||
$p2 = Pagination::ofPage(new PageNumber(1), new PageSize(20));
|
||||
$p3 = Pagination::ofPage(new PageNumber(2), new PageSize(20));
|
||||
|
||||
// THEN
|
||||
expect($p1->equals($p2))->toBeTrue()
|
||||
->and($p1->equals($p3))->toBeFalse()
|
||||
->and($p1->equals(null))->toBeFalse()
|
||||
->and($p1->equals('pagination'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue