fix(extension): 확장 수명주기 캐시 무효화 순서 회귀 및 실패 사유 전달
route:cache 는 새 앱을 부팅해 라우트를 수집하고, 그 부팅의 확장 라우트 프로바이더는 DB 가 아니라 캐시된 활성 확장 목록을 읽는다. 그래서 rebuild 가 invalidate*StatusCache 보다 앞서면 방금 바뀐 상태가 빠진 채 라우트가 박제되고, 라우트 캐시에는 스캔 폴백이 없어 오류도 로그도 없이 404 가 된다. 무효화를 굽기 직전이 아니라 DB 상태 쓰기 직후로 올려, 같은 목록을 읽는 훅 매핑 캐시까지 함께 바로잡았다. update 경로는 Updating 전이 직후에 비우면 그 창의 오토로드 갱신이 확장을 비활성으로 판정하므로, 상태 복원 직후에 비운 뒤 훅 캐시를 다시 굽는다. 플러그인 라우트 프로바이더에는 활성 게이트가 없어 비활성 플러그인의 API 가 계속 응답했다. 화면·메뉴만 사라지고 기능은 살아 있는 상태였다. 모듈과 같은 기준을 적용했다. 실패 사유가 하위 계층에서 버려져 관리자 화면에 :error 자리표시자가 그대로 노출되던 문제도 고쳤다. 반환 경로를 깨지 않도록 배열 키 reason 과 뒤에 붙인 선택적 out 파라미터로 사유를 실어 올리고, 확장이 수명주기 훅에서 사유를 남길 수 있는 통로를 추가했다. 설치 경로의 광역 RuntimeException catch 는 도메인 예외로 좁혀 원본 키와 파라미터를 응답에 싣는다 — 상태코드 422 는 유지해 사용자 계약을 함께 바꾸지 않는다. 언어팩 화면은 프로덕션에서 예외 원문을 싣지 않는 것이 확정된 계약이므로, 자리를 일반 문구로 채우는 대신 치환 자리 자체를 제거했다. 원문은 종전대로 errors 통로를 거쳐 디버그 모드에서 도달한다.
This commit is contained in:
1 parent
565f6c0117
commit
03cbb99196
35 files changed
+1362
-239
No files matched your search
@@ -311,6 +311,7 @@ class ModuleManager implements ModuleManagerInterface
|
||||
* @param \Closure|null $onProgress 진행 콜백 (?string $step, string $message)
|
||||
* @param VendorMode $vendorMode vendor 디렉토리 처리 모드
|
||||
* @param bool $force 강제 설치 여부
|
||||
* @param string|null $failureReason 실패 시 사유가 담기는 out 파라미터 (성공 시 null)
|
||||
* @return bool 설치 성공 여부
|
||||
*
|
||||
* @throws \Exception 모듈을 찾을 수 없거나 의존성 문제 시
|
||||
@@ -320,7 +321,10 @@ class ModuleManager implements ModuleManagerInterface
|
||||
?\Closure $onProgress = null,
|
||||
VendorMode $vendorMode = VendorMode::Auto,
|
||||
bool $force = false,
|
||||
?string &$failureReason = null,
|
||||
): bool {
|
||||
$failureReason = null;
|
||||
|
||||
// identifier 형식 검증 (내부 호출 방어)
|
||||
ExtensionManager::validateIdentifierFormat($moduleName);
|
||||
|
||||
@@ -404,9 +408,12 @@ class ModuleManager implements ModuleManagerInterface
|
||||
$this->validateSeoVariables($module, 'module');
|
||||
|
||||
// 모듈 설치 실행
|
||||
$module->clearLifecycleFailureReason();
|
||||
$result = $module->install();
|
||||
|
||||
if (! $result) {
|
||||
$failureReason = $module->getLifecycleFailureReason() ?? __('modules.errors.unknown_error');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -540,9 +547,15 @@ class ModuleManager implements ModuleManagerInterface
|
||||
{
|
||||
$module = $this->getModule($moduleName);
|
||||
if (! $module) {
|
||||
return ['success' => false, 'layouts_registered' => 0];
|
||||
return [
|
||||
'success' => false,
|
||||
'layouts_registered' => 0,
|
||||
'reason' => __('modules.errors.not_found', ['module' => $moduleName]),
|
||||
];
|
||||
}
|
||||
|
||||
$module->clearLifecycleFailureReason();
|
||||
|
||||
// 상태 가드: 진행 중 상태 체크
|
||||
$record = $this->moduleRepository->findByIdentifier($module->getIdentifier());
|
||||
if ($record) {
|
||||
@@ -628,6 +641,13 @@ class ModuleManager implements ModuleManagerInterface
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 모듈 상태 캐시 무효화 — DB 상태 쓰기 직후에 둔다.
|
||||
// 뒤따르는 굽기(RouteCacheHelper::rebuild() 의 route:cache, 훅 캐시 재생성)는
|
||||
// 새 애플리케이션을 부팅해 "캐시된" 활성 모듈 목록을 읽는다. 여기서 비우지 않으면
|
||||
// 방금 활성으로 바뀐 이 모듈이 목록에서 빠진 채 라우트가 박제되고,
|
||||
// 라우트 캐시에는 스캔 폴백이 없어 오류·경고 없이 그 엔드포인트만 404 가 된다.
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// soft deleted된 모듈 레이아웃 복원 (재활성화 시)
|
||||
$this->restoreModuleLayouts($module->getIdentifier());
|
||||
|
||||
@@ -650,9 +670,6 @@ class ModuleManager implements ModuleManagerInterface
|
||||
$this->incrementExtensionCacheVersion();
|
||||
RouteCacheHelper::rebuild();
|
||||
|
||||
// 모듈 상태 캐시 무효화
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 본인인증 route scope 캐시 무효화 — 재활성화 시 이 모듈이 선언한 정책이
|
||||
// 다시 enforce 대상에 포함되도록 한다 (applyActiveExtensionScope 재평가).
|
||||
IdentityPolicy::flushRouteScopeCache();
|
||||
@@ -667,7 +684,18 @@ class ModuleManager implements ModuleManagerInterface
|
||||
HookManager::doAction('core.modules.activated', $moduleName);
|
||||
}
|
||||
|
||||
return ['success' => $result, 'layouts_registered' => $layoutsRegistered];
|
||||
if (! $result) {
|
||||
// 모듈이 스스로 활성화를 거부했다. 사유를 남겼으면 그대로 싣고,
|
||||
// 남기지 않았으면 일반 문구로 대체한다 — 원인 자리를 비워 두면
|
||||
// 관리자 화면에 치환되지 않은 자리표시자가 그대로 노출된다.
|
||||
return [
|
||||
'success' => false,
|
||||
'layouts_registered' => $layoutsRegistered,
|
||||
'reason' => $module->getLifecycleFailureReason() ?? __('modules.errors.unknown_error'),
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => true, 'layouts_registered' => $layoutsRegistered];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -714,9 +742,15 @@ class ModuleManager implements ModuleManagerInterface
|
||||
): array {
|
||||
$module = $this->getModule($moduleName);
|
||||
if (! $module) {
|
||||
return ['success' => false, 'layouts_deleted' => 0];
|
||||
return [
|
||||
'success' => false,
|
||||
'layouts_deleted' => 0,
|
||||
'reason' => __('modules.errors.not_found', ['module' => $moduleName]),
|
||||
];
|
||||
}
|
||||
|
||||
$module->clearLifecycleFailureReason();
|
||||
|
||||
// 상태 가드: 진행 중 상태 체크
|
||||
$record = $this->moduleRepository->findByIdentifier($module->getIdentifier());
|
||||
if ($record) {
|
||||
@@ -777,6 +811,12 @@ class ModuleManager implements ModuleManagerInterface
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 모듈 상태 캐시 무효화 — DB 상태 쓰기 직후에 둔다.
|
||||
// 뒤따르는 RouteCacheHelper::rebuild() 가 캐시된 활성 모듈 목록을 읽으므로,
|
||||
// 여기서 비우지 않으면 방금 비활성으로 바꾼 모듈의 라우트가 그대로 박제되어
|
||||
// 비활성 상태에서도 그 API 가 계속 호출 가능한 상태로 남는다.
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 모듈 레이아웃 soft delete
|
||||
$layoutsDeleted = $this->softDeleteModuleLayouts($module->getIdentifier());
|
||||
|
||||
@@ -796,9 +836,6 @@ class ModuleManager implements ModuleManagerInterface
|
||||
// 모듈 자체 캐시 전체 정리
|
||||
$this->flushModuleCache($module);
|
||||
|
||||
// 모듈 상태 캐시 무효화
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 본인인증 route scope 캐시 무효화 — 비활성 모듈이 선언한 정책이 enforce 대상에서
|
||||
// 즉시 제외되도록 한다. 정책 행 자체는 변경하지 않으므로(enabled 운영자 설정 보존)
|
||||
// IdentityPolicy 모델 이벤트가 발화하지 않아, 라이프사이클에서 명시적으로 호출한다.
|
||||
@@ -811,7 +848,15 @@ class ModuleManager implements ModuleManagerInterface
|
||||
HookManager::doAction('core.modules.after_deactivate', $module->getIdentifier());
|
||||
}
|
||||
|
||||
return ['success' => $result, 'layouts_deleted' => $layoutsDeleted];
|
||||
if (! $result) {
|
||||
return [
|
||||
'success' => false,
|
||||
'layouts_deleted' => $layoutsDeleted,
|
||||
'reason' => $module->getLifecycleFailureReason() ?? __('modules.errors.unknown_error'),
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => true, 'layouts_deleted' => $layoutsDeleted];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -859,12 +904,19 @@ class ModuleManager implements ModuleManagerInterface
|
||||
* @param string $moduleName 제거할 모듈명
|
||||
* @param bool $deleteData 모듈 데이터(테이블) 삭제 여부
|
||||
* @param \Closure|null $onProgress 진행 콜백 (?string $step, string $message)
|
||||
* @param string|null $failureReason 실패 시 사유가 담기는 out 파라미터 (성공 시 null)
|
||||
* @return bool 제거 성공 여부
|
||||
*
|
||||
* @throws \Exception 모듈을 찾을 수 없을 때
|
||||
*/
|
||||
public function uninstallModule(string $moduleName, bool $deleteData = false, ?\Closure $onProgress = null): bool
|
||||
{
|
||||
public function uninstallModule(
|
||||
string $moduleName,
|
||||
bool $deleteData = false,
|
||||
?\Closure $onProgress = null,
|
||||
?string &$failureReason = null,
|
||||
): bool {
|
||||
$failureReason = null;
|
||||
|
||||
// 상태 가드: 진행 중 상태 체크
|
||||
$existingRecord = $this->moduleRepository->findByIdentifier($moduleName);
|
||||
if ($existingRecord) {
|
||||
@@ -897,8 +949,13 @@ class ModuleManager implements ModuleManagerInterface
|
||||
DB::beginTransaction();
|
||||
|
||||
// 모듈 제거 실행
|
||||
$module->clearLifecycleFailureReason();
|
||||
$result = $module->uninstall();
|
||||
|
||||
if (! $result) {
|
||||
$failureReason = $module->getLifecycleFailureReason() ?? __('modules.errors.unknown_error');
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
// 권한·메뉴·역할은 $deleteData=true 시에만 삭제.
|
||||
// false 시 보존하여 재설치 시 기존 역할 할당/커스터마이징이 복원 가능하도록 한다.
|
||||
@@ -960,6 +1017,12 @@ class ModuleManager implements ModuleManagerInterface
|
||||
|
||||
// 오토로드 병합 실행 (트랜잭션 외부에서 실행)
|
||||
if ($result) {
|
||||
// 모듈 상태 캐시 무효화 — DB 에서 모듈 행을 지운 직후(커밋 직후)에 둔다.
|
||||
// 뒤따르는 굽기(오토로드 갱신 내 훅 캐시 재생성, RouteCacheHelper::rebuild())가
|
||||
// 캐시된 활성 모듈 목록을 읽으므로, 여기서 비우지 않으면 이미 제거된 모듈이
|
||||
// 목록에 남은 채로 라우트·훅이 박제된다.
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
$onProgress?->__invoke('autoload', '오토로드 갱신 중...');
|
||||
$this->extensionManager->updateComposerAutoload();
|
||||
|
||||
@@ -977,9 +1040,6 @@ class ModuleManager implements ModuleManagerInterface
|
||||
// 모듈 자체 캐시 전체 정리
|
||||
$this->flushModuleCache($module);
|
||||
|
||||
// 모듈 상태 캐시 무효화
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 확장 미들웨어 인덱스 무효화 — 제거된 모듈의 미들웨어가 게이트 매칭에서 즉시 제외.
|
||||
ExtensionMiddlewareRegistry::flush();
|
||||
|
||||
@@ -4495,6 +4555,13 @@ class ModuleManager implements ModuleManagerInterface
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 모듈 상태 캐시 무효화 — 상태 복원 쓰기 직후에 둔다.
|
||||
// Updating 전이 직후에는 비우지 않는다: 그러면 Updating 창 안의
|
||||
// updateComposerAutoload() 가 DB 를 재조회해 이 모듈을 비활성으로 판정하고
|
||||
// 훅 캐시에서 리스너를 떨군다(지금 없는 결함을 새로 만든다).
|
||||
// 복원 직후에 비워야 뒤따르는 굽기(라우트·훅)가 복원된 상태를 읽는다.
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 9. 레이아웃 갱신 (이전 상태가 active였으면)
|
||||
// refreshModuleLayouts()는 캐시 무효화 + 캐시 버전 증가를 포함
|
||||
$onProgress?->__invoke('layout', '레이아웃 갱신 중...');
|
||||
@@ -4518,7 +4585,13 @@ class ModuleManager implements ModuleManagerInterface
|
||||
$this->clearAllTemplateRoutesCaches();
|
||||
$this->incrementExtensionCacheVersion();
|
||||
RouteCacheHelper::rebuild();
|
||||
self::invalidateModuleStatusCache();
|
||||
|
||||
// 훅 캐시 재생성 — Updating 창 안의 updateComposerAutoload() 가 구운 훅 캐시에는
|
||||
// 그 시점 이 모듈이 Updating(=비활성)으로 판정되어 리스너가 통째로 빠져 있을 수 있다.
|
||||
// 훅 캐시 폴백은 파일 부재/손상에만 작동하므로 내용이 stale 한 경우는 조용히 통과한다.
|
||||
// 상태를 복원하고 상태 캐시를 비운 지금 다시 구워야 그 누락이 교정된다.
|
||||
// updateComposerAutoload() 전체를 재호출하지 않는다 — composer autoload 병합은 이미 끝났고 비싸다.
|
||||
$this->extensionManager->regenerateHookCache();
|
||||
|
||||
// 훅 발행: 모듈 업데이트 완료 (Artisan 직접 호출 시에도 리스너 트리거)
|
||||
HookManager::doAction('core.modules.updated', $identifier);
|
||||
|
||||
Reference in new issue
Block a user