最近向 Laravel 框架提交了一個(gè) 想法 — 在 PaginatedResourceResponse
中添加一個(gè)自定義分頁(yè)信息方法的檢測(cè),以便在使用 Resource
類(lèi)輸出信息時(shí),能夠非常方便地自定義分頁(yè)信息。
為什么需要它
我基本上都是在開(kāi)發(fā) API。早期時(shí)候我都是直接返回,但是這種方式有時(shí)候會(huì)出現(xiàn)一些問(wèn)題,也不方便維護(hù),加上經(jīng)常需要添加自定義字段和針對(duì)不同端給出不同數(shù)據(jù)的情況,我后來(lái)就一直在使用 Resource
來(lái)定義返回的數(shù)據(jù)?!就扑]:laravel視頻教程】
使用 Resource
很方便也能夠讓邏輯清晰。但它有個(gè)不好的地方,那就是分頁(yè)信息太多了。針對(duì) API 項(xiàng)目而言,大多數(shù)情況下,默認(rèn)輸出的分頁(yè)信息里很多字段并不需要,并且由于經(jīng)常對(duì)接的是一些老項(xiàng)目,需要沿用老的數(shù)據(jù)格式或者做兼容,分頁(yè)信息的字段大不相同,沒(méi)辦法直接使用默認(rèn)返回的分頁(yè)信息。
我不知道大家是怎么處理類(lèi)似情況時(shí)的分頁(yè)信息的,但在此之前,為了能夠達(dá)到目的,我通常有兩種做法,一是自定義 Response
,在這里面把數(shù)據(jù)信息進(jìn)行重新定義,二是將 Resource
相關(guān)的類(lèi)全部自定義一遍。
我對(duì) Laravel 底層并不是很了解,我也不擅長(zhǎng)做抽象的框架開(kāi)發(fā),但是在經(jīng)歷這些之后,我發(fā)現(xiàn)事情能夠變得簡(jiǎn)單很多,正如我在 PR 闡述的那樣,如果可以在 src/Illuminate/Http/Resources/Json/PaginatedResourceResponse.php
中組建分頁(yè)信息時(shí),能夠使用其對(duì)應(yīng) Resource
類(lèi)的組件分頁(yè)信息,那不就不需要每次大費(fèi)周章的進(jìn)行自定義很多類(lèi)了嗎。于是我就提交了這個(gè)想法給 Laravel 框架。這個(gè)提交在一開(kāi)始并沒(méi)有被直接接受,而是在經(jīng)過(guò) Taylor 調(diào)整后被合并,并發(fā)布在 v8.73.2。
這是我第一次向 Laravel 貢獻(xiàn)代碼,也是第一次向這么大的代碼庫(kù)提交合并請(qǐng)求,雖然沒(méi)有被直接采用,但結(jié)果足以振奮人心。
使用示例
那么,我來(lái)簡(jiǎn)單的示例一下如何使用吧。
默認(rèn)輸出
{ "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": "« 上一頁(yè)", "active": false }, { "url": "http://cooman.cootab-v4.test/api/favicons?page=1", "label": "1", "active": true }, { "url": null, "label": "下一頁(yè) »", "active": false } ], "path": "http://cooman.cootab-v4.test/api/favicons", "per_page": 15, "to": 5, "total": 5 }}
這是 Laravel 默認(rèn)輸出的分頁(yè)信息,是不是很多字段,當(dāng)然這足夠應(yīng)對(duì)很多場(chǎng)景的使用。但有時(shí)候也會(huì)因此犯難。我們需要一點(diǎn)靈活。
使用 ResourceCollection
類(lèi)時(shí)
我們先來(lái)看看底層邏輯吧!
當(dāng)在控制器返回一個(gè) ResourceCollection
時(shí),最終會(huì)調(diào)用其 toResponse
方法以響應(yīng)。那么可以直接找到該方法看看:
/** * Create an HTTP response that represents the object. * * @param IlluminateHttpRequest $request * @return IlluminateHttpJsonResponse */ public function toResponse($request) { if ($this->resource instanceof AbstractPaginator || $this->resource instanceof AbstractCursorPaginator) { return $this->preparePaginatedResponse($request); } return parent::toResponse($request); }
看到?jīng)],如果當(dāng)前資源是個(gè)分頁(yè)對(duì)象時(shí),它就把任務(wù)轉(zhuǎn)向處理分頁(yè)響應(yīng)了。接著看:
/** * Create a paginate-aware HTTP response. * * @param IlluminateHttpRequest $request * @return IlluminateHttpJsonResponse */ 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); }
噢,它又轉(zhuǎn)給了 PaginatedResourceResponse
,這是我們最終需要修改的類(lèi),由于 toResponse
的內(nèi)容太長(zhǎng),就不在這里貼出,反正就是在這里開(kāi)始組建響應(yīng)的數(shù)據(jù),分頁(yè)信息當(dāng)然也是在這里面做的處理,不過(guò)它有個(gè)獨(dú)立的方法。該方法就是 paginationInformation
, 這是在提交 PR 前的邏輯:
/** * Add the pagination information to the response. * * @param IlluminateHttpRequest $request * @return array */ protected function paginationInformation($request) { $paginated = $this->resource->resource->toArray(); return [ 'links' => $this->paginationLinks($paginated), 'meta' => $this->meta($paginated), ]; }
如果你細(xì)心的話(huà),你應(yīng)該能夠想到,這里的 $this->resource
其實(shí)就是上面的 ResourceCollection
的實(shí)例,那么它的 resource
就是我們的列表數(shù)據(jù),也就是分頁(yè)信息實(shí)例。既然如此,那我們?yōu)楹尾荒茉?ResourceCollection
中進(jìn)行分頁(yè)信息的處理呢?當(dāng)然可以,但我們需要加點(diǎn)東西,這就是我提交的想法。
合并 PR 之后,它的邏輯是這樣的:
/** * Add the pagination information to the response. * * @param IlluminateHttpRequest $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; }
很簡(jiǎn)單的處理方式,如果對(duì)應(yīng)資源類(lèi)中有自定義的分頁(yè)信息組建方法,那就使用它自己的,目前而言,這確實(shí)是個(gè)好想法。
于此,如何自定義分頁(yè)信息應(yīng)該很清晰了。那就是在自己相應(yīng)的 ResourceCollection
類(lèi)中添加 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'], ]; }
這是自定義后的數(shù)據(jù)輸出情況:
{ "data": [], "page": 1, "per_page": 15, "total": 5, "total_page": 1}
結(jié)果如我所愿。
使用 Resource
類(lèi)時(shí)
我通常只喜歡定義一個(gè) Resource
類(lèi)來(lái)應(yīng)對(duì)單個(gè)對(duì)象和列表的情況,這里主要關(guān)注如何處理列表數(shù)據(jù)的分頁(yè)自定義。
在控制器中,我一般都是這樣使用:
public function Index(){ // .... return SomeResource::collection($paginatedData);}
再來(lái)看看 collection
方法里做了什么:
/** * Create a new anonymous resource collection. * * @param mixed $resource * @return IlluminateHttpResourcesJsonAnonymousResourceCollection */ 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; } }); }
原來(lái)它把數(shù)據(jù)轉(zhuǎn)給了 ResourceCollection
,那么只需要將這個(gè) AnonymousResourceCollection
做個(gè)自定義不就可以了。
總結(jié)
這是一個(gè)很小優(yōu)化,但是很有用。
在此之前,如果想要隨著 Resource
返回自定義分頁(yè)信息,會(huì)比較麻煩,需要自定義很多東西,這樣的方式,對(duì)老用戶(hù)而言小菜一碟,但是對(duì)新手就可能是件棘手的問(wèn)題。那么自此之后,無(wú)論是老用戶(hù)還是新手這件事將變得易如反掌。只需要在對(duì)應(yīng)的 ResourceCollection
類(lèi)中添加 paginationInformation
方法,類(lèi)似下面這樣:
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'], ]; }
不過(guò),如果你使用的是 Resource::collection($pageData)
方式,那么還需要額外自定義一個(gè) ResourceCollection
類(lèi),并重寫(xiě)對(duì)應(yīng) Resource
類(lèi)的 collection
方法。
我通常會(huì)定義一個(gè)對(duì)應(yīng)的基類(lèi),然后其它的都繼承它。也可以做個(gè) trait
,然后共用。
最后
其實(shí),這個(gè)想法我很早就想提交的,但是我一直比較猶豫,這到底是不是一個(gè)很大眾的需求。不過(guò)我最后想明白了,這樣做既然能為我節(jié)省大量重復(fù)且危險(xiǎn)的工作,有那么多的開(kāi)發(fā)者,總會(huì)有人需要的,所以我提交了,同時(shí)也是驗(yàn)證下我的想法到底是否可行,我的做法是否最優(yōu),結(jié)果當(dāng)然是我學(xué)到了很多,比如寫(xiě)稍微復(fù)雜的測(cè)試用例。
另外,我想知道大家有沒(méi)其它方法,或你們是怎么對(duì)待不同情況的分頁(yè)信息的。
最后的最后,你如果也有好的想法,那么盡快提交吧!