The Paginator class handles two distinct jobs: calculating pagination metadata from a total item count, and rendering Bootstrap-compatible page navigation HTML. For database queries, QueryBuilder::paginate() runs both the count and data queries automatically and returns the same object shape — so renderLinks() works with either source.
- Class:
JiFramework\Core\Utilities\Paginator - Access:
$app->paginator - Lazy-loaded — instantiated on first access
// Database pagination — one method call handles everything
$result = $app->db->table('users')->where('active', 1)->orderBy('name')->paginate(15);
foreach ($result->data as $user) { ... }
echo $result->totalItems . ' users across ' . $result->totalPages . ' pages';
echo $app->paginator->renderLinks($result, '/users');
// Non-database pagination (array, API results, etc.)
$items = $myApiClient->getItems(); // returns all items
$total = count($items);
$pag = $app->paginator->paginate(20, $total);
$page = array_slice($items, $pag->offset, $pag->perPage);
echo $app->paginator->renderLinks($pag, '/items');
paginate(int $perPage, int $totalItems, array $options = []): object
Calculate all pagination metadata from a total item count. Use this when you already have the total (e.g. from a manual COUNT query, an array, or an external API). For database queries, prefer QueryBuilder::paginate() which handles the count automatically.
$perPage— (int) Number of items per page. Clamped to a minimum of 1.$totalItems— (int) Total number of items across all pages.$options— (array) Optional overrides:currentPage— (int) Override the current page. Defaults to$_GET['page'], then1.queryParams— (array) Extra query parameters to carry through page links. Defaults to$_GET(minus thepagekey).
Returns: object with the following properties:
currentPage— (int) The resolved current page number, clamped to[1, totalPages].totalPages— (int) Total number of pages. Always at least1.totalItems— (int) The total item count passed in.perPage— (int) Items per page.offset— (int) Row offset forLIMIT … OFFSETqueries:(currentPage - 1) * perPage.hasNext— (bool)truewhen there is a next page.hasPrevious— (bool)truewhen there is a previous page.nextPage— (int) Next page number (clamped tototalPages).previousPage— (int) Previous page number (clamped to1).queryParams— (string) URL-encoded extra params with&separators and a trailing&, ready to prepend to thepage=parameter in links. Empty string when there are no extra params.
$total = $app->db->table('products')->where('active', 1)->count();
$pag = $app->paginator->paginate(20, $total);
// Use the offset to slice your data query
$products = $app->db->table('products')
->where('active', 1)
->limit($pag->perPage)
->offset($pag->offset)
->get();
echo "Page {$pag->currentPage} of {$pag->totalPages}";
echo $app->paginator->renderLinks($pag, '/products');
QueryBuilder::paginate(int $perPage, ?int $page = null): object
Paginate a database query in one call. Internally runs two queries — a COUNT(*) using the same WHERE / JOIN / GROUP BY conditions, then a SELECT with LIMIT and OFFSET — and returns a single result object.
$perPage— (int) Number of rows per page.$page— (int|null) Current page number. Reads$_GET['page']whennull.
Returns: object — the same shape as Paginator::paginate() plus one additional property:
data— (array) The rows for the current page.
Because the return shape is identical, Paginator::renderLinks() works directly with the result.
// Simplest case — reads page number from $_GET['page'] automatically
$result = $app->db->table('orders')
->where('status', 'pending')
->orderByDesc('created_at')
->paginate(20);
foreach ($result->data as $order) {
echo $order['id'] . ': ' . $order['total'];
}
echo "Showing page {$result->currentPage} of {$result->totalPages}";
echo "({$result->totalItems} total orders)";
echo $app->paginator->renderLinks($result, '/admin/orders');
// Explicit page number (e.g. from a REST API route /api/products?page=3)
$result = $app->db->table('products')->paginate(15, 3);
How it works:
- Snapshots the current query state (wheres, bindings, joins, columns, ordering).
- Runs
COUNT(*)using allWHEREconditions — gives the true total. - Restores the query state.
- Runs
SELECT … LIMIT … OFFSETfor the requested page. - Returns a unified result object.
renderLinks(object $paginationData, string $baseUrl, int $maxPagesToShow = 5): string
Generate a Bootstrap-compatible pagination nav as an HTML string. Accepts the result object from either Paginator::paginate() or QueryBuilder::paginate(). Returns an empty string when there is only one page, so you can echo it unconditionally.
$paginationData— (object) The pagination result object.$baseUrl— (string) Base URL for page links, e.g.'/users'or'/search'. Automatically HTML-escaped.$maxPagesToShow— (int) Maximum number of numbered page buttons in the sliding window. Default:5.
Returns: string — an HTML <nav> block, or empty string when totalPages <= 1.
Output structure:
- Wraps in
<nav aria-label="Page navigation"><ul class="pagination">— compatible with Bootstrap 4 and 5. - Previous / next buttons use
«/»and are disabled on the first / last page. - The active page has
class="page-item active". - All
hrefvalues use&as the query string separator (valid HTML). $baseUrlis run throughhtmlspecialchars()— safe against XSS.
$result = $app->db->table('users')->paginate(15);
// Works unconditionally — returns "" when there is only 1 page
echo $app->paginator->renderLinks($result, '/users');
// Custom window size: show up to 7 page numbers
echo $app->paginator->renderLinks($result, '/users', 7);
// The base URL can be a path with query params already attached
// — extra params from $_GET (e.g. search=foo) are carried through automatically
echo $app->paginator->renderLinks($result, '/search');
// → href="/search?search=foo&page=2"
Standard admin list page
// pages/admin/users.php
$result = $app->db->table('users')
->where('active', 1)
->orderBy('name')
->paginate(25);
<p>Showing page <?= $result->currentPage ?> of <?= $result->totalPages ?>
(<?= $result->totalItems ?> users)</p>
<table>
<?php foreach ($result->data as $user): ?>
<tr><td><?= htmlspecialchars($user['name']) ?></td></tr>
<?php endforeach; ?>
</table>
<?= $app->paginator->renderLinks($result, '/admin/users') ?>
Search results with preserved query string
// pages/search.php
$q = trim($_GET['q'] ?? '');
$result = $app->db->table('products')
->where('name', 'LIKE', '%' . $q . '%')
->orderBy('name')
->paginate(20);
// renderLinks() carries $_GET['q'] through page links automatically
// → href="/search?q=laptop&page=2"
echo $app->paginator->renderLinks($result, '/search');
API endpoint returning paginated JSON
// api/products.php
$page = (int) ($_GET['page'] ?? 1);
$perPage = (int) ($_GET['per_page'] ?? 15);
$result = $app->db->table('products')->paginate($perPage, $page);
$app->json(200, [
'data' => $result->data,
'meta' => [
'current_page' => $result->currentPage,
'total_pages' => $result->totalPages,
'total_items' => $result->totalItems,
'per_page' => $result->perPage,
'has_next' => $result->hasNext,
'has_previous' => $result->hasPrevious,
],
]);
Paginating a PHP array (non-database)
// Paginate any array — useful for in-memory lists or cached data
$allItems = $cache->get('product_list') ?? [];
$total = count($allItems);
$pag = $app->paginator->paginate(10, $total);
$page = array_slice($allItems, $pag->offset, $pag->perPage);
echo "Page {$pag->currentPage} of {$pag->totalPages}";
foreach ($page as $item) { ... }
echo $app->paginator->renderLinks($pag, '/products');
Checking navigation state manually
$result = $app->db->table('posts')->paginate(10);
if ($result->hasPrevious) {
echo '<a href="/posts?page=' . $result->previousPage . '">← Newer</a>';
}
if ($result->hasNext) {
echo '<a href="/posts?page=' . $result->nextPage . '">Older →</a>';
}