somework/offset-page

Convert page-based APIs to offset-based pagination. Transform any paginated data source into seamless offset-based access for databases, REST APIs, and external services.

Installs: 5 238

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 0

Open Issues: 1

pkg:composer/somework/offset-page

3.0.0 2025-12-05 16:54 UTC

README

CI Latest Stable Version License PHP Version

Transform page-based APIs into offset-based pagination with zero hassle

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

Why This Package?

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.

Why Choose This Over Manual Implementation?

  • โœ… 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

Why Choose This Over Framework-Specific Solutions?

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

Installation

composer require somework/offset-page

Quickstart

Get started in 30 seconds:

use SomeWork\OffsetPage\OffsetAdapter;

// Your page-based API or database function
function fetchPage(int $page, int $pageSize): array {
    $offset = ($page - 1) * $pageSize;
    // Your database query or API call here
    return fetchFromDatabase($offset, $pageSize);
}

// Create adapter with a callback
$adapter = OffsetAdapter::fromCallback(function (int $page, int $pageSize) {
    $data = fetchPage($page, $pageSize);
    foreach ($data as $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.

How It Works

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

Usage Patterns

Database with LIMIT/OFFSET

$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

REST API with Page Parameters

$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-69

Custom Source Implementation

For complex scenarios, implement SourceInterface:

use SomeWork\OffsetPage\SourceInterface;

class DatabaseSource implements SourceInterface
{
    public function __construct(private PDO $pdo) {}

    public function execute(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 = new OffsetAdapter(new DatabaseSource($pdo));
$items = $adapter->fetchAll(1000, 100);

Advanced Usage

Error Handling

The library provides specific exceptions for different error types:

use SomeWork\OffsetPage\Exception\InvalidPaginationArgumentException;
use SomeWork\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();
}

Streaming Results

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
}

Getting Result Metadata

$result = $adapter->execute(50, 25);
$items = $result->fetchAll();
$count = $result->getFetchedCount(); // Number of items actually returned

Upgrading

See UPGRADE.md for migration guides between major versions, including breaking changes and upgrade paths.

Development

Available Scripts

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 checks

Testing

The 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

Author

Igor Pinchuk - i.pinchuk.work@gmail.com

License

This project is licensed under the MIT License - see the LICENSE file for details.