<?php
namespace Domain\ValueObject;
/**
* Pagination Value Object for consistent pagination handling.
*
* Immutable object that encapsulates pagination logic.
* Use fromRequest() for creating from HTTP request parameters.
*/
final class Pagination
{
public function __construct(
public readonly int $page,
public readonly int $limit,
public readonly int $offset,
public readonly int $totalCount = 0
) {
}
/**
* Create pagination from page and limit values.
*
* @param int $page Current page number (1-based)
* @param int $limit Items per page
* @param int $maxLimit Maximum allowed limit
*/
public static function create(int $page = 1, int $limit = 50, int $maxLimit = 100): self
{
$page = max(1, $page);
$limit = min($maxLimit, max(1, $limit));
return new self($page, $limit, ($page - 1) * $limit);
}
/**
* Create pagination using RequestParams provider.
* This is infrastructure-agnostic by accepting a callable.
*
* @param int $defaultLimit Default items per page
* @param int $maxLimit Maximum allowed limit
* @param callable(string, int): int $paramProvider Function to get int params
*/
public static function fromParams(
int $defaultLimit = 50,
int $maxLimit = 100,
?callable $paramProvider = null
): self {
// Default provider uses global request context if no provider given
if ($paramProvider === null) {
$paramProvider = static function (string $key, int $default): int {
return $default;
};
}
$page = max(1, $paramProvider('page', 1));
$limit = min($maxLimit, max(1, $paramProvider('limit', $defaultLimit)));
return new self($page, $limit, ($page - 1) * $limit);
}
/**
* Create pagination from request parameters.
* Convenience method for controllers.
*
* @param int $defaultLimit Default items per page
* @param int $maxLimit Maximum allowed limit
*/
public static function fromRequest(int $defaultLimit = 50, int $maxLimit = 100): self
{
return self::fromParams($defaultLimit, $maxLimit, static function (string $key, int $default): int {
$value = filter_input(INPUT_GET, $key, FILTER_VALIDATE_INT);
return $value !== false && $value !== null ? $value : $default;
});
}
/**
* Create new instance with total count set.
*/
public function withTotal(int $count): self
{
return new self($this->page, $this->limit, $this->offset, $count);
}
/**
* Calculate total number of pages.
*/
public function totalPages(): int
{
if ($this->totalCount === 0) {
return 0;
}
return (int) ceil($this->totalCount / $this->limit);
}
/**
* Check if there is a next page.
*/
public function hasNextPage(): bool
{
return $this->page < $this->totalPages();
}
/**
* Check if there is a previous page.
*/
public function hasPrevPage(): bool
{
return $this->page > 1;
}
/**
* Get next page number (or current if at last page).
*/
public function nextPage(): int
{
return $this->hasNextPage() ? $this->page + 1 : $this->page;
}
/**
* Get previous page number (or 1 if at first page).
*/
public function prevPage(): int
{
return $this->hasPrevPage() ? $this->page - 1 : 1;
}
/**
* Get range of visible page numbers for pagination UI.
*
* @param int $range Number of pages to show around current page
* @return array<int>
*/
public function getPageRange(int $range = 2): array
{
$totalPages = $this->totalPages();
if ($totalPages === 0) {
return [];
}
$start = max(1, $this->page - $range);
$end = min($totalPages, $this->page + $range);
return range($start, $end);
}
/**
* Convert to array for template usage.
*
* @return array{page: int, limit: int, offset: int, totalCount: int, totalPages: int, hasNext: bool, hasPrev: bool}
*/
public function toArray(): array
{
return [
'page' => $this->page,
'limit' => $this->limit,
'offset' => $this->offset,
'totalCount' => $this->totalCount,
'totalPages' => $this->totalPages(),
'hasNext' => $this->hasNextPage(),
'hasPrev' => $this->hasPrevPage(),
];
}
}