Paginator

Overview

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()

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'], then 1.
    • queryParams(array) Extra query parameters to carry through page links. Defaults to $_GET (minus the page key).

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 least 1.
  • totalItems(int) The total item count passed in.
  • perPage(int) Items per page.
  • offset(int) Row offset for LIMIT … OFFSET queries: (currentPage - 1) * perPage.
  • hasNext(bool) true when there is a next page.
  • hasPrevious(bool) true when there is a previous page.
  • nextPage(int) Next page number (clamped to totalPages).
  • previousPage(int) Previous page number (clamped to 1).
  • queryParams(string) URL-encoded extra params with & separators and a trailing &, ready to prepend to the page= 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()

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'] when null.

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:

  1. Snapshots the current query state (wheres, bindings, joins, columns, ordering).
  2. Runs COUNT(*) using all WHERE conditions — gives the true total.
  3. Restores the query state.
  4. Runs SELECT … LIMIT … OFFSET for the requested page.
  5. Returns a unified result object.

Examples

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>';
}