Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

joram's avatar
Level 1

Pagination using JSON API strategy.

I'm trying to implement a JSON API pagination strategy using the Laravel paginator. I'd like to use the query string parameters page[number] and page[size] to request specific page numbers and page size as suggested in the JSON API spec.

The paginator will get the page number from the request when the paginators pageName option is specified using the array dot notation (page.number) but it won't set it correctly in the generated URLs.

Is there any way to get the paginator to generate URLs in the form of page[number]=2?

Steps To Reproduce:

Consider the following trait:

use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

trait PaginatesResources
{
    protected $pageSizeDefault = 10;

    protected $pageNumberKey = 'page.number';
    protected $pageSizeKey = 'page.size';

    /**
     * @param Builder $query
     * @return LengthAwarePaginator
     */
    protected function paginate(Builder $query): LengthAwarePaginator
    {
        /** @var Request $request */
        $request = app('request');

        $size = $request->input($this->pageSizeKey, $this->pageSizeDefault);

        $paginator = $query->paginate($size, ['*'], $this->pageNumberKey);
        $paginator->appends(array_except($request->input(), $this->pageNumberKey));

        return $paginator;
    }
}

This resolves the current page correctly from URLs like resource?page[number]=2, but generates URLs in the form of resource?page.number=2 instead of the desired resource?page[number]=2.

To work around this, it's possible to modify the initialization to:

$paginator = $query->paginate($size, ['*'], $this->pageNumberKey);
$paginator->setPageName('page[number]');
// or use: preg_replace("/(.*)\.(.*)/", "$1[$2]", $this->pageNumberKey);
$paginator->appends(array_except($request->input(), $this->pageNumberKey));

Feels a bit hacky and would duplicate the page key definition in the trait (or the use of some extra processing to transform the dot notation to square brackets notation).

The following code in Illuminate\Pagination\AbstractPaginator is responsible for generating the URLs:

public function url($page)
{
    // [...]
    $parameters = [$this->pageName => $page];

    if (count($this->query) > 0) {
        $parameters = array_merge($this->query, $parameters);
    }
    // [...]
}

Replacing this code chunk with the following solves the issue for this use-case, but I'm not certain of any side-effects:

$parameters = $this->query;

array_set($parameters, $this->pageName, $page);

Anyone else bumped into this? Would this be an improvement in the pagination component or should this be solved in app code like above?

Note: cross-posted from https://github.com/laravel/framework/issues/19167

0 likes
7 replies
freekmurze's avatar

Hi, on which object should this trait be applied?

joram's avatar
Level 1

@freekmurze a controller or other class responsible for handling router requests (in my case), although it shouldn't really make any difference for the question I think.

freekmurze's avatar

Turns out Illuminate\Database\Eloquent\Builder is Macroable

So you can do this in a service provider:

Builder::macro('jsonPaginate', function($maxResults = 30) {
    $size = request()->input('page.size', $maxResults);
    if ($size > $maxResults) {
        $size = $maxResults;
    }

    $paginator = $this->paginate($size, ['*'], 'page.number');

    $paginator->setPageName('page[number]');

    $paginator->appends(array_except(request()->input(), 'page.number'));

    return $paginator;
});

and use on all models.

Model::jsonPaginate()
joram's avatar
Level 1

@freekmurze That's nice. Going all npm style with PHP micro-packages, soon enough we'll need a yarn for PHP ;)

nicolaszein's avatar

@freekmurze Do you have sample code on using your paginate package please? it looks great but i need to see how to use axios to get the next or previous pages via your package.

Please or to participate in this conversation.