공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다. 응답·행의 무게를 다룬 · 과 겹치지 않는다. 공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이 기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은 per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지 번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로 정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은 계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다. 계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면 그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다. 응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새 필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate 응답은 필드 단위로 이전과 동일하다. 요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일 재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어 화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한 변경이 종전처럼 다음 요청에 반영된다. 검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한 엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어 활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩 대신 선언형 config 로 옮겼다. 총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을 돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
109 lines
3.2 KiB
PHP
109 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Enums\PermissionType;
|
|
use App\Models\Permission;
|
|
use App\Models\Role;
|
|
|
|
/**
|
|
* 비회원(guest) 역할 해석기
|
|
*
|
|
* 비회원 권한 판정은 한 요청에서 수십 번 일어난다 — 미들웨어, Gate, 리소스가 각각 묻는다.
|
|
* 매번 역할과 권한을 다시 조회하지 않도록 요청당 한 번만 적재하고 그 결과를 공유한다.
|
|
*
|
|
* 캐시 수명은 **요청 스코프**다. 프로세스 정적으로 두면 상주형 실행 환경에서 권한을
|
|
* 바꿔도 그 프로세스가 살아 있는 동안 예전 권한으로 판정한다 — 권한 변경 즉시 반영이
|
|
* 깨지므로 그렇게 하지 않는다.
|
|
*
|
|
* 미들웨어와 Gate 가 각자 같은 조회를 들고 있으면 한쪽만 고쳐졌을 때 두 경로의 판정이
|
|
* 갈린다. 그래서 해석은 이 클래스 한 곳에서만 한다.
|
|
*/
|
|
class GuestRoleResolver
|
|
{
|
|
/**
|
|
* 해석 결과를 담는 요청 속성 키
|
|
*/
|
|
private const CACHE_KEY = '_guest_role_cache';
|
|
|
|
/**
|
|
* guest 역할을 권한과 함께 조회합니다 (요청당 1회).
|
|
*
|
|
* @return Role|null guest 역할 (미정의 시 null)
|
|
*/
|
|
public static function resolve(): ?Role
|
|
{
|
|
$request = request();
|
|
|
|
if ($request->attributes->has(self::CACHE_KEY)) {
|
|
return $request->attributes->get(self::CACHE_KEY);
|
|
}
|
|
|
|
$role = Role::where('identifier', 'guest')
|
|
->with('permissions')
|
|
->first();
|
|
|
|
$request->attributes->set(self::CACHE_KEY, $role);
|
|
|
|
return $role;
|
|
}
|
|
|
|
/**
|
|
* guest 역할이 특정 권한을 보유하는지 판정합니다.
|
|
*
|
|
* 적재해 둔 권한 컬렉션에서 판정하므로 판정마다 쿼리가 나가지 않습니다.
|
|
*
|
|
* @param string $identifier 권한 식별자
|
|
* @param PermissionType|null $type 권한 타입 (null 이면 타입 무관)
|
|
* @return bool 보유 여부
|
|
*/
|
|
public static function hasPermission(string $identifier, ?PermissionType $type = null): bool
|
|
{
|
|
$role = self::resolve();
|
|
|
|
if (! $role) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($role->permissions as $permission) {
|
|
if ($permission->identifier !== $identifier) {
|
|
continue;
|
|
}
|
|
|
|
if ($type === null || self::permissionType($permission) === $type) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 캐시를 비웁니다.
|
|
*
|
|
* 권한 재시드(코어 업데이트 / 확장 설치) 직후처럼 같은 요청 안에서 권한 구성이
|
|
* 바뀌는 경우에 호출합니다.
|
|
*/
|
|
public static function flush(): void
|
|
{
|
|
request()->attributes->remove(self::CACHE_KEY);
|
|
}
|
|
|
|
/**
|
|
* 권한의 타입을 Enum 으로 정규화합니다.
|
|
*
|
|
* @param Permission $permission 권한 모델
|
|
* @return PermissionType|null 정규화된 타입 (해석 불가 시 null)
|
|
*/
|
|
private static function permissionType($permission): ?PermissionType
|
|
{
|
|
$type = $permission->type;
|
|
|
|
if ($type instanceof PermissionType) {
|
|
return $type;
|
|
}
|
|
|
|
return $type === null ? null : PermissionType::tryFrom((string) $type);
|
|
}
|
|
}
|