ashleydawson/simple-pagination
Framework-agnostic pagination library for PHP. Provide callbacks to count total items and fetch a slice (offset/length), and it returns a Pagination object with items plus page metadata and ranges. Works with arrays, DB lists, Doctrine, Solr, and more.
Installation:
composer require ashleydawson/simple-pagination
Basic Setup:
use AshleyDawson\SimplePagination\Paginator;
$paginator = new Paginator();
$paginator->setItemsPerPage(10);
$paginator->setPagesInRange(5);
Define Callbacks:
$items = ['item1', 'item2', ..., 'itemN'];
$paginator->setItemTotalCallback(function () use ($items) {
return count($items);
});
$paginator->setSliceCallback(function ($offset, $length) use ($items) {
return array_slice($items, $offset, $length);
});
Paginate and Use:
$pagination = $paginator->paginate((int) request('page', 1));
// Render items
foreach ($pagination->getItems() as $item) {
echo $item;
}
// Render pagination links
foreach ($pagination->getPages() as $page) {
echo '<a href="?page=' . $page . '">' . $page . '</a> ';
}
Replace Laravel’s built-in pagination (e.g., App\Models\Post::paginate(10)) with this package for custom data sources (e.g., external APIs, non-Eloquent collections, or complex queries). Example:
$paginator = new Paginator();
$paginator->setItemsPerPage(15);
// Custom API data fetch
$paginator->setItemTotalCallback(function () {
return $this->apiClient->countItems();
});
$paginator->setSliceCallback(function ($offset, $length) {
return $this->apiClient->fetchItems($offset, $length);
});
$pagination = $paginator->paginate(request('page'));
Array Pagination: Use for in-memory collections (e.g., cached results, filtered arrays).
$array = [1, 2, 3, ..., 100];
$paginator->setSliceCallback(fn($offset, $length) => array_slice($array, $offset, $length));
Database Pagination:
Replace LIMIT/OFFSET with this package for flexible pagination (e.g., dynamic itemsPerPage).
$paginator->setSliceCallback(function ($offset, $length) {
return DB::table('posts')
->orderBy('created_at')
->skip($offset)
->take($length)
->get();
});
API/External Data: Wrap third-party APIs (e.g., Stripe, Elasticsearch) with callbacks.
$paginator->setItemTotalCallback(function () {
return $this->stripe->countCustomers();
});
$paginator->setSliceCallback(function ($offset, $length) {
return $this->stripe->listCustomers($offset, $length);
});
Dynamic Configuration: Use constructor or fluent methods to reconfigure per request (e.g., admin vs. user views).
$adminPaginator = new Paginator([
'itemsPerPage' => 50,
'pagesInRange' => 10,
]);
Laravel Request Binding: Bind pagination parameters to a DTO or Form Request:
public function rules() {
return ['page' => 'sometimes|integer|min:1'];
}
Then use request('page') in paginate().
Blade Integration: Create a reusable pagination view:
@component('pagination', ['pages' => $pagination->getPages()])
@slot('first') <a href="?page=1">First</a> @endslot
@slot('prev') <a href="?page={{ $pagination->getPreviousPageNumber() }}">Prev</a> @endslot
@endcomponent
Caching:
Cache the Pagination object or its metadata for performance:
$cacheKey = "pagination_{$request->page}";
$pagination = Cache::remember($cacheKey, now()->addHours(1), function () use ($paginator) {
return $paginator->paginate(request('page'));
});
Testing: Mock callbacks for unit tests:
$paginator->setItemTotalCallback(fn() => 100);
$paginator->setSliceCallback(fn($offset, $length) => ['item1', 'item2']);
$this->assertEquals(['item1', 'item2'], $paginator->paginate(1)->getItems());
Offset Calculation:
array_slice with large offsets can be slow or fail.array_chunk for pre-sliced arrays or optimize database queries with LIMIT/OFFSET.Callback Scope:
use.use ($var) or bind methods to the class:
$paginator->setSliceCallback([$this, 'sliceItems']);
Metadata Overwrite:
setMeta() overwrites existing metadata.$pagination->setMeta(array_merge($pagination->getMeta(), ['new_key' => 'value']));
Page Range Edge Cases:
pagesInRange may exclude the first/last page if misconfigured.totalNumberOfPages < pagesInRange and adjust logic.Iterator Return Types:
Iterator or Generator may not work as expected in older PHP versions.$paginator->setSliceCallback(function () {
yield 'item1';
yield 'item2';
});
Verify Callbacks: Log callback outputs to debug:
$paginator->setItemTotalCallback(function () {
$count = count($items);
Log::debug("Total items: {$count}");
return $count;
});
Check Pagination Object: Dump the entire object to inspect metadata:
dd($pagination->toArray());
Edge Cases: Test with:
setItemTotalCallback(fn() => 0)).itemsPerPage >= totalItems).page=1000).Custom Pagination Views:
Extend the Pagination class to add methods like getUrl($page):
class CustomPagination extends \AshleyDawson\SimplePagination\Pagination {
public function getUrl($page) {
return route('items.index', ['page' => $page]);
}
}
Pre/Post Query Logic:
Use setBeforeQueryCallback/setAfterQueryCallback for:
Pagination object dynamically:
$paginator->setAfterQueryCallback(function (Paginator $paginator, Pagination $pagination) {
$pagination->setMeta(['timestamp' => now()->toIso8601String()]);
});
Integration with Laravel: Create a macro for Eloquent collections:
Collection::macro('simplePaginate', function ($perPage) {
$paginator = new Paginator();
$paginator->setItemsPerPage($perPage);
$paginator->setItemTotalCallback(fn() => $this->count());
$paginator->setSliceCallback(fn($offset, $length) => $this->slice($offset, $length));
return $paginator->paginate(request('page', 1));
});
Usage:
$posts = Post::query()->simplePaginate(15);
Localization:
Override page labels (e.g., "Previous" → "Anterior") by extending the Pagination class and modifying the getPages() logic.
Performance:
For large datasets, avoid OFFSET in databases (use keyset pagination instead) and cache the Pagination object:
$cacheKey = "pagination_{$request->page}_{$request->sort}";
How can I help you explore Laravel packages today?