공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다. 응답·행의 무게를 다룬 · 과 겹치지 않는다. 공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이 기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은 per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지 번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로 정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은 계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다. 계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면 그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다. 응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새 필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate 응답은 필드 단위로 이전과 동일하다. 요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일 재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어 화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한 변경이 종전처럼 다음 요청에 반영된다. 검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한 엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어 활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩 대신 선언형 config 로 옮겼다. 총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을 돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
136 lines
4.9 KiB
PHP
136 lines
4.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Resources;
|
|
|
|
use App\Contracts\Pagination\BoundedTotalAware;
|
|
use App\Helpers\PermissionHelper;
|
|
use App\Http\Resources\Traits\HasRowNumber;
|
|
use App\Support\Query\BoundedPage;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator as LengthAwarePaginatorContract;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
|
use Illuminate\Pagination\CursorPaginator;
|
|
|
|
/**
|
|
* API 컬렉션 리소스 기본 클래스
|
|
*
|
|
* 모든 API Collection은 이 클래스를 상속해야 합니다.
|
|
* - HasRowNumber: 순번 부여
|
|
* - abilityMap(): 컬렉션 레벨 abilities (페이지 버튼 제어)
|
|
*/
|
|
abstract class BaseApiCollection extends ResourceCollection
|
|
{
|
|
use HasRowNumber;
|
|
|
|
/**
|
|
* 컬렉션 레벨 능력(can_*) 매핑을 반환합니다.
|
|
*
|
|
* 페이지 레벨 버튼 제어용 (생성, 일괄삭제 등).
|
|
* abilities가 불필요한 컬렉션은 오버라이드하지 않습니다.
|
|
*
|
|
* @return array<string, string> ['can_delete' => 'permission.identifier', ...]
|
|
*/
|
|
protected function abilityMap(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* 컬렉션 레벨 abilities를 해석합니다.
|
|
*
|
|
* @param Request $request HTTP 요청 객체
|
|
* @return array<string, bool>
|
|
*/
|
|
public function resolveCollectionAbilities(Request $request): array
|
|
{
|
|
$map = $this->abilityMap();
|
|
if (empty($map)) {
|
|
return [];
|
|
}
|
|
|
|
$user = $request->user();
|
|
$abilities = [];
|
|
|
|
foreach ($map as $key => $permission) {
|
|
$abilities[$key] = PermissionHelper::check($permission, $user);
|
|
}
|
|
|
|
return $abilities;
|
|
}
|
|
|
|
/**
|
|
* paginator 인 경우 표준 pagination 메타를 반환합니다.
|
|
*
|
|
* 무한스크롤 화면이 전체 개수(total)와 다음 페이지 존재 여부(has_more_pages)를
|
|
* 정확히 판정할 수 있도록 노출합니다. 전체 조회(get) 등 paginator 가 아닌
|
|
* 경우에는 빈 배열을 반환하므로 toArray 에서 array_merge 로 안전하게 합칠 수 있습니다.
|
|
*
|
|
* 페이지 결과는 세 형태 중 하나이며, 형태를 스스로 판정해 그 형태가 실제로 아는
|
|
* 값만 내보냅니다. 모르는 값을 0 이나 1 로 채우면 화면이 그것을 사실로 읽습니다.
|
|
*
|
|
* | 형태 | 예 | total | last_page | 추가 |
|
|
* |---|---|---|---|---|
|
|
* | 전체건수형 | `paginate()` | 정확 | 정확 | — |
|
|
* | 상한형 | {@see BoundedPage} | 정확 또는 하한 | 하한이면 null | `total_relation` `total_is_exact` `result_cap` |
|
|
* | 단순형 | `simplePaginate()` / `cursorPaginate()` | 없음 | 없음 | 커서면 `next_cursor` `prev_cursor` |
|
|
*
|
|
* 기존 필드는 하나도 제거하지 않으므로, 표준 `paginate()` 를 쓰는 기존 컬렉션의
|
|
* 응답은 이전과 완전히 동일합니다.
|
|
*
|
|
* @return array<string, mixed> ['pagination' => [...]] 또는 빈 배열
|
|
*/
|
|
protected function paginationMeta(): array
|
|
{
|
|
$resource = $this->resource;
|
|
|
|
if ($resource instanceof CursorPaginator) {
|
|
return ['pagination' => $this->cursorPaginationMeta($resource)];
|
|
}
|
|
|
|
if (! method_exists($resource, 'currentPage')) {
|
|
return [];
|
|
}
|
|
|
|
$meta = [
|
|
'current_page' => $resource->currentPage(),
|
|
'per_page' => $resource->perPage(),
|
|
'from' => $resource->firstItem(),
|
|
'to' => $resource->lastItem(),
|
|
'has_more_pages' => $resource->hasMorePages(),
|
|
];
|
|
|
|
// 단순형(simplePaginate)은 총 건수를 아예 세지 않으므로 total/last_page 를 내보내지 않는다.
|
|
if ($resource instanceof LengthAwarePaginatorContract) {
|
|
$meta['last_page'] = $resource->lastPage();
|
|
$meta['total'] = $resource->total();
|
|
}
|
|
|
|
if ($resource instanceof BoundedTotalAware) {
|
|
$meta['total_relation'] = $resource->totalRelation()->value;
|
|
$meta['total_is_exact'] = $resource->totalRelation()->isExact();
|
|
$meta['result_cap'] = $resource->resultCap();
|
|
}
|
|
|
|
return ['pagination' => $meta];
|
|
}
|
|
|
|
/**
|
|
* 커서 페이지의 pagination 메타를 만듭니다.
|
|
*
|
|
* 커서 방식은 총 건수와 페이지 번호를 계산하지 않습니다. 대신 앞뒤 이동 커서를
|
|
* 그대로 실어 보내며, 화면은 이 값으로 이전/다음 버튼을 만듭니다.
|
|
*
|
|
* @param CursorPaginator $resource 커서 페이지 결과
|
|
* @return array<string, mixed> pagination 메타
|
|
*/
|
|
private function cursorPaginationMeta(CursorPaginator $resource): array
|
|
{
|
|
return [
|
|
'per_page' => $resource->perPage(),
|
|
'has_more_pages' => $resource->hasMorePages(),
|
|
'next_cursor' => $resource->nextCursor()?->encode(),
|
|
'prev_cursor' => $resource->previousCursor()?->encode(),
|
|
];
|
|
}
|
|
}
|