fix(core): installer_completed fast-path가 빈 DB 배포 시 부팅을 깨뜨리는 문제 수정 및 7.0.3 릴리스
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 동기화.
This commit is contained in:
1 parent
13aeb433cc
commit
c26b49a997
16 files changed
+386
-23
No files matched your search
+1
-1
@@ -3,7 +3,7 @@ APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_VERSION=7.0.2
|
||||
APP_VERSION=7.0.3
|
||||
|
||||
APP_LOCALE=ko
|
||||
APP_FALLBACK_LOCALE=ko
|
||||
|
||||
@@ -3,7 +3,7 @@ APP_ENV=testing
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_VERSION=7.0.2
|
||||
APP_VERSION=7.0.3
|
||||
|
||||
APP_LOCALE=ko
|
||||
APP_FALLBACK_LOCALE=ko
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [7.0.3] - 2026-07-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- 이미 설치를 마친 환경의 설정 파일(`.env`)을 새 서버로 복사해, 데이터베이스가 아직 비어 있는 상태에서 설치를 이어가려 할 때 `php artisan migrate`로 테이블을 만들기도 전에 부팅 단계에서 오류가 나며 중단되던 문제를 수정했습니다. 이제 데이터베이스 작업 명령이 실행되는 동안에는 테이블 존재 여부를 실제로 확인하므로, 기존 서버의 설정을 재사용하는 신규 배포에서도 설치가 정상적으로 진행됩니다. (#41 @GyusoonKim 님께서 제보해주셨습니다.)
|
||||
|
||||
## [7.0.2] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@ unzip g7-release.zip
|
||||
|
||||
# 압축 해제 결과 확인 — 루트 디렉토리가 g7이 아니면 이름 변경
|
||||
ls -la
|
||||
# (필요 시) mv g7-7.0.2 g7
|
||||
# (필요 시) mv g7-7.0.3 g7
|
||||
|
||||
# ZIP 파일 정리 (선택)
|
||||
rm g7-release.zip
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.2-blue" alt="Version"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/version-7.0.3-blue" alt="Version"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/PHP-8.2%2B-777BB4?logo=php&logoColor=white" alt="PHP"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/Laravel-12.x-FF2D20?logo=laravel&logoColor=white" alt="Laravel"></a>
|
||||
<a href="#"><img src="https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black" alt="React"></a>
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Module;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -63,8 +64,6 @@ trait CachesModuleStatus
|
||||
/**
|
||||
* 모듈 상태 캐시를 무효화합니다.
|
||||
* 모듈 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidateModuleStatusCache(): void
|
||||
{
|
||||
@@ -79,10 +78,14 @@ trait CachesModuleStatus
|
||||
* 설치 완료 상태(`config('app.installer_completed')`)일 때는 테이블 존재를
|
||||
* 전제로 하여 `Schema::hasTable()` 호출을 건너뜁니다. 인스톨러 이전 환경이나
|
||||
* 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isExtensionTableReady(string $table): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Plugin;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -63,8 +64,6 @@ trait CachesPluginStatus
|
||||
/**
|
||||
* 플러그인 상태 캐시를 무효화합니다.
|
||||
* 플러그인 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidatePluginStatusCache(): void
|
||||
{
|
||||
@@ -76,10 +75,14 @@ trait CachesPluginStatus
|
||||
/**
|
||||
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
|
||||
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isPluginTableReady(): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Contracts\Extension\CacheInterface;
|
||||
use App\Enums\ExtensionStatus;
|
||||
use App\Extension\Cache\CoreCacheDriver;
|
||||
use App\Models\Template;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -86,8 +87,6 @@ trait CachesTemplateStatus
|
||||
/**
|
||||
* 템플릿 상태 캐시를 무효화합니다.
|
||||
* 템플릿 상태 변경 시 (install, activate, deactivate, uninstall) 호출해야 합니다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function invalidateTemplateStatusCache(): void
|
||||
{
|
||||
@@ -101,10 +100,14 @@ trait CachesTemplateStatus
|
||||
/**
|
||||
* 설치 완료 상태에서는 `Schema::hasTable()` 호출을 건너뜁니다.
|
||||
* 인스톨러 이전 환경이나 테스트에서는 기존 체크 경로로 폴백합니다.
|
||||
*
|
||||
* 단, 마이그레이션 계열 명령 실행 중에는 `INSTALLER_COMPLETED=true` 라도 테이블이
|
||||
* 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
* 신뢰하지 않고 실제 `Schema::hasTable()` 폴백으로 진입합니다.
|
||||
*/
|
||||
private static function isTemplateTableReady(): bool
|
||||
{
|
||||
if (config('app.installer_completed')) {
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Services\LanguagePack\LanguagePackBundledRegistrar;
|
||||
use App\Services\LanguagePack\LanguagePackRegistry;
|
||||
use App\Services\LanguagePack\LanguagePackSeedInjector;
|
||||
use App\Services\LanguagePack\LanguagePackTranslator;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -477,8 +478,7 @@ class LanguagePackServiceProvider extends ServiceProvider
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->app->runningInConsole()
|
||||
&& in_array($_SERVER['argv'][1] ?? null, ['migrate', 'migrate:fresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'], true)) {
|
||||
if (InstallerContext::isSchemaMutatingCommand()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -52,7 +53,12 @@ class ModuleRouteServiceProvider extends ServiceProvider
|
||||
|
||||
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
|
||||
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
|
||||
if (! config('app.installer_completed')) {
|
||||
//
|
||||
// 단, 마이그레이션 계열 명령 실행 중에는 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;
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Providers;
|
||||
|
||||
use App\Extension\ExtensionManager;
|
||||
use App\Extension\Testing\ExtensionTestAllowlist;
|
||||
use App\Support\InstallerContext;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -48,7 +49,12 @@ class PluginRouteServiceProvider extends ServiceProvider
|
||||
|
||||
// 설치 완료 상태에서는 Schema introspection 을 건너뜀 (매 요청 쿼리 제거).
|
||||
// 인스톨러 이전 환경에서는 기존 체크 경로로 폴백.
|
||||
if (! config('app.installer_completed')) {
|
||||
//
|
||||
// 단, 마이그레이션 계열 명령 실행 중에는 INSTALLER_COMPLETED=true 라도 테이블이
|
||||
// 아직 없을 수 있으므로(빈 DB 새 서버에 .env 복사 후 migrate 전) fast-path 를
|
||||
// 무력화하고 hasTable 검증 경로로 진입한다. 빈 DB 면 여기서 early return 하여
|
||||
// 이후 플러그인 부팅/상태 조회가 table not found 로 부팅을 깨뜨리는 것을 막는다.
|
||||
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
||||
try {
|
||||
if (! Schema::hasTable('plugins')) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* 부팅 시점 "DB 스키마 준비성" 판정에 쓰이는 인스톨러 컨텍스트 헬퍼.
|
||||
*
|
||||
* `config('app.installer_completed')` fast-path 는 프로덕션에서 `Schema::hasTable()`
|
||||
* 호출을 건너뛰어 매 요청 오버헤드를 제거한다. 그러나 `INSTALLER_COMPLETED=true` 가
|
||||
* 기록된 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을
|
||||
* 만들기 전에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는
|
||||
* 쿼리가 "table not found" 로 부팅을 깨뜨린다.
|
||||
*
|
||||
* 이 헬퍼는 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 단일
|
||||
* SSoT 로 캡슐화하여, 해당 컨텍스트에서 fast-path 를 무력화하는 가드에 쓰인다.
|
||||
*/
|
||||
class InstallerContext
|
||||
{
|
||||
/**
|
||||
* 스키마를 파괴/재생성하는 콘솔 명령(마이그레이션 계열)이 실행 중인지 판정합니다.
|
||||
*
|
||||
* 이 컨텍스트에서는 `installer_completed=true` 라도 테이블이 아직 없을 수 있으므로
|
||||
* fast-path(hasTable 스킵)를 신뢰해서는 안 됩니다. 웹 요청 등 콘솔이 아닌
|
||||
* 실행 경로에서는 항상 false 를 반환합니다.
|
||||
*
|
||||
* @return bool 마이그레이션 계열 명령 실행 중 여부
|
||||
*/
|
||||
public static function isSchemaMutatingCommand(): bool
|
||||
{
|
||||
if (! app()->runningInConsole()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$_SERVER['argv'][1] ?? null,
|
||||
['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -231,7 +231,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => env('APP_VERSION', '7.0.2'),
|
||||
'version' => env('APP_VERSION', '7.0.3'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -334,11 +334,14 @@ protected function loadModuleRoutes(): void
|
||||
|
||||
### 동작 표
|
||||
|
||||
| 환경 | `installer_completed` | 동작 |
|
||||
|------|----------------------|------|
|
||||
| 프로덕션 (설치 완료) | `true` | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
|
||||
| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
|
||||
| 테스트 (`.env.testing` 에 플래그 없음) | `false` | 기존 `hasTable` 경로 |
|
||||
| 환경 | `installer_completed` | 마이그레이션 명령 | 동작 |
|
||||
|------|----------------------|------------------|------|
|
||||
| 프로덕션 (설치 완료, 웹 요청) | `true` | 아님 | 가드 통과 → `hasTable` 스킵 (쿼리 0건) |
|
||||
| 설치 완료 `.env` 복사 + 빈 DB (마이그레이션 전) | `true` | **실행 중** | fast-path 무력화 → `hasTable` 폴백 (table not found 부팅 실패 방지) |
|
||||
| 인스톨러 실행 중 / 마이그레이션 전 | `false` (기본값) | — | 기존 `hasTable` 폴백 경로 (원본 동작 보존) |
|
||||
| 테스트 (`.env.testing` 에 플래그 없음) | `false` | — | 기존 `hasTable` 경로 |
|
||||
|
||||
두 번째 행이 이 가드의 핵심 케이스다. `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 테이블 존재를 잘못 전제하여 뒤따르는 쿼리가 "table not found" 로 부팅을 깨뜨린다. 이 컨텍스트에서는 fast-path 를 신뢰하지 않고 실제 `hasTable` 검증 경로로 폴백해야 한다 (아래 "마이그레이션 컨텍스트 가드" 참조).
|
||||
|
||||
### 적용 가이드
|
||||
|
||||
@@ -346,6 +349,66 @@ protected function loadModuleRoutes(): void
|
||||
- **`hasTable` 폴백 경로는 반드시 유지** — `installer_completed=false` 환경에서도 안전하게 부팅되어야 함
|
||||
- **try/catch 도 그대로 유지** — DB 연결 실패 시 안전한 스킵 계약 준수
|
||||
- 가드는 hasTable 체크 **전체 블록을 `if (! config('app.installer_completed'))` 로 래핑** 하는 형태
|
||||
- **fast-path 이후 쿼리가 try/catch 로 보호되지 않는 지점은 마이그레이션 컨텍스트 가드를 함께 적용** (아래 참조). 예외로 보호되는 지점은 빈 DB 에서도 안전하므로 불필요
|
||||
|
||||
### 마이그레이션 컨텍스트 가드
|
||||
|
||||
`installer_completed` fast-path 는 테이블 존재를 전제한다. 그러나 `INSTALLER_COMPLETED=true` 가 적힌 `.env` 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate` 로 테이블을 만들기 **전** 에 앱이 부팅되면, fast-path 가 `true` 를 반환하고 곧이어 실제 쿼리(`Model::where()->pluck()` 등)가 실행되어 "table not found" 로 부팅이 실패한다. 마이그레이션을 실행하려는 부팅에서 그 명령이 오히려 실패하는 닭-달걀 상태다.
|
||||
|
||||
이를 막기 위해, 스키마를 파괴/재생성하는 마이그레이션 계열 콘솔 명령 판정을 `App\Support\InstallerContext::isSchemaMutatingCommand()` 로 단일 SSoT 화하고, fast-path 지점에 가드로 결합한다. 명령 리스트: `migrate`, `migrate:fresh`, `migrate:refresh`, `migrate:rollback`, `migrate:reset`, `db:wipe`.
|
||||
|
||||
```php
|
||||
// App\Support\InstallerContext
|
||||
public static function isSchemaMutatingCommand(): bool
|
||||
{
|
||||
if (! app()->runningInConsole()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(
|
||||
$_SERVER['argv'][1] ?? null,
|
||||
['migrate', 'migrate:fresh', 'migrate:refresh', 'migrate:rollback', 'migrate:reset', 'db:wipe'],
|
||||
true
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
가드 결합 방식은 지점의 **논리 극성** 에 따라 다르다:
|
||||
|
||||
- **Trait (fast-path 통과 조건)** — `&&` 로 결합. 마이그레이션 중이면 fast-path 를 건너뛰고 hasTable 폴백으로 진입.
|
||||
|
||||
```php
|
||||
if (! InstallerContext::isSchemaMutatingCommand() && config('app.installer_completed')) {
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
- **RouteServiceProvider (hasTable 검증 블록 진입 조건)** — `||` 로 결합. 마이그레이션 중이면 hasTable 검증 경로로 진입해 빈 DB 시 early return → 무방비 `pluck` 도달 차단.
|
||||
|
||||
```php
|
||||
if (! config('app.installer_completed') || InstallerContext::isSchemaMutatingCommand()) {
|
||||
try {
|
||||
if (! Schema::hasTable('modules')) {
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
} catch (\Exception) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`isRegistryReady()` 처럼 fast-path 앞에서 조기 반환하는 지점은 `if (InstallerContext::isSchemaMutatingCommand()) { return false; }` 로 단순 게이트한다.
|
||||
|
||||
#### 부류 A (가드 필수) vs 부류 B (불필요)
|
||||
|
||||
| 부류 | 조건 | 예 | 조치 |
|
||||
|------|------|-----|------|
|
||||
| A | fast-path 이후 쿼리가 try/catch 로 **미보호** → 빈 DB 에서 부팅 실패 | `Caches{Module,Plugin,Template}Status`, `Module/PluginRouteServiceProvider` | 마이그레이션 컨텍스트 가드 적용 |
|
||||
| B | fast-path 이후 쿼리가 try/catch(\Throwable) 로 **보호** → 예외를 삼켜 안전한 기본값 반환 | `NotificationHookListener`, `IdentityPolicyRepository::{listHookTargets,activeExtensionIdentifiers}` | 가드 불필요 (예외 흡수로 안전) |
|
||||
|
||||
### config 캐시 재생성 주의
|
||||
|
||||
- `config:cache` 는 config 소스를 변경하는 라이프사이클에서 `App\Support\ConfigCacheHelper::rebuild()` 로 자동 재생성된다 (아래 "config 캐시 자동 재생성" 참조). `.env` 를 직접 편집한 경우처럼 헬퍼를 거치지 않는 변경은 여전히 `php artisan config:clear && php artisan config:cache` 를 수동 실행한다
|
||||
|
||||
### 확장 Trait 패턴
|
||||
|
||||
@@ -7,8 +7,11 @@ use App\Extension\ModuleManager;
|
||||
use App\Extension\PluginManager;
|
||||
use App\Extension\TemplateManager;
|
||||
use App\Listeners\NotificationHookListener;
|
||||
use App\Providers\ModuleRouteServiceProvider;
|
||||
use App\Providers\PluginRouteServiceProvider;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use ReflectionClass;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
@@ -22,12 +25,43 @@ class InstallerCompletedGuardTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @var array<int, string>|null 원본 argv 백업 */
|
||||
private ?array $originalArgv = null;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
config(['app.installer_completed' => false]);
|
||||
|
||||
if ($this->originalArgv !== null) {
|
||||
$_SERVER['argv'] = $this->originalArgv;
|
||||
$this->originalArgv = null;
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* $_SERVER['argv'] 를 마이그레이션 명령으로 임시 설정하고 콜백을 실행합니다.
|
||||
*
|
||||
* PHPUnit 은 이미 콘솔 컨텍스트이므로, argv[1] 만 'migrate' 로 바꾸면
|
||||
* InstallerContext::isSchemaMutatingCommand() 가 true 가 되어 fast-path 가 무력화된다.
|
||||
* argv 는 finally 에서 반드시 원복해 다른 테스트 오염을 막는다.
|
||||
*
|
||||
* @param callable $callback 마이그레이션 컨텍스트에서 실행할 코드
|
||||
*/
|
||||
private function withMigrationArgv(callable $callback): void
|
||||
{
|
||||
$this->originalArgv = $_SERVER['argv'] ?? [];
|
||||
$_SERVER['argv'] = ['artisan', 'migrate'];
|
||||
|
||||
try {
|
||||
$callback();
|
||||
} finally {
|
||||
$_SERVER['argv'] = $this->originalArgv;
|
||||
$this->originalArgv = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 콜백 실행 중 발생한 information_schema.tables 쿼리만 수집합니다.
|
||||
*
|
||||
@@ -187,4 +221,90 @@ class InstallerCompletedGuardTest extends TestCase
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), 'installer_completed=false 에서는 기존 hasTable 경로가 실행되어야 합니다');
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 마이그레이션 컨텍스트 가드 (빈 DB 배포 안전성)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| INSTALLER_COMPLETED=true .env 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate`
|
||||
| 부팅 시, fast-path 가 테이블 존재를 잘못 전제해 뒤따르는 쿼리가 table not found 로
|
||||
| 부팅을 깨뜨리던 결함을 막는다. 마이그레이션 계열 명령 컨텍스트에서는
|
||||
| installer_completed=true 라도 fast-path 를 우회해 hasTable 실검증 경로로 진입해야 한다.
|
||||
| (RefreshDatabase 는 테이블을 만들므로 실제 not-found 는 재현 불가 → 가드 발동으로
|
||||
| hasTable 쿼리가 다시 발생하는지를 쿼리카운트로 검증)
|
||||
*/
|
||||
|
||||
public function test_module_trait_uses_has_table_during_migration_despite_installer_completed(): void
|
||||
{
|
||||
config(['app.installer_completed' => true]);
|
||||
|
||||
$this->withMigrationArgv(function () {
|
||||
$schemaQueries = $this->captureSchemaTableQueries(
|
||||
fn () => ModuleManager::getActiveModuleIdentifiers()
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), '마이그레이션 중에는 installer_completed=true 라도 hasTable 폴백 경로로 진입해야 합니다');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_plugin_trait_uses_has_table_during_migration_despite_installer_completed(): void
|
||||
{
|
||||
config(['app.installer_completed' => true]);
|
||||
|
||||
$this->withMigrationArgv(function () {
|
||||
$schemaQueries = $this->captureSchemaTableQueries(
|
||||
fn () => PluginManager::getActivePluginIdentifiers()
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), '마이그레이션 중에는 installer_completed=true 라도 hasTable 폴백 경로로 진입해야 합니다');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_template_trait_uses_has_table_during_migration_despite_installer_completed(): void
|
||||
{
|
||||
config(['app.installer_completed' => true]);
|
||||
|
||||
$this->withMigrationArgv(function () {
|
||||
$schemaQueries = $this->captureSchemaTableQueries(
|
||||
fn () => TemplateManager::getActiveTemplateIdentifiers()
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), '마이그레이션 중에는 installer_completed=true 라도 hasTable 폴백 경로로 진입해야 합니다');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_module_route_provider_uses_has_table_during_migration_despite_installer_completed(): void
|
||||
{
|
||||
config(['app.installer_completed' => true]);
|
||||
|
||||
$provider = new ModuleRouteServiceProvider($this->app);
|
||||
$method = (new ReflectionClass($provider))->getMethod('loadModuleRoutes');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$this->withMigrationArgv(function () use ($provider, $method) {
|
||||
$schemaQueries = $this->captureSchemaTableQueries(
|
||||
fn () => $method->invoke($provider)
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), '마이그레이션 중에는 installer_completed=true 라도 modules hasTable 검증 경로로 진입해 무방비 pluck 을 막아야 합니다');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_plugin_route_provider_uses_has_table_during_migration_despite_installer_completed(): void
|
||||
{
|
||||
config(['app.installer_completed' => true]);
|
||||
|
||||
$provider = new PluginRouteServiceProvider($this->app);
|
||||
$method = (new ReflectionClass($provider))->getMethod('loadPluginRoutes');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$this->withMigrationArgv(function () use ($provider, $method) {
|
||||
$schemaQueries = $this->captureSchemaTableQueries(
|
||||
fn () => $method->invoke($provider)
|
||||
);
|
||||
|
||||
$this->assertGreaterThan(0, count($schemaQueries), '마이그레이션 중에는 installer_completed=true 라도 plugins hasTable 검증 경로로 진입해야 합니다');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Support;
|
||||
|
||||
use App\Support\InstallerContext;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* InstallerContext 회귀 테스트.
|
||||
*
|
||||
* `INSTALLER_COMPLETED=true` .env 를 빈 DB 새 서버에 복사한 뒤 `php artisan migrate`
|
||||
* 부팅 시, installer_completed fast-path 가 테이블 존재를 잘못 전제해 부팅을 깨뜨리던
|
||||
* 결함을 막기 위한 마이그레이션 계열 명령 판정 헬퍼를 검증한다.
|
||||
*
|
||||
* PHPUnit 실행 자체가 콘솔 컨텍스트(runningInConsole()===true)이므로,
|
||||
* $_SERVER['argv'][1] 을 조작해 마이그레이션 명령 여부만 바꿔 판정을 검증한다.
|
||||
* argv 는 finally 에서 반드시 원복해 다른 테스트 오염을 막는다.
|
||||
*/
|
||||
class InstallerContextTest extends TestCase
|
||||
{
|
||||
/** @var array<int, string>|null 원본 argv 백업 */
|
||||
private ?array $originalArgv = null;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
if ($this->originalArgv !== null) {
|
||||
$_SERVER['argv'] = $this->originalArgv;
|
||||
$this->originalArgv = null;
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 주어진 argv[1] 로 $_SERVER['argv'] 를 임시 설정하고 콜백을 실행합니다.
|
||||
*
|
||||
* @param string $command artisan 명령명 (argv[1] 위치)
|
||||
* @param callable $callback argv 설정 상태에서 실행할 코드
|
||||
*/
|
||||
private function withArgv(string $command, callable $callback): void
|
||||
{
|
||||
if ($this->originalArgv === null) {
|
||||
$this->originalArgv = $_SERVER['argv'] ?? [];
|
||||
}
|
||||
|
||||
$_SERVER['argv'] = ['artisan', $command];
|
||||
|
||||
try {
|
||||
$callback();
|
||||
} finally {
|
||||
$_SERVER['argv'] = $this->originalArgv;
|
||||
$this->originalArgv = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function test_returns_true_for_migrate(): void
|
||||
{
|
||||
$this->withArgv('migrate', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_true_for_migrate_fresh(): void
|
||||
{
|
||||
$this->withArgv('migrate:fresh', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_true_for_migrate_refresh(): void
|
||||
{
|
||||
// migrate:refresh 는 참조 구현(LanguagePackServiceProvider)에 누락돼 있던 명령.
|
||||
// 스키마를 drop/recreate 하므로 동일한 빈 DB 구간을 만든다 → 반드시 포함.
|
||||
$this->withArgv('migrate:refresh', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_true_for_migrate_rollback(): void
|
||||
{
|
||||
$this->withArgv('migrate:rollback', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_true_for_migrate_reset(): void
|
||||
{
|
||||
$this->withArgv('migrate:reset', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_true_for_db_wipe(): void
|
||||
{
|
||||
$this->withArgv('db:wipe', function () {
|
||||
$this->assertTrue(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_false_for_non_migration_command(): void
|
||||
{
|
||||
$this->withArgv('serve', function () {
|
||||
$this->assertFalse(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
|
||||
public function test_returns_false_for_route_list(): void
|
||||
{
|
||||
$this->withArgv('route:list', function () {
|
||||
$this->assertFalse(InstallerContext::isSchemaMutatingCommand());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user