Files
Gnuboard7/app/Extension/Traits/InvalidatesLayoutCache.php
T
HeuJung de18159986 fix(core,admin_basic): 정적 게시 재게시 누락 3건 수정 + 관리자 수동 복구 UI + sudo core:update 소유권 정합
- 템플릿 update 는 레이아웃 변경 여부와 무관하게 캐시 버전을 올리고(실패 복원 뒤에도), 자산 주소 방식 전환 3경로도 bump 한다
- config:cache 가 컨테이너 인스턴스를 덮어 이후 terminating 재게시가 사라지던 결함을 복원 헬퍼로 차단
- custom/ 변경 감지와 게시가 같은 열거자(재귀·크기 포함)를 쓰고, 서명은 호스트별로 저장
- 캐시 버전·서명 키를 만료시키지 않는다(기본 TTL 24h 로 매일 전체 재생성되던 문제)
- 관리자 > 환경설정 > 일반 「초기 화면 정적 파일」 카드: 상태 조회 + 지금 다시 만들기(확인 모달), 대시보드 알림 버튼 연결, CLI 와 같은 statusReport 소비
- sudo core:update 경로의 root 소유 잔존(설정 디렉토리·임시 폴더·로그·업그레이드 마이그레이션 산출물) 상속·정합화, ext-static 명령 root 경고와 sudo -u 힌트
- 안내·문서·트러블슈팅 4건·audit 룰 ext-cache-version-raw-read·ja 언어팩 동기
2026-09-06 15:22:52 +09:00

180 lines
8.9 KiB
PHP

<?php
namespace App\Extension\Traits;
use App\Contracts\Extension\CacheInterface;
use App\Contracts\Repositories\LayoutRepositoryInterface;
use App\Enums\LayoutSourceType;
use App\Extension\Cache\CoreCacheDriver;
use App\Services\ExtensionStaticCacheService;
use Illuminate\Support\Facades\Log;
/**
* 레이아웃 캐시 무효화 공통 로직을 제공하는 Trait
*
* 버전 없는 내부 캐시 키를 능동 삭제합니다:
* 1. template.{templateId}.layout.{layoutName} - LayoutService에서 사용
* 2. template.{templateId}.layout.{layoutName}.{sourceHash} - 모듈/플러그인 레이아웃
* 3. 위 두 형태의 `.with_source_meta` 접미사 변종 - 편집기용 병합 응답 캐시
*
* 버전 포함 캐시 (layout.{identifier}.{name}.v{version})는
* incrementExtensionCacheVersion() + TTL로 무효화됩니다.
* 레이아웃 내용 편집 시에만 현재 버전 키를 능동 삭제합니다 — 일반 응답 키와
* 레이아웃 편집기 응답 키(`.meta` 접미사, `with_source_meta=1`) 두 가지 모두.
*
* 이 Trait를 사용하는 클래스는 반드시 다음 속성/메서드를 제공해야 합니다:
* - $layoutRepository: LayoutRepositoryInterface 인스턴스
*
* @property LayoutRepositoryInterface $layoutRepository
*/
trait InvalidatesLayoutCache
{
/**
* 확장(모듈/플러그인)의 레이아웃 캐시를 무효화합니다.
*
* @param string $extensionIdentifier 확장 식별자 (모듈 또는 플러그인)
* @param string $extensionType 확장 타입 ('module' 또는 'plugin')
*/
protected function invalidateExtensionLayoutCache(string $extensionIdentifier, string $extensionType = 'module'): void
{
try {
// 확장 타입에 따라 올바른 소스 타입으로 레이아웃 조회
$sourceType = $extensionType === 'plugin' ? LayoutSourceType::Plugin : LayoutSourceType::Module;
$extensionLayouts = $this->layoutRepository->getBySourceIdentifier($extensionIdentifier, $sourceType);
foreach ($extensionLayouts as $layout) {
// 레이아웃에 연결된 템플릿 식별자 조회
$templateIdentifier = $layout->template?->identifier ?? '';
$this->forgetLayoutCacheKeys($layout, $templateIdentifier);
}
Log::info("{$extensionType} 레이아웃 캐시 무효화 완료: {$extensionIdentifier}");
} catch (\Exception $e) {
Log::warning("레이아웃 캐시 무효화 중 오류: {$extensionIdentifier}", [
'type' => $extensionType,
'error' => $e->getMessage(),
]);
}
}
/**
* 템플릿의 레이아웃 캐시를 무효화합니다.
*
* @param int $templateId 템플릿 ID
* @param string $templateIdentifier 템플릿 식별자 (PublicLayoutController 캐시 삭제에 필요)
*/
protected function invalidateTemplateLayoutCache(int $templateId, string $templateIdentifier = ''): void
{
try {
// 개별 레이아웃 캐시 삭제
$layouts = $this->layoutRepository->getByTemplateId($templateId);
foreach ($layouts as $layout) {
$this->forgetLayoutCacheKeys($layout, $templateIdentifier);
}
if ($templateIdentifier) {
Log::info("템플릿 레이아웃 캐시 무효화 완료: {$templateIdentifier}");
}
} catch (\Exception $e) {
Log::warning('레이아웃 캐시 무효화 중 오류', [
'template_id' => $templateId,
'template_identifier' => $templateIdentifier,
'error' => $e->getMessage(),
]);
}
}
/**
* 단일 레이아웃의 캐시 키를 삭제합니다.
*
* 버전 없는 내부 캐시는 능동 삭제합니다.
* 버전 포함 PublicLayoutController 캐시는 현재 버전 키만 삭제합니다
* (레이아웃 내용 편집 시 버전 변경 없이 내용만 바뀌는 경우에 필요).
*
* @param object $layout 레이아웃 모델 (template_id, name, source_type, source_identifier 필드 필요)
* @param string $templateIdentifier 템플릿 식별자 (PublicLayoutController 캐시 삭제에 필요)
*/
protected function forgetLayoutCacheKeys(object $layout, string $templateIdentifier = ''): void
{
$cache = $this->resolveLayoutCache();
// 1. LayoutService 내부 캐시 (버전 없음 → 능동 삭제)
// 편집기용 병합 응답은 `.with_source_meta` 접미사 키로 별도 캐싱되므로
// (LayoutService::getMergedLayoutCacheKey) 두 변종을 함께 지운다 —
// 접미사 키를 남기면 activate/deactivate/uninstall/refresh 후에도
// 편집기가 stale 병합 캐시를 받는다 (#588).
$cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name));
$cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, withSourceMeta: true));
// 2. 소스 해시 포함 키 (버전 없음 → 능동 삭제) — 접미사는 소스 해시 뒤
if ($layout->source_type && $layout->source_identifier) {
$sourceType = $layout->source_type->value;
$cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, $sourceType, $layout->source_identifier));
$cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, $sourceType, $layout->source_identifier, true));
}
// 3. PublicLayoutController 캐시 (버전 포함) — 일반 응답 + 편집기(`.meta`) 응답 두 키 모두.
// PublicLayoutController::serve() 가 `with_source_meta=1`(레이아웃 편집기) 응답을 `.meta`
// 접미사 별도 키로 캐싱하므로, 그 키를 함께 삭제하지 않으면 템플릿/레이아웃 상태 변화
// (refresh-layout / activate / deactivate / uninstall 등) 후에도 편집기가 stale 캐시를
// 받는다. 레이아웃 저장 경로(LayoutService::clearPublicServingCache)는 이미 두 키를
// 지우므로 정합을 맞춘다.
if ($templateIdentifier) {
// 트레이트 게터 경유 — 이 트레이트만 조합한 클래스가 있을 수 있어 `self::` 가 아니라
// `ClearsTemplateCaches` 를 조합한 서비스 클래스를 통해 부른다(트레이트 정적 직접 호출은
// PHP 8.1+ E_DEPRECATED). 원시 키 읽기는 `cache:clear` 직후 0 을 돌려주어 실제 키를 못 지운다.
$cacheVersion = ExtensionStaticCacheService::getExtensionCacheVersion();
$cache->forget("layout.{$templateIdentifier}.{$layout->name}.v{$cacheVersion}");
$cache->forget("layout.{$templateIdentifier}.{$layout->name}.v{$cacheVersion}.meta");
}
}
/**
* 레이아웃 캐시 무효화에 사용할 코어 캐시 드라이버를 반환합니다.
*
* 레이아웃 캐시 키(`template.*.layout.*`, `layout.*` 등)는 모두 코어 소유
* 키이므로 항상 `g7:core:` 접두사 네임스페이스에서 저장/삭제되어야 한다.
* 따라서 컨테이너의 `CacheInterface` 바인딩(모듈/플러그인 테스트가 일시적으로
* `PluginCacheDriver` 등으로 재바인딩할 수 있음)에 의존하지 않고 항상
* CoreCacheDriver 를 직접 생성한다. 의존 시 누수된 바인딩 때문에
* `g7:plugin.*` 네임스페이스로 forget 이 빗나가 캐시가 실제로 삭제되지 않는다.
*/
private function resolveLayoutCache(): CacheInterface
{
return new CoreCacheDriver(config('cache.default', 'array'));
}
/**
* 레이아웃 캐시 키를 생성합니다.
*
* `$withSourceMeta` 가 true 면 `.with_source_meta` 접미사를 붙인다 — 편집기용
* 병합 응답 키(LayoutService::getMergedLayoutCacheKey 와 동형, 접미사는 소스 해시 뒤).
*
* @param int $templateId 템플릿 ID
* @param string $layoutName 레이아웃 이름
* @param string|null $sourceType 소스 타입 (선택)
* @param string|null $sourceIdentifier 소스 식별자 (선택)
* @param bool $withSourceMeta 편집기용 병합 응답 키 여부 (선택)
* @return string 캐시 키
*/
protected function buildLayoutCacheKey(
int $templateId,
string $layoutName,
?string $sourceType = null,
?string $sourceIdentifier = null,
bool $withSourceMeta = false
): string {
$baseKey = "template.{$templateId}.layout.{$layoutName}";
$metaSuffix = $withSourceMeta ? '.with_source_meta' : '';
if ($sourceType && $sourceIdentifier) {
$sourceHash = md5($sourceType.$sourceIdentifier);
return "{$baseKey}.{$sourceHash}{$metaSuffix}";
}
return "{$baseKey}{$metaSuffix}";
}
}