Convert any page-based data source (APIs, databases, external services) into seamless offset-based pagination. Perfect for when your app needs "give me items 50-99" but your data source only speaks "give me page 3 with 25 items each".
✨ Framework-agnostic • 🚀 High performance • 🛡️ Type-safe • 🧪 Well tested
The Problem: Your application uses offset-based pagination ("show items 100-199"), but your database or API only supports page-based pagination ("give me page 5 with 20 items").
Manual Solution: Write complex math to convert offsets to pages, handle edge cases, manage memory efficiently, and deal with different data source behaviors.
This Package: Handles all the complexity automatically. Just provide a callback that fetches pages, and get seamless offset-based access.
- ✅ Zero Boilerplate - One callback function vs dozens of lines of pagination math
- ✅ Memory Efficient - Lazy loading prevents loading unnecessary data
- ✅ Type Safe - Full PHP 8.2+ type safety with generics
- ✅ Well Tested - Comprehensive test suite covering edge cases
- ✅ Framework Agnostic - Works with any PHP project (Laravel, Symfony, plain PHP, etc.)
- ✅ Production Ready - Used in real applications with battle-tested logic
Unlike Laravel's paginate() or Symfony's pagination components that are tied to specific frameworks and ORMs, this package:
- Works with any data source (SQL, NoSQL, REST APIs, GraphQL, external services)
- Has zero dependencies on frameworks or ORMs
- Provides consistent behavior across different projects and teams
- Is future-proof - not tied to any framework's roadmap
composer require somework/offset-pageGet started in 30 seconds:
useSomeWork\OffsetPage\OffsetAdapter;
// Your page-based API or database functionfunctionfetchPage(int$page, int$pageSize): array {
$offset = ($page - 1) * $pageSize;
// Your database query or API call herereturnfetchFromDatabase($offset, $pageSize);
}
// Create adapter with a callback$adapter = OffsetAdapter::fromCallback(function (int$page, int$pageSize) {
$data = fetchPage($page, $pageSize);
foreach ($dataas$item) {
yield$item;
}
});
// Get items 50-99 (that's offset 50, limit 50)$items = $adapter->fetchAll(50, 50);
// That's it! Your page-based source now works with offset-based requests.The adapter automatically converts your offset-based requests into page-based requests:
// You want: "Give me items 50-99"$items = $adapter->fetchAll(50, 50);
// The adapter translates this into:// Page 3 (items 51-75), Page 4 (items 76-100)// Then returns exactly items 50-99 from the results$adapter = OffsetAdapter::fromCallback(function (int$page, int$pageSize) {
$offset = ($page - 1) * $pageSize;
$stmt = $pdo->prepare("SELECT * FROM users LIMIT ? OFFSET ?");
$stmt->execute([$pageSize, $offset]);
foreach ($stmt->fetchAll() as$user) {
yield$user;
}
});
$users = $adapter->fetchAll(100, 25); // Users 100-124$adapter = OffsetAdapter::fromCallback(function (int$page, int$pageSize) {
$response = $httpClient->get("/api/products?page={$page}&size={$pageSize}");
$data = json_decode($response->getBody(), true);
foreach ($data['products'] as$product) {
yield$product;
}
});
$products = $adapter->fetchAll(50, 20); // Products 50-69For complex scenarios, implement SourceInterface:
useSomeWork\OffsetPage\SourceInterface;
class DatabaseSource implements SourceInterface
{
publicfunction__construct(privatePDO$pdo) {}
publicfunctionexecute(int$page, int$pageSize): \Generator
{
$offset = ($page - 1) * $pageSize;
$stmt = $this->pdo->prepare("SELECT * FROM items LIMIT ? OFFSET ?");
$stmt->execute([$pageSize, $offset]);
foreach ($stmt->fetchAll() as$item) {
yield$item;
}
}
}
$adapter = newOffsetAdapter(newDatabaseSource($pdo));
$items = $adapter->fetchAll(1000, 100);The library provides specific exceptions for different error types:
useSomeWork\OffsetPage\Exception\InvalidPaginationArgumentException;
useSomeWork\OffsetPage\Exception\PaginationExceptionInterface;
try {
$result = $adapter->fetchAll(-1, 50); // Invalid!
} catch (InvalidPaginationArgumentException$e) {
echo"Invalid parameters: " . $e->getMessage();
} catch (PaginationExceptionInterface$e) {
echo"Pagination error: " . $e->getMessage();
}For memory-efficient processing of large result sets:
$result = $adapter->execute(1000, 500);
while (null !== ($item = $result->fetch())) {
processItem($item); // Process one at a time
}$result = $adapter->execute(50, 25);
$items = $result->fetchAll();
$count = $result->getFetchedCount(); // Number of items actually returnedSee UPGRADE.md for migration guides between major versions, including breaking changes and upgrade paths.
This project includes several composer scripts for development and quality assurance:
composer test# Run PHPUnit tests
composer stan # Run PHPStan static analysis
composer cs-check # Check code style with PHP-CS-Fixer
composer cs-fix # Fix code style issues with PHP-CS-Fixer
composer quality # Run static analysis and code style checksThe library includes comprehensive tests covering:
- Unit tests for all core classes
- Integration tests for real-world scenarios
- Property-based tests for edge cases
- Memory usage and performance tests
- Exception handling scenarios
Igor Pinchuk — I work on legacy PHP and Symfony systems: audits, rescue and refactoring, performance. My open-source packages have been installed over 70,000 times on Packagist.
Inherited a codebase nobody wants to touch? Written questions get written answers — no discovery call required. i.pinchuk.work@gmail.com · LinkedIn
This project is licensed under the MIT License - see the LICENSE file for details.