DEV Community

Cover image for Customize paging information when using the Laravel Resource class
Chuoke
Chuoke

Posted on

Customize paging information when using the Laravel Resource class

Recently submitted an idea to Laravel framework - add a custom paging information in PaginatedResourceResponse detection method, in order to use the Resource class output information, able to custom paging information is very convenient.

Why do I need this?

I was basically developing APIs. In the early days, I used to return data directly, but this method sometimes had some problems and was not easy to maintain, and often needed to add custom fields and give different data for different ends, So I have been using Resource to define the response data.

Using Resource is convenient and logical. The downside is that there is too much paging information. For API projects, in most cases, many fields in the default output of paging information are not needed, and because some old projects often need to use the old data format or do compatibility, the paging information fields are very different, you cannot directly use the default return of paging information.

I don't know how you handle paging information in a situation like this, but in order to achieve this goal, I usually do two ways: one is to customize Response, in which the data information is redefined, and the other is to customize all the Resource related classes.

I didn't know much about Laravel internal, and I wasn't good at abstract framework development, but after going through this, I found that things will be a lot easier, As I describe in PR, If can be in Http/Resources/Json/SRC/Illuminate/PaginatedResourceResponse.php formed the paging information, to be able to use its corresponding Resource class paging information, There's no need to customize a lot of classes every time. So I just submit the idea to Laravel. This submission has not been directly in the first place accepted, but after being adjusted by Taylor and it was merged finally, and published in v8.73.2.

This was the first time I had contributed to Laravel, and the first time I had submitted a merge request to such a large codebase, and while it wasn't directly adopted, the results were encouraging.

Use Case

So, let me give you a simple example of how to use it.

Default Output

{  
    "data": [],
    "links": {
        "first": "http://cooman.cootab-v4.test/api/favicons?page=1",
        "last": "http://cooman.cootab-v4.test/api/favicons?page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "« 上一页",
                "active": false
            },
            {
                "url": "http://cooman.cootab-v4.test/api/favicons?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": null,
                "label": "下一页 »",
                "active": false
            }
        ],
        "path": "http://cooman.cootab-v4.test/api/favicons",
        "per_page": 15,
        "to": 5,
        "total": 5
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the default page information output by Laravel, which is a lot of fields, but of course, it is enough for many scenarios. But sometimes it can be difficult. We need some flexibility.

When using the ResourceCollection

Let's look at the underlying logic first.

When the controller returns a ResourceCollection, its toResponse method is eventually called in response. Then we can directly find the method to look at:

   /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function toResponse($request)
    {
        if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) {
            return $this->preparePaginatedResponse($request);
        }

        return parent::toResponse($request);
    }
Enter fullscreen mode Exit fullscreen mode

See, if the current resource is a paginate object, it shifts the task to processing paging responses. Then look at:

    /**
     * Create a paginate-aware HTTP response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\JsonResponse
     */
    protected function preparePaginatedResponse($request)
    {
        if ($this->preserveAllQueryParameters) {
            $this->resource->appends($request->query());
        } elseif (! is_null($this->queryParameters)) {
            $this->resource->appends($this->queryParameters);
        }

        return (new PaginatedResourceResponse($this))->toResponse($request);
    }
Enter fullscreen mode Exit fullscreen mode

Oh, it is transferred to the PaginatedResourceResponse, this is our final class that needs to be modified. Because toResponse content is too long, not posted here, anyway, it is here that start the response data, paging information and do the processing on the inside, of course, But it has a separate method. This method is called paginationInformation, which is the logic before submitting the PR:

/**
     * Add the pagination information to the response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function paginationInformation($request)
    {
        $paginated = $this->resource->resource->toArray();

        return [
            'links' => $this->paginationLinks($paginated),
            'meta' => $this->meta($paginated),
        ];
    }
Enter fullscreen mode Exit fullscreen mode

If you are careful, you can see that $this->resource is an instance of ResourceCollection, and its resource is our list data, i.e. paging information instance. In this case, why can't we handle paging information in ResourceCollection?
Sure, but we need to add something, and that's the idea I submitted.

After merging PR, its logic looks like this:

/**
     * Add the pagination information to the response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function paginationInformation($request)
    {
        $paginated = $this->resource->resource->toArray();

        $default = [
            'links' => $this->paginationLinks($paginated),
            'meta' => $this->meta($paginated),
        ];

        if (method_exists($this->resource, 'paginationInformation')) {
            return $this->resource->paginationInformation($request, $paginated, $default);
        }

        return $default;
    }
Enter fullscreen mode Exit fullscreen mode

A simple way to do this is to use your own custom method of building paging information in the corresponding resource class, which is a good idea for now.

At this moment, it should be clear how to customize the paging information. Add paginationInformation to your own ResourceCollection class, for example:

public function paginationInformation($request, $paginated, $default): array
    {
        return [
            'page' => $paginated['current_page'],
            'per_page' => $paginated['per_page'],
            'total' => $paginated['total'],
            'total_page' => $paginated['last_page'],
        ];
    }
Enter fullscreen mode Exit fullscreen mode

Here is the output of the custom data:

{
    "data": [],
    "page": 1,
    "per_page": 15,
    "total": 5,
    "total_page": 1
}
Enter fullscreen mode Exit fullscreen mode

It turned out as I had hoped.

When using the Resource

I usually only like to define a Resource class for a single object and a list, but here I focus on how to handle paging customization of list data.

This is how I use it in controllers:

public function Index()
{
    // ....
    return  SomeResource::collection($paginatedData);
}
Enter fullscreen mode Exit fullscreen mode

Let's look at what the collection method does:

   /**
     * Create a new anonymous resource collection.
     *
     * @param  mixed  $resource
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
     */
    public static function collection($resource)
    {
        return tap(new AnonymousResourceCollection($resource, static::class), function ($collection) {
            if (property_exists(static::class, 'preserveKeys')) {
                $collection->preserveKeys = (new static([]))->preserveKeys === true;
            }
        });
    }
Enter fullscreen mode Exit fullscreen mode

Originally it transfer data to the ResourceCollection, so only need to put this AnonymousResourceCollection do a custom.

Conclusion

This is a small optimization, but useful.

Previously, returning custom paging information along with Resource was a bit of a hassle and required a lot of customization, which was a breeze for veteran users but could be tricky for newbies. After that, it will be a piece of cake for both old and new users. Just add the paginationInformation method to the corresponding ResourceCollection class, something like the following:

public function paginationInformation($request, $paginated, $default): array
    {
        return [
            'page' => $paginated['current_page'],
            'per_page' => $paginated['per_page'],
            'total' => $paginated['total'],
            'total_page' => $paginated['last_page'],
        ];
    }
Enter fullscreen mode Exit fullscreen mode

However, if you are using Resource::collection($pageData), you will need to define an additional ResourceCollection class and rewrite the collection methods of the corresponding Resource class.

I usually define a corresponding base class, and everything else inherits from it. You can also make a trait and share it.

In the End

In fact, I wanted to submit this idea for a long time, but I have been hesitant about whether it is a popular demand. But I finally figured it out, since doing so can save a lot of repetitive and dangerous work, there are so many developers, someone will always need, so I submitted, But also to see if my idea works, whether I did it the best way. Of course, I learned a lot as a result, like writing slightly more complex test cases.

Also, I want to know if you have any other methods, or how you treat paging information in different cases.

Finally, if you have a good idea, submit it as soon as possible!

Top comments (0)