最近向 Laravel 框架提交了一個 想法 — 在 PaginatedResourceResponse
中新增一個自定義分頁資訊方法的檢測,以便在使用 Resource
類輸出資訊時,能夠非常方便地自定義分頁資訊。
為什麼需要它
我基本上都是在開發 API。早期時候我都是直接返回,但是這種方式有時候會出現一些問題,也不方便維護,加上經常需要新增自定義欄位和針對不同端給出不同資料的情況,我後來就一直在使用 Resource
來定義返回的資料。
使用 Resource
很方便也能夠讓邏輯清晰。但它有個不好的地方,那就是分頁資訊太多了。針對 API 專案而言,大多數情況下,預設輸出的分頁資訊裡很多欄位並不需要,並且由於經常對接的是一些老專案,需要沿用老的資料格式或者做相容,分頁資訊的欄位大不相同,沒辦法直接使用預設返回的分頁資訊。
我不知道大家是怎麼處理類似情況時的分頁資訊的,但在此之前,為了能夠達到目的,我通常有兩種做法,一是自定義 Response
,在這裡面把資料資訊進行重新定義,二是將 Resource
相關的類全部自定義一遍。
我對 Laravel 底層並不是很瞭解,我也不擅長做抽象的框架開發,但是在經歷這些之後,我發現事情能夠變得簡單很多,正如我在 PR 闡述的那樣,如果可以在 src/Illuminate/Http/Resources/Json/PaginatedResourceResponse.php
中組建分頁資訊時,能夠使用其對應 Resource
類的元件分頁資訊,那不就不需要每次大費周章的進行自定義很多類了嗎。於是我就提交了這個想法給 Laravel 框架。這個提交在一開始並沒有被直接接受,而是在經過 Taylor 調整後被合併,併發布在 v8.73.2。
這是我第一次向 Laravel 貢獻程式碼,也是第一次向這麼大的程式碼庫提交合並請求,雖然沒有被直接採用,但結果足以振奮人心。
使用示例
那麼,我來簡單的示例一下如何使用吧。
預設輸出
{
"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
}
}
這是 Laravel 預設輸出的分頁資訊,是不是很多欄位,當然這足夠應對很多場景的使用。但有時候也會因此犯難。我們需要一點靈活。
使用 ResourceCollection
類時
我們先來看看底層邏輯吧!
當在控制器返回一個 ResourceCollection
時,最終會呼叫其 toResponse
方法以響應。那麼可以直接找到該方法看看:
/**
* 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);
}
看到沒,如果當前資源是個分頁物件時,它就把任務轉向處理分頁響應了。接著看:
/**
* 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);
}
噢,它又轉給了 PaginatedResourceResponse
,這是我們最終需要修改的類,由於 toResponse
的內容太長,就不在這裡貼出,反正就是在這裡開始組建響應的資料,分頁資訊當然也是在這裡面做的處理,不過它有個獨立的方法。該方法就是 paginationInformation
, 這是在提交 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),
];
}
如果你細心的話,你應該能夠想到,這裡的 $this->resource
其實就是上面的 ResourceCollection
的例項,那麼它的 resource
就是我們的列表資料,也就是分頁資訊例項。既然如此,那我們為何不能在 ResourceCollection
中進行分頁資訊的處理呢?當然可以,但我們需要加點東西,這就是我提交的想法。
合併 PR 之後,它的邏輯是這樣的:
/**
* 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;
}
很簡單的處理方式,如果對應資源類中有自定義的分頁資訊組建方法,那就使用它自己的,目前而言,這確實是個好想法。
於此,如何自定義分頁資訊應該很清晰了。那就是在自己相應的 ResourceCollection
類中新增 paginationInformation
方法即可,比如:
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'],
];
}
這是自定義後的資料輸出情況:
{
"data": [],
"page": 1,
"per_page": 15,
"total": 5,
"total_page": 1
}
結果如我所願。
使用 Resource
類時
我通常只喜歡定義一個 Resource
類來應對單個物件和列表的情況,這裡主要關注如何處理列表資料的分頁自定義。
在控制器中,我一般都是這樣使用:
public function Index()
{
// ....
return SomeResource::collection($paginatedData);
}
再來看看 collection
方法裡做了什麼:
/**
* 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;
}
});
}
原來它把資料轉給了 ResourceCollection
,那麼只需要將這個 AnonymousResourceCollection
做個自定義不就可以了。
總結
這是一個很小優化,但是很有用。
在此之前,如果想要隨著 Resource
返回自定義分頁資訊,會比較麻煩,需要自定義很多東西,這樣的方式,對老使用者而言小菜一碟,但是對新手就可能是件棘手的問題。那麼自此之後,無論是老使用者還是新手這件事將變得易如反掌。只需要在對應的 ResourceCollection
類中新增 paginationInformation
方法,類似下面這樣:
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'],
];
}
不過,如果你使用的是 Resource::collection($pageData)
方式,那麼還需要額外自定義一個 ResourceCollection
類,並重寫對應 Resource
類的 collection
方法。
我通常會定義一個對應的基類,然後其它的都繼承它。也可以做個 trait
,然後共用。
最後
其實,這個想法我很早就想提交的,但是我一直比較猶豫,這到底是不是一個很大眾的需求。不過我最後想明白了,這樣做既然能為我節省大量重複且危險的工作,有那麼多的開發者,總會有人需要的,所以我提交了,同時也是驗證下我的想法到底是否可行,我的做法是否最優,結果當然是我學到了很多,比如寫稍微複雜的測試用例。
另外,我想知道大家有沒其它方法,或你們是怎麼對待不同情況的分頁資訊的。
最後的最後,你如果也有好的想法,那麼儘快提交吧!
首發於:使用 Laravel Resource 類時自定義分頁資訊
本作品採用《CC 協議》,轉載必須註明作者和本文連結