Customize paging information when using the Laravel Resource class

A recent submission to the Laravel framework idea - Add a detection of a custom paging information method to the PaginatedResourceResponse to make it very easy to customize the paging information when using the Resource class to output information.

Why need it

I'm basically developing an API. In the early days, I always returned directly, but sometimes this method had some problems and was not easy to maintain. In addition to the frequent need to add custom fields and give different data to different ends, I have been using Resource to define the returned data.

Resource is easy and logical. But one disadvantage is that there is too much paging information. For API projects, in most cases, many of the fields in the default output paging information are not needed, and since some old projects are often docked, they need to follow the old data format or be compatible. The fields of paging information are quite different, and the default returned paging information cannot be used directly.

I don't know how you're dealing with paging information in similar situations, but until then, I've usually done two things to get there: customize the Response, redefine the data information here, and customize all the Resource-related classes once.

I don't know much about the bottom of Laravel, nor am I good at abstract framework development, but after experiencing this, I find things can be much simpler, just like I am PR As explained, if you can assemble paging information in src/Illuminate/Http/Resources/Json/PaginatedResourceResponse.php and use its component paging information for the Resource class, you don't need to customize many classes every time you go through a lengthy chapter. So I submitted this idea to Laravel Frame. This submission was not directly accepted at first, but was merged after Taylor adjustment and published in v8.73.2.

This is the first time I've contributed code to Laravel and submitted a merge request to such a large code base. Although not directly adopted, the results are encouraging.

Use examples

Well, 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": "« Previous page",
                "active": false
            },
            {
                "url": "http://cooman.cootab-v4.test/api/favicons?page=1",
                "label": "1",
                "active": true
            },
            {
                "url": null,
                "label": "next page »",
                "active": false
            }
        ],
        "path": "http://cooman.cootab-v4.test/api/favicons",
        "per_page": 15,
        "to": 5,
        "total": 5
    }
}

This is the paging information that Laravel outputs by default, is it a lot of fields, which is enough for many scenarios. But sometimes it can be difficult. We need a little flexibility.

When using the ResourceCollection class

Let's look at the underlying logic first!

When the controller returns a ResourceCollection, its toResponse method is eventually called to respond. Then you can find the method directly to see:

   /**
     * 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);
    }

See, if the current resource is a paging object, it turns the task to process the paging response. Next look:

    /**
     * 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);
    }

Oh, it goes to PaginatedResourceResponse, which is the class we ultimately need to modify. Because the content of toResponse is too long, we don't post it here. It's here to start building the response data. Paging information is also handled here, of course, but it has a separate method. This method is paginationInformation, which is the logic before the PR is submitted:

/**
     * 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),
        ];
    }

If you're careful, you should be able to think that $this->resource here is actually an instance of the ResouurceCollection above, and its resource is our list data, that is, the paging information instance. In that case, why can't we process paging information in the ResourceCollection? Of course, but we need to add something. That's what I submitted.

After merging PR, its logic is as follows:

/**
     * 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;
    }

Simple way to handle it, if you have a custom paging information organization method in the corresponding resource class, then use it yourself, which is really a good idea for now.

Here, it should be clear how to customize the paging information. That is, add a paginationInformation method to your corresponding ResourceCollection class, such as:

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'],
        ];
    }

This is the customized data output:

{
    "data": [],
    "page": 1,
    "per_page": 15,
    "total": 5,
    "total_page": 1
}

The result is as I expected.

When using the Resource class

I usually only like to define a Resource class to handle individual objects and lists, where the main focus is on how to handle paging customizations of list data.

In the controller, I usually use this:

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

Let's see what we've done in the collection method:

   /**
     * 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;
            }
        });
    }

Originally it transferred the data to the ResourceCollection, you just need to customize the AnonymousResourceCollection.

summary

This is a small optimization, but it is useful.

Previously, it would have been cumbersome to return custom paging information with Resource, requiring a lot of customization, which was a piece of cake for older users, but could be a tricky problem for beginners. From then on, it will be easier for both old and new users. Simply add the paginationInformation method to the corresponding ResourceCollection class, similar to 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'],
        ];
    }

However, if you are using the Resource::collection($pageData) method, you also need to customize an additional ResourceCollection class and override the collection method for the corresponding Resource class.

I usually define a corresponding base class, then all others inherit it. You can also make a trait and share it.

Last

In fact, I wanted to submit this idea long ago, but I have been hesitant about whether it is really a popular demand. However, I finally want to understand that since doing this saves me a lot of repetitive and dangerous work, there are so many developers who will always need it, so I submitted it and verified whether my idea is working or not, and whether my approach is optimal. Of course, I learned a lot, such as writing slightly more complex test cases.

In addition, I would like to know if there are any other ways for you to do this, or how you treat paging information in different situations.

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

Started at: Customize paging information when using the Laravel Resource class

Tags: Laravel

Posted on Tue, 30 Nov 2021 14:44:48 -0500 by vB3Dev.Com