README
Lightweight PHP pagination trait for database objects and collections.
Installation
composer require timefrontiers/php-pagination
Quick Start
use TimeFrontiers\Helper\Pagination;
class UserRepository {
use Pagination;
public function getUsers():array {
// Set pagination from request (?page=2&per_page=25)
$this->fromRequest();
// Get total count
$this->setTotalCount($this->countAll());
// Query with pagination
$sql = "SELECT * FROM users {$this->limitClause()}";
return $this->query($sql);
}
}
$repo = new UserRepository();
$users = $repo->getUsers();
// Display info
echo "Page {$repo->currentPage()} of {$repo->totalPages()}";
echo "Showing {$repo->itemStart()}-{$repo->itemEnd()} of {$repo->totalCount()}";
Usage with DatabaseObject
use TimeFrontiers\Helper\DatabaseObject;
use TimeFrontiers\Helper\Pagination;
class User {
use DatabaseObject, Pagination;
protected static string $_table_name = 'users';
}
// In controller
$user = new User();
$user->fromRequest()->setTotalCount(User::countAll());
$users = User::findBySql(
"SELECT * FROM users ORDER BY created_at DESC {$user->limitClause()}"
);
// Pass pagination to view
$pagination = $user->paginationToArray();
Configuration
Set Page
$this->setPage(3); // Go to page 3
$this->setPerPage(50); // 50 items per page
$this->setTotalCount(1000); // 1000 total items
From Request
// Uses $_GET['page'] and $_GET['per_page']
$this->fromRequest();
// Custom parameter names
$this->fromRequest('p', 'limit', 25);
// Uses $_GET['p'] for page, $_GET['limit'] for per_page, default 25
Getters
$this->currentPage(); // int: Current page number
$this->perPage(); // int: Items per page
$this->totalCount(); // int: Total items
$this->totalPages(); // int: Total pages
$this->offset(); // int: SQL OFFSET value
Navigation
$this->previousPage(); // int: Previous page number
$this->nextPage(); // int: Next page number
$this->hasPreviousPage(); // bool: Has previous?
$this->hasNextPage(); // bool: Has next?
$this->isFirstPage(); // bool: On first page?
$this->isLastPage(); // bool: On last page?
Item Range
// Page 2, 20 per page, 95 total items
$this->itemStart(); // 21
$this->itemEnd(); // 40
$this->itemRange(); // [21, 40]
// "Showing 21-40 of 95 results"
echo "Showing {$this->itemStart()}-{$this->itemEnd()} of {$this->totalCount()} results";
State Checks
$this->isEmpty(); // bool: No results?
$this->isNotEmpty(); // bool: Has results?
$this->hasPages(); // bool: More than 1 page?
$this->isValidPage(5); // bool: Is page 5 valid?
SQL Helpers
// LIMIT clause
$sql = "SELECT * FROM users {$this->limitClause()}";
// "SELECT * FROM users LIMIT 20 OFFSET 40"
// As array
$limit = $this->limitOffset();
// ['limit' => 20, 'offset' => 40]
Page Range for UI
// Generate page numbers for pagination UI
// Current page: 5, Total: 20
$pages = $this->pageRange(2);
// [1, null, 3, 4, 5, 6, 7, null, 20]
// null = ellipsis (...)
// Render pagination
foreach ($this->pageRange(2) as $page) {
if ($page === null) {
echo '<span>...</span>';
} else {
$active = $page === $this->currentPage() ? 'active' : '';
echo "<a href='?page={$page}' class='{$active}'>{$page}</a>";
}
}
// Get all pages (no ellipsis)
$this->pages(); // [1, 2, 3, 4, 5, ...]
URL Helpers
// Build URL for specific page
$this->pageUrl(3); // "/users?page=3&search=john"
// Previous/Next URLs
$this->previousPageUrl(); // "/users?page=1" or null
$this->nextPageUrl(); // "/users?page=3" or null
// Custom base URL
$this->pageUrl(5, '/api/users', 'p'); // "/api/users?p=5"
API Response
// Basic metadata
$meta = $this->paginationToArray();
/*
[
'current_page' => 2,
'per_page' => 20,
'total' => 95,
'total_pages' => 5,
'from' => 21,
'to' => 40,
'has_more' => true,
'is_first_page' => false,
'is_last_page' => false,
]
*/
// With links
$meta = $this->paginationMeta('/api/users');
/*
[
'current_page' => 2,
...
'links' => [
'first' => '/api/users?page=1',
'last' => '/api/users?page=5',
'prev' => '/api/users?page=1',
'next' => '/api/users?page=3',
]
]
*/
// API response
return [
'data' => $users,
'meta' => $this->paginationMeta(),
];
Complete Example
Repository Pattern
class ArticleRepository {
use Pagination;
private PDO $db;
public function __construct(PDO $db) {
$this->db = $db;
}
public function getPaginated(array $filters = []):array {
// Configure from request
$this->fromRequest();
// Build query
$where = $this->buildWhere($filters);
// Get total count
$countSql = "SELECT COUNT(*) FROM articles {$where}";
$total = $this->db->query($countSql)->fetchColumn();
$this->setTotalCount((int)$total);
// Get paginated results
$sql = "SELECT * FROM articles {$where}
ORDER BY created_at DESC
{$this->limitClause()}";
$articles = $this->db->query($sql)->fetchAll();
return [
'data' => $articles,
'meta' => $this->paginationToArray(),
];
}
}
Controller
$repo = new ArticleRepository($pdo);
$result = $repo->getPaginated(['status' => 'published']);
// JSON API
header('Content-Type: application/json');
echo json_encode($result);
Blade-style View
<!-- Results info -->
<p>
Showing <?= $pagination->itemStart() ?>-<?= $pagination->itemEnd() ?>
of <?= $pagination->totalCount() ?> results
</p>
<!-- Pagination links -->
<nav>
<?php if ($pagination->hasPreviousPage()): ?>
<a href="<?= $pagination->previousPageUrl() ?>">← Previous</a>
<?php endif; ?>
<?php foreach ($pagination->pageRange(2) as $page): ?>
<?php if ($page === null): ?>
<span>...</span>
<?php else: ?>
<a href="<?= $pagination->pageUrl($page) ?>"
class="<?= $page === $pagination->currentPage() ? 'active' : '' ?>">
<?= $page ?>
</a>
<?php endif; ?>
<?php endforeach; ?>
<?php if ($pagination->hasNextPage()): ?>
<a href="<?= $pagination->nextPageUrl() ?>">Next →</a>
<?php endif; ?>
</nav>
Method Reference
Setters
| Method |
Description |
setPage(int $page) |
Set current page |
setPerPage(int $per_page) |
Set items per page (1-1000) |
setTotalCount(int $count) |
Set total item count |
fromRequest($page_key, $per_page_key, $default) |
Load from $_GET |
Getters
| Method |
Returns |
Description |
currentPage() |
int |
Current page number |
perPage() |
int |
Items per page |
totalCount() |
int |
Total items |
totalPages() |
int |
Total pages |
offset() |
int |
SQL OFFSET value |
previousPage() |
int |
Previous page number |
nextPage() |
int |
Next page number |
itemStart() |
int |
First item number on page |
itemEnd() |
int |
Last item number on page |
itemRange() |
array |
[start, end] |
Checks
| Method |
Returns |
Description |
isEmpty() |
bool |
No results |
isNotEmpty() |
bool |
Has results |
hasPages() |
bool |
Multiple pages exist |
hasPreviousPage() |
bool |
Previous page exists |
hasNextPage() |
bool |
Next page exists |
isFirstPage() |
bool |
On first page |
isLastPage() |
bool |
On last page |
isValidPage(int) |
bool |
Page number is valid |
SQL & URLs
| Method |
Returns |
Description |
limitClause() |
string |
"LIMIT X OFFSET Y" |
limitOffset() |
array |
['limit' => X, 'offset' => Y] |
pageUrl(int, ?string, string) |
string |
URL for page |
previousPageUrl() |
?string |
Previous page URL |
nextPageUrl() |
?string |
Next page URL |
pageRange(int) |
array |
Page numbers with ellipsis |
pages() |
array |
All page numbers |
Export
| Method |
Returns |
Description |
paginationToArray() |
array |
Pagination metadata |
paginationMeta(?string, string) |
array |
Metadata with links |
License
MIT