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}"; } }