PaginationHelper slices any array or collection into pages and produces the surrounding page numbers needed to render a pagination UI. It is designed for HTML views — pair it with the built-in pagination component to get a fully working page navigator.
For API endpoints use Cursor Pagination instead.
use PHP_SF\System\Classes\Helpers\PaginationHelper;
$page = (int) $this->request->query->get( 'page', 1 );
$players = User::findAll(); // array or collection
$ph = new PaginationHelper(
arr: $players,
messagesPerPage: 20,
page: $page,
);
return $this->render( players_page::class, [
'players' => $ph->paginate(), // items for the current page
'pages' => $ph->getPages(), // page numbers for the nav component
] );
new PaginationHelper(
iterable $arr,
int $messagesPerPage = 10,
int $page = 1,
)
| Parameter | Default | Description |
|---|---|---|
$arr |
— | Any array or object with a count() method |
$messagesPerPage |
10 |
Items shown per page |
$page |
1 |
Current page number (1-based) |
If $page exceeds the total number of pages, it is clamped to the last page automatically.
public function paginate(): array
Returns only the items belonging to the current page, preserving the original array keys.
$ph = new PaginationHelper( $items, 10, $page );
$current = $ph->paginate(); // items for page $page
public function getTotalPages(): int
Total number of pages. Useful for displaying "Page 3 of 12" in a view.
$total = $ph->getTotalPages();
public function getPages(): array
Returns an associative array of page numbers surrounding the current page — at most two to the left, the current, and two to the right, plus first and last if the current page is far enough from either end:
[
'first' => 1, // present if current > 3
'page2left' => 3,
'page1left' => 4,
'current' => 5,
'page1right' => 6,
'page2right' => 7,
'last' => 20, // present if current + 2 < total
]
Keys are omitted when they would fall outside the valid range. On a single-page result, the array is empty.
PaginationHelper itself only computes page numbers — it does not render anything. The view component that turns those numbers into a Bootstrap-compatible <ul> navigator lives in the application, not in the framework.
The framework ships a ready-to-use template at templates/Components/pagination.php (namespace PHP_SF\Templates\Components). You can reuse it as-is by referencing PHP_SF\Templates\Components\pagination directly in your view — no copy or namespace rename is required. If the framework component doesn't fit your needs (different markup, CSS, or behaviour), copy it into your project's templates/Components/pagination.php under App\View\Components and adjust freely.
| Prop | Type | Description |
|---|---|---|
pages |
array |
Output of PaginationHelper::getPages() |
onclickFunction |
string |
Name of a JS function to call with the page number when a button is clicked |
// In the view (AbstractView::show()):
$this->import( pagination::class, [
'pages' => $this->pages,
'onclickFunction' => 'changePage',
] );
The component renders nothing when $pages is empty (single page or no items) — no need to guard the import call.
The component calls onclickFunction(pageNumber) on each clickable button. Define the function in your page JS before the component renders:
function changePage(page) {
window.location.href = updateQueryParam( window.location.href, 'page', page );
}
For AJAX-powered tables that reload without a full page navigation:
function changePage(page) {
loadPlayersPage( page );
}
The component renders a <ul class="page-numbers pagination pagination-sm"> with <li> elements. Each button carries one of two CSS classes:
| Class | Meaning |
|---|---|
pagination-link enabled |
Clickable — has an onclick handler |
pagination-link disabled |
Not clickable — used for the current page and ... ellipsis gaps |
The current page <li> additionally gets the current class.
Example output for page 5 of 20:
<ul class="page-numbers pagination pagination-sm">
<li class="pagination-link enabled" onclick="changePage(1)"><span>1</span></li>
<li class="pagination-link disabled"><span>...</span></li>
<li class="pagination-link enabled" onclick="changePage(3)"><span>3</span></li>
<li class="pagination-link enabled" onclick="changePage(4)"><span>4</span></li>
<li class="pagination-link disabled current"><span>5</span></li>
<li class="pagination-link enabled" onclick="changePage(6)"><span>6</span></li>
<li class="pagination-link enabled" onclick="changePage(7)"><span>7</span></li>
<li class="pagination-link disabled"><span>...</span></li>
<li class="pagination-link enabled" onclick="changePage(20)"><span>20</span></li>
</ul>
The component also emits a small inline <script> that applies border-radius to the first and last visible buttons — rounding the ends of the pill shape without requiring extra CSS.
The component uses Bootstrap's pagination and pagination-sm classes. Add your own rules targeting .pagination-link to override appearance:
.pagination-link.enabled:hover { background: #2a2a3e; }
.pagination-link.current { font-weight: bold; color: #fff; }
Controller:
$ph = new PaginationHelper( $players, messagesPerPage: 25, page: $page );
return $this->render( players_page::class, [
'players' => $ph->paginate(),
'pages' => $ph->getPages(),
] );
View:
// render the list
foreach ( $this->players as $player ) { ... }
// render the navigator
$this->import( pagination::class, [
'pages' => $this->pages,
'onclickFunction' => 'changePage',
] );
Page JS:
function changePage(page) {
window.location.href = updateQueryParam( window.location.href, 'page', page );
}
The controller renders three pieces of state — all simple primitives, nothing ORM-bound:
return $this->render( players_page::class, [
'players' => $ph->paginate(), // array<array-key, User> — only this page's rows
'pages' => $ph->getPages(), // array<string, int>|[] — see below
'total_pages' => $ph->getTotalPages(),// int — useful for "Page 3 of 12"
'current_page' => $page, // int — clamped to last page internally
] );
The pages shape (already documented under getPages()) is the only input the view needs to render the navigator — typically the controller also forwards total_pages / current_page if the view prints them.
A realistic end-to-end controller — same shape as the host project's PHP_SF routes (#[Route] attribute + $this->request->query + $this->render()):
use App\Http\Middleware\auth;
use PHP_SF\System\Classes\Abstracts\AbstractController;
use PHP_SF\System\Classes\Helpers\PaginationHelper;
use PHP_SF\System\Router\Route;
use App\View\Pages\PlayersPage;
#[Route( url: 'players', httpMethod: 'GET', middleware: auth::class )]
final class PlayersController extends AbstractController
{
public function list(): Response
{
$page = max( 1, (int) $this->request->query->get( 'page', 1 ) );
$players = User::findAll();
$ph = new PaginationHelper( $players, messagesPerPage: 25, page: $page );
return $this->render( PlayersPage::class, [
'players' => $ph->paginate(),
'pages' => $ph->getPages(),
'total_pages' => $ph->getTotalPages(),
'current_page' => $page,
] );
}
}
The corresponding view (App\View\Pages\PlayersPage) renders the rows with a foreach over $this->players, then imports the navigator at the bottom of the page:
public function show(): void {
?>
<table>
<?php foreach ( $this->players as $player ) : ?>
<tr><td><?= htmlspecialchars( $player->login ) ?></td></tr>
<?php endforeach ?>
</table>
<?php
$this->import( pagination::class, [
'pages' => $this->pages,
'onclickFunction' => 'changePage',
] );
}
PaginationHelper is pure logic over an iterable — the right home is a unit test (or a Codeception Cest that boots the helper directly without spinning the full request kernel). The sketch below asserts the three documented invariants: clamping, the paginate() slice, and the getPages() shape:
// tests/Unit/PaginationHelperCest.php
namespace Tests\Unit;
use Codeception\Scenario;
use PHP_SF\System\Classes\Helpers\PaginationHelper;
final class PaginationHelperCest
{
public function paginateReturnsOnlyTheCurrentPage( Scenario $scenario ): void
{
$items = range( 1, 100 );
$ph = new PaginationHelper( $items, messagesPerPage: 10, page: 3 );
$scenario->assertEquals( range( 21, 30 ), array_values( $ph->paginate() ) );
}
public function pageAboveTotalIsClampedToLastPage( Scenario $scenario ): void
{
$items = range( 1, 25 );
$ph = new PaginationHelper( $items, messagesPerPage: 10, page: 999 );
$scenario->assertEquals( 3, $ph->getTotalPages() );
$scenario->assertEquals( range( 21, 25 ), array_values( $ph->paginate() ) );
}
public function getPagesReturnsSurroundingWindow( Scenario $scenario ): void
{
$ph = new PaginationHelper( range( 1, 200 ), 10, page: 10 );
$scenario->assertEquals( [
'first' => 1,
'page2left' => 8,
'page1left' => 9,
'current' => 10,
'page1right' => 11,
'page2right' => 12,
'last' => 20,
], $ph->getPages() );
}
public function getPagesIsEmptyForASinglePage( Scenario $scenario ): void
{
$ph = new PaginationHelper( range( 1, 5 ), 10, page: 1 );
$scenario->assertEmpty( $ph->getPages() );
}
}
For an end-to-end test that exercises the component, render the view in a Codeception Functional test, walk the response DOM for <ul class="page-numbers">, and click the second <li> to assert that ?page=N is appended to the URL.