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

Perpage Trait Laravel Package

quantumtecnology/perpage-trait

A lightweight Laravel trait that adds flexible per-page pagination to your models and queries. Read and validate the per_page request parameter, apply sensible defaults and maximum limits, and keep list endpoints consistent with minimal code.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a micro-package — one trait, a handful of lines, no dependents. Treat it as vendored code you happen to install via Composer, not as a framework extension. Read src/ end to end before you write a single controller; it will take less time than reading this page.

Install and confirm discovery

composer require quantumtecnology/perpage-trait

Check vendor/quantumtecnology/perpage-trait/composer.json for an extra.laravel.providers block. If there is one, a service provider is auto-registered; if not, the trait is plain PHP and needs nothing. There is no config file to publish in the typical case — if config/ exists in the package, publish it with php artisan vendor:publish --tag=perpage (verify the tag name in the provider).

Where to look first

  1. src/ — the trait itself. Identify (a) the method name exposed to your controllers, (b) the query-string key it reads (almost certainly per_page), (c) the default value, and (d) whether it enforces a maximum.
  2. README.md — for the intended calling convention.
  3. composer.json — require will tell you the minimum PHP and Laravel versions; with 1 star and no releases you should confirm it is not pinned to an older framework.

First use case

The canonical usage: add the trait to a controller, call the per-page accessor, hand the result to paginate().

use Illuminate\Routing\Controller;
use QuantumTecnology\PerPageTrait\PerPageTrait; // verify namespace in src/

class UserController extends Controller
{
    use PerPageTrait;

    public function index()
    {
        return User::query()->paginate($this->perPage());
    }
}

Then hit /users?per_page=50 and confirm the payload's per_page (or meta.per_page in an API Resource) reports 50. That single round trip is your smoke test.

Implementation Patterns

Put it on a base controller, not on every controller. The trait is only useful if it is uniform across resources. Add it to App\Http\Controllers\Controller so every child controller inherits the behaviour, then override per-controller only where a resource genuinely needs a different default.

abstract class Controller extends BaseController
{
    use PerPageTrait;

    protected int $defaultPerPage = 15;
    protected int $maxPerPage = 100;
}

If the trait hardcodes the default/max internally, subclass-level override is not possible — in that case wrap it:

protected function perPageFor(Request $request, int $default = 15, int $max = 100): int
{
    return min(max((int) $this->perPage(), 1), $max) ?: $default;
}

Keep it out of the model layer. Pagination size is a transport concern. Reading request() from inside a model or repository makes the class untestable and hides coupling. Call the accessor in the controller (or a form request) and pass an int downward:

$users = $this->userRepository->paginate($this->perPageFor($request));

API versus web defaults. Dashboards want 25–50 rows; JSON clients usually want smaller default pages with a higher ceiling. Rather than branching inside controllers, configure per route group:

Route::middleware('api')->group(function () {
    // API resources: default 50, cap 200
});

Route::middleware('web')->group(function () {
    // HTML tables: default 15, cap 100
});

If the trait reads the default from a config key, set config/perpage.php (or .env → config) once per environment and leave controllers untouched.

Preserve the parameter across pagination links. This is the single most common integration bug. In Blade:

{{ $users->withQueryString()->links() }}

withQueryString() (Laravel 7+) appends all current query parameters, so per_page survives page 2. For older versions use ->appends(request()->query()). In API responses, the next_page_url generated by Laravel already includes the query string — verify it does, do not assume.

Combine with filters and sorts deliberately. Establish an ordering: apply where clauses, then orderBy, then paginate last. Paginating before filtering gives you a page of the wrong rows, and orderBy after pagination gives you unstable page boundaries.

$users = User::query()
    ->when($request->q, fn ($q, $term) => $q->where('name', 'like', "%{$term}%"))
    ->orderBy('name')
    ->paginate($this->perPageFor($request))
    ->withQueryString();

Always include a deterministic tiebreaker in the sort (orderBy('name')->orderBy('id')) or users will see duplicated and missing rows as they page through.

Validate before you paginate. A per_page of 100000 will happily load your whole table into memory. Either clamp it (see above) or validate:

$request->validate(['per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);

Prefer a FormRequest so the rule is discoverable and reusable across the index and export endpoints.

Testing. Because the trait reads from the request, the test surface is the query string, not the model:

public function test_per_page_is_honoured(): void
{
    User::factory()->count(30)->create();

    $this->getJson('/api/users?per_page=10')
        ->assertOk()
        ->assertJsonCount(10, 'data')
        ->assertJsonPath('meta.per_page', 10);
}

public function test_per_page_is_clamped(): void
{
    $this->getJson('/api/users?per_page=99999')
        ->assertOk()
        ->assertJsonPath('meta.per_page', 100);
}

Write the clamping test first — it documents the invariant and will catch it if the trait's behaviour changes on a minor upgrade.

Gotchas and Tips

Confirm the maximum exists before relying on it. With one star and zero dependents, the package has effectively no public audit trail. Open the trait and look for a min()/max() or a cap constant. If there is no bound, you must add one yourself; an unbounded per_page is a one-line denial-of-service against your database.

Type coercion. Query strings are strings. paginate('25') mostly works in Laravel because of loose casting, but min($perPage, $max) with a string operand can silently produce 0 or a TypeError depending on PHP version. Cast to int at the boundary and never pass the raw request value into arithmetic.

Zero, negative, and non-numeric input. ?per_page=0 and ?per_page=abc should degrade to the default, not to an empty result set or a 500. Test all three. Laravel's paginate() treats 0 as "no rows" in some versions, which is almost never what the caller meant.

Trait method collisions. If your base controller also defines perPage() — or another trait does — you will get a fatal error about conflicting methods. Resolve explicitly:

use PerPageTrait, OtherTrait {
    PerPageTrait::perPage insteadof OtherTrait;
    OtherTrait::perPage as otherPerPage;
}

Do not confuse it with Laravel's own $perPage property. Eloquent models and paginators both have their own notion of per-page. If you set protected $perPage = 15; on a model, that becomes the fallback for paginate() with no argument — and it will silently disagree with whatever this trait returns. Pick one source of truth, and if it is the trait, remove the model property.

Cursor pagination is different. cursorPaginate() accepts a per-page count but produces cursor-based links with no page numbers. If you use it, the trait's job is unchanged (it just supplies the count), but withQueryString()/appends() semantics and the meta shape differ. Confirm your frontend handles next_cursor rather than current_page.

Inertia and Livewire. Inertia props receive the paginator as JSON; links contains the query string only if you call withQueryString(). In Livewire, pagination state lives in the component and per_page is usually a public property — the trait's request-reading behaviour fights this. Use the trait for stateless HTTP endpoints, and a component property for Livewire tables.

Vendor pinning. With no releases and no dependents, upstream breakage is invisible until composer update runs. Pin to a commit:

"quantumtecnology/perpage-trait": "dev-main#a1b2c3d"

or, given the size, copy the trait into app/Support/ and drop the dependency. For ~30 lines of request parsing, inlining removes an entire class of supply-chain and maintenance risk.

Debugging. When the value looks wrong, print the resolved number and the raw input together at the top of the action:

logger()->debug('pagination', [
    'raw'      => request('per_page'),
    'resolved' => $this->perPage(),
    'type'     => gettype($this->perPage()),
]);

If the raw input is correct and the resolved value is not, the problem is in the trait or your override. If both are correct and the response is wrong, the problem is downstream — a cached response, a resource transformer that drops per_page, or a query builder that ignores the count.

Extension points are limited to what you write. This package is unlikely to expose events or a driver system. Your leverage is composition: wrap the call in your own accessor, enforce your own caps, and keep the trait as a thin input-parsing adapter at the edge of the controller. That way, if you replace it — which is likely for a package at this maturity level — you change one method and nothing else.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk