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
}
}
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);
}
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);
}
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),
];
}
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;
}
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'],
];
}
Here is the output of the custom data:
{
"data": [],
"page": 1,
"per_page": 15,
"total": 5,
"total_page": 1
}
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);
}
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;
}
});
}
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'],
];
}
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)