Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lampager Laravel Laravel Package

lampager/lampager-laravel

Laravel integration for Lampager, adding efficient cursor-based (seek) pagination to Eloquent and the query builder. Paginate large datasets with stable, reversible ordering and a simple API, avoiding offset performance issues in Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

lampager/lampager-laravel is the Laravel adapter for Lampager: keyset (cursor) pagination for the query builder and Eloquent, with no OFFSET and no COUNT(*) query. Use it wherever paginate() becomes slow or semantically wrong — infinite scroll, "load more" feeds, JSON APIs, admin tables on large tables.

composer require lampager/lampager-laravel

That is the whole setup. There is no config file to publish, no migrations, no routes; package discovery registers the service provider and the query-builder macro. v0.6.1 is a compatibility-only release — no migration steps, nothing to change in your code.

First use case — replace an offset-paginated feed:

use App\Models\Post;

$result = Post::query()
    ->where('published', true)
    ->orderBy('created_at', 'desc')
    ->orderBy('id', 'desc')   // unique tiebreaker — mandatory
    ->lampager()
    ->paginate(20);

$result->records;      // Collection of models
$result->hasPrevious;  // bool — was there a page before this one?
$result->hasNext;      // bool — is there another page?

Where to look first, in order:

  1. The README's basic example — confirm the exact macro name and paginate() signature for your version (the adapter is thin, so the surface area is small).
  2. Lampager\Laravel\QueryBuilder and the result class — the only two objects you touch day to day.
  3. The upstream lampager/lampager docs — the "how does it know the cursor?" magic lives there, not in the Laravel adapter. Everything the wrapper does is derived from the where() and orderBy() calls you put on the query before lampager().

The core mental model: you build the query as if you were filtering "everything after the last row I saw", and the package rewrites it to also fetch the previous page (reversing order and flipping comparison operators) when needed.

Implementation Patterns

Feed / infinite scroll (next page only)

The cursor is just the last row's sort values, taken from the request and replayed as conditions:

$query = Post::query()->where('published', true);

if ($request->filled('cursor_id')) {
    $query->where('id', '<', (int) $request->input('cursor_id'));
}

$result = $query
    ->orderBy('id', 'desc')
    ->lampager()
    ->paginate(20);

return response()->json([
    'data'        => PostResource::collection($result->records),
    'has_next'    => $result->hasNext,
    'next_cursor' => optional($result->records->last())->id,
]);

Validate and whitelist every cursor input — an unvalidated cursor parameter is a user-supplied WHERE clause.

API Resource wrapper

The release notes touch the Resource integration tests, and this is the natural way to expose results. Wrap the result once and reuse it:

class PaginatedCollection extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'data' => $this->records,
            'meta' => [
                'has_previous' => $this->hasPrevious,
                'has_next'     => $this->hasNext,
            ],
        ];
    }
}

Bidirectional tables (admin / archive browsers)

hasPrevious is not a COUNT. The package answers it by issuing a reversed, LIMIT 1 probe query, so "previous" links work without knowing the page number. Pass a backward cursor the same way you pass a forward one; the wrapper handles flipping.

Compose before you wrap

Scopes, filters, with(), and select() all belong on the underlying Eloquent builder before lampager():

$result = Post::query()
    ->with('author')
    ->whereHas('tags', fn ($q) => $q->whereIn('slug', $slugs))
    ->orderBy('created_at', 'desc')
    ->orderBy('id', 'desc')
    ->lampager()
    ->paginate(15);

Eager loading still works because the parent rows are fetched in a single keyset query.

Relations

Pagination on a relation reads well and keeps tenancy scoping intact:

$result = $user->posts()->where('draft', false)->orderBy('id', 'desc')->lampager()->paginate(10);

Testing assertions worth writing

  • Assert the query count is one (plus the optional previous-probe) — no count(*) and no offset.
  • Assert that page 2 contains no rows from page 1 (pluck('id') intersection is empty) — this catches missing tiebreakers instantly.
  • Assert hasNext is false on the final page and that the final page's row count is < perPage.
  • Re-run the same test against each database you actually deploy to (the adapter generates SQL operators, not Eloquent abstractions).

Gotchas and Tips

  • The sort key must be unique. orderBy('created_at') alone gives you skipped and duplicated rows at page boundaries. Always append a unique tiebreaker (id) — this is the single most common bug with keyset pagination and the adapter cannot detect it for you.
  • No total(), no lastPage(), no page numbers. That is the trade-off that buys you the performance. If a UI needs "page 7 of 42", either run a separate cached count() or use offset pagination.
  • Unsupported query shapes. The wrapper rewrites the conditions it understands. orderByRaw, groupBy, having, union, distinct(), and complex joins with duplicated rows are outside that model and will produce wrong cursors rather than an exception. Keep the query simple: filters + whereHas + plain orderBy.
  • Nullable sort columns are a trap. Keyset comparisons over NULL values don't behave like id comparisons. Make the sort column NOT NULL with a default, or normalise it before ordering.
  • Casts and timezones. Cursor values come back from request input and are compared against raw DB columns. If you cast a column to datetime or a custom cast, round-trip the database value, not the Carbon-formatted one, or you'll skip rows at second/microsecond boundaries.
  • Debugging: enable DB::listen() / DB::enableQueryLog() in a test and read the generated SQL. Seeing the reversed probe query is the fastest way to understand why a page boundary misbehaves. If the SQL looks right and results look wrong, the bug is almost always your orderBy set.
  • Applying filters after lampager() works through __call forwarding, but mixing the two styles makes it hard to reason about what the wrapper saw. Pick "build the query first, wrap last" as a convention.
  • Version pinning. This is a 0.x package. Pin ^0.6.1 and read the changelog before minor bumps; the changelog is short and honest, which makes this cheap.
  • Compatibility, per v0.6.1: PHP 8.5 is now in CI, Laravel 13 is documented, and the constraint proactively allows Laravel 14. If you are on Laravel 11/12, the only change that affects you is the ResourceTest fix — a CI-only concern unless you had a failing resource-wrapping test of your own. No behavioural changes, no deprecations.
  • Extension points are thin by design. The adapter is a macro plus a small processor/query-builder layer over upstream Lampager. If you need to change how SQL is generated (custom operators, a database the adapter doesn't cover), do it upstream against Lampager's processor interface rather than monkey-patching here — and check how the service provider binds it before relying on container rebinding.
  • Don't confuse paginate() with Laravel's. On the Lampager wrapper it means keyset pagination and returns the Lampager result object; on a plain Eloquent builder it returns LengthAwarePaginator. If you get total() missing errors, you wrapped the wrong object or forgot the lampager() call.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib