Customize pagination data in Laravels API Resources

Posted 06 Feb 2021

One of my projects I've recently work on needed an api to give other companies the ability to query my database. Fortunately, Laravel offers API resources, an amazingly easy way to format your models data, handle pagination data and more. So naturally I went with them to format my response.

Everything went really smooth and the result was amazing, except one ugly links array in my meta pagination data:

{    
  "data": [
    {
      "name": "Abkhazian",
      "id": 1,
      "value": 0
    }
  ],
  "links": {
    "first": "http://homestead.test/api/languages?page=1",
    "last": "http://homestead.test/api/languages?page=184",
    "prev": null,
    "next": "http://homestead.test/api/languages?page=2"
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 184,
    "links": [
      {
        "url": null,
        "label": "« Back",
        "active": false
      },
      {
        "url": "http://homestead.test/api/languages?page=1",
        "label": 1,
        "active": true
      },
      // ...
      {
        "url": "http://homestead.test/api/languages?page=184",
        "label": 184,
        "active": false
      },
      {
        "url": "http://homestead.test/api/languages?page=2",
        "label": "Forward »",
        "active": false
      }
    ],
    "path": "http://homestead.test/api/languages",
    "per_page": 1,
    "to": 1,
    "total": 184,
  }
}

See that? While I understand that you can use it to build the pagination in your frontend, I don't want to have it included in my api response. This must be easy to remove, shouldn't it?

It isn't! After some googling and tinkering, I found that Taylor himself recently added these links to the api resource class and that they are hardcoded into the response. It was time for me to find a way to overwrite this method in charge. The meta method on the PaginatedResourceResponse class generates the array that is included into the response. To exclude the links key, we just need add 'links' to the except array.

protected function meta($paginated) {
  return Arr::except($paginated, [
    'links', // How do we add this here?
    'data', 
    'first_page_url', 
    'last_page_url', 
    'prev_page_url', 
    'next_page_url', 
  ]); 
}

This way, everything you manually added to the meta of the resource is still included. But how do we overwrite this function? Here comes the tricky part.

Background: Our normal resource (e. g. CompanyResource) extends the JsonResource class. This class provides us the static collection function that we use to convert our database collection (e. g. CompanyResource::collection($companies)). This method returns an AnonymousResourceCollection, which in turn extends the ResourceCollection, that runs a method to prepare the pagination data. In this preparation step it finally returns the desired PaginatedResourceResponse. This class has our meta function that we want to overwrite.

How we overwrite this function

Fistly, we create our own CustomMetaResponse class. This extends the default PaginatedResourceResponse and overwrites the meta method. Whatever we return from this method will be the meta array. Since we only want to remove the links key, we just include it in the except array.

use Illuminate\Http\Resources\Json\PaginatedResourceResponse;
use Illuminate\Support\Arr;

class CustomMetaResponse extends PaginatedResourceResponse { 
  protected function meta($paginated) {
    returnArr::except($paginated, [
      'links', // Add here the links to be excluded from the response
      'data', 
      'first_page_url', 
      'last_page_url', 
      'prev_page_url', 
      'next_page_url', 
    ]); 
  }
}

Next we need to create our own ResourceCollection. This overwrites the preparePaginatedResponse method to return our new CustomMetaResponse instead of the PaginatedResourceResponse. The rest of the function is exactly the same as in the original AnonymousResporceCollection

use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

class CustomMetaPaginationCollection extends AnonymousResourceCollection {
  protected function preparePaginatedResponse($request) { 
    if ($this->preserveAllQueryParameters) { 
      $this->resource->appends($request->query()); 
    } elseif (! is_null($this->queryParameters)) { 
      $this->resource->appends($this->queryParameters); 
    } 
    return (CustomMetaResponse($this))->toResponse($request); // Here we return our newly created Response 
  }
}

And finally, to instruct our resource to use the CustomMetaPaginationCollection instead of the default AnonymousResourceCollection we overwrite the collection method of our resource:

use Illuminate\Http\Resources\Json\JsonResource;

class CompanyResource extends JsonResource
{
  // the tap returns our new CustomMetaPaginationCollection
  public static function collection($resource) {
    return tap(new CustomMetaPaginationCollection($resource, static::class), function ($collection) {
      if (property_exists(static::class, 'preserveKeys')) {
        $collection->preserveKeys = (new static([]))->preserveKeys === true;
      }
    });
  }

  public function toArray($request) {
    return [
      'name' => $this->title,
      'id' => $this->id,
      'value' => $this->pivot ? $this->pivot->value : 0,
    ];
  }
}

To make this more reusable, you could put the collection method onto your own abstract JsonResource class or put it into a trait and reuse it with other api resources.

This is definitely not a very clean way and I wish there is a better way to customize the pagination meta, but I couldn't find one. If you know a better way, shoot me a tweet!