INSTALLER_COMPLETED=true 가 적힌 .env 를 빈 DB 새 서버에 복사한 뒤
php artisan migrate 로 테이블을 만들기 전에 앱이 부팅되면, installer_completed
fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 table not found 로
부팅을 깨뜨렸다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는
닭-달걀 상태가 된다.
fast-path 자체(매 요청 information_schema 쿼리 제거 성능 최적화)는 유지하되,
이미 존재하던 참조 구현(LanguagePackServiceProvider::isRegistryReady)의 방어
패턴을 InstallerContext::isSchemaMutatingCommand 로 SSoT 화하여 나머지 5곳에
통일 적용했다.
- app/Support/InstallerContext.php: 마이그레이션 계열 콘솔 명령 판정 헬퍼
(migrate/migrate:fresh/migrate:refresh/migrate:rollback/migrate:reset/db:wipe)
- Caches{Module,Plugin,Template}Status: fast-path 앞에 && 가드
- {Module,Plugin}RouteServiceProvider: hasTable 검증 진입 조건에 || 가드로
무방비 pluck 도달 차단
- LanguagePackServiceProvider: 인라인 argv 검사를 헬퍼로 치환(migrate:refresh 보강)
부류 B(NotificationHookListener, IdentityPolicyRepository)는 fast-path 이후
쿼리가 이미 try/catch 로 보호되어 빈 DB 에서도 안전하므로 수정 제외.
최초 설치 흐름은 migrate 시점에 완료 플래그가 없어(false) 이번 가드의 영향권
밖이며 원래대로 hasTable 폴백을 탄다. 가드가 실제 발동하는 유일 케이스는
"플래그 true + 빈 DB" 뿐이고, 정상 운영 웹 요청은 fast-path 성능이 유지된다.
회귀 테스트: InstallerContextTest 8건 + InstallerCompletedGuardTest 5건 추가
(수정 전 5건 fail → 수정 후 green). 관련 스위트 355 passed.
7.0.3 출시 버전 bump: 코어 버전 SSoT 7곳 + CHANGELOG 동기화.
150 lines
5.3 KiB
PHP
150 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Enums\ExtensionStatus;
|
|
use App\Extension\ExtensionManager;
|
|
use App\Extension\Testing\ExtensionTestAllowlist;
|
|
use App\Models\Module;
|
|
use App\Support\InstallerContext;
|
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class ModuleRouteServiceProvider extends ServiceProvider
|
|
{
|
|
/**
|
|
* The path to the "home" route for your application.
|
|
*
|
|
* Typically, users are redirected here after authentication.
|
|
*
|
|
* @var string
|
|
*/
|
|
public const HOME = '/home';
|
|
|
|
/**
|
|
* 라우트 모델 바인딩, 패턴 필터 및 기타 라우트 구성을 정의합니다.
|
|
*/
|
|
public function boot(): void
|
|
{
|
|
$this->routes(function () {
|
|
$this->loadModuleRoutes();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 모듈의 라우트 파일들을 로드합니다.
|
|
*
|
|
* 활성화된 모듈만 라우트를 등록합니다.
|
|
*/
|
|
protected function loadModuleRoutes(): void
|
|
{
|
|
// .env 파일이 없으면 스킵 (인스톨러 실행 전)
|
|
if (! File::exists(base_path('.env'))) {
|
|
return;
|
|
}
|
|
|
|
$modulesPath = base_path('modules');
|
|
|
|
if (! File::exists($modulesPath)) {
|
|
return;
|
|
}
|
|
|
|
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
|
|
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
|
|
//
|
|
// 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
|
|
// 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
|
// 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
|
|
// 아래 무방비 pluck 이 table not found 로 부팅을 깨뜨리는 것을 막는다.
|
|
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
|
try {
|
|
if (! Schema::hasTable('modules')) {
|
|
return;
|
|
}
|
|
|
|
// identifier 컬럼이 없으면 스킵 (마이그레이션 미적용 상태)
|
|
if (! Schema::hasColumn('modules', 'identifier')) {
|
|
return;
|
|
}
|
|
} catch (\Exception) {
|
|
// DB 연결 실패 시 조용히 스킵 (설정 오류, 마이그레이션 전 등)
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 활성화된 모듈 identifier 목록 가져오기
|
|
$activeModuleIdentifiers = Module::where('status', ExtensionStatus::Active->value)
|
|
->pluck('identifier')
|
|
->toArray();
|
|
|
|
$modules = File::directories($modulesPath);
|
|
$allowlistActive = ExtensionTestAllowlist::isActive();
|
|
|
|
foreach ($modules as $module) {
|
|
$moduleName = basename($module);
|
|
$moduleFile = $module.'/module.php';
|
|
|
|
// 테스트 환경 확장 격리: allowlist 밖 모듈의 라우트 등록 차단
|
|
if ($allowlistActive && ! ExtensionTestAllowlist::isAllowed('module', $moduleName)) {
|
|
continue;
|
|
}
|
|
|
|
// 활성화된 모듈만 라우트 로드
|
|
if (! in_array($moduleName, $activeModuleIdentifiers)) {
|
|
continue;
|
|
}
|
|
|
|
// 모듈 파일이 존재하는지 확인
|
|
if (! File::exists($moduleFile)) {
|
|
continue;
|
|
}
|
|
|
|
// 디렉토리명(vendor-module)을 네임스페이스(Vendor\Module)로 변환
|
|
$namespace = $this->convertDirectoryToNamespace($moduleName);
|
|
$moduleClass = "Modules\\{$namespace}\\Module";
|
|
|
|
// 클래스가 아직 로드되지 않은 경우에만 require
|
|
// (_bundled에서 이미 로드된 경우 중복 선언 방지)
|
|
if (! class_exists($moduleClass, false)) {
|
|
require_once $moduleFile;
|
|
}
|
|
|
|
if (! class_exists($moduleClass)) {
|
|
continue;
|
|
}
|
|
|
|
$moduleInstance = new $moduleClass;
|
|
$routes = $moduleInstance->getRoutes();
|
|
|
|
// API 라우트 로드
|
|
if (isset($routes['api']) && File::exists($routes['api'])) {
|
|
Route::prefix('api/modules/'.$moduleName)
|
|
->name('api.modules.'.$moduleName.'.')
|
|
->middleware('api')
|
|
->group($routes['api']);
|
|
}
|
|
|
|
// 웹 라우트 로드
|
|
if (isset($routes['web']) && File::exists($routes['web'])) {
|
|
Route::prefix('modules/'.$moduleName)
|
|
->name('web.modules.'.$moduleName.'.')
|
|
->middleware('web')
|
|
->group($routes['web']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 디렉토리명(vendor-module)을 네임스페이스(Vendor\Module)로 변환합니다.
|
|
*
|
|
* @param string $directoryName 디렉토리명 (예: sirsoft-sample)
|
|
* @return string 네임스페이스 (예: Sirsoft\Sample)
|
|
*/
|
|
protected function convertDirectoryToNamespace(string $directoryName): string
|
|
{
|
|
return ExtensionManager::directoryToNamespace($directoryName);
|
|
}
|
|
}
|