Files
Gnuboard7/app/Console/Commands/Core/ExecuteBundledUpdatesCommand.php
T
2026-05-11 11:29:41 +09:00

178 lines
7.4 KiB
PHP

<?php
namespace App\Console\Commands\Core;
use App\Extension\ModuleManager;
use App\Extension\PluginManager;
use App\Extension\TemplateManager;
use App\Extension\Vendor\VendorMode;
use App\Services\LanguagePackService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
/**
* 번들 확장(모듈/플러그인/템플릿/언어팩) 일괄 업데이트를 별도 PHP 프로세스에서 실행합니다.
*
* `core:update` 의 `BundledExtensionUpdatePrompt::executeBulkUpdate` 가 사용자 선택을
* 매니페스트로 직렬화한 후 본 커맨드를 `proc_open` 으로 spawn 한다. 자식은 fresh PHP
* 프로세스이므로 디스크에 막 적용된 신버전 코어 코드(특히 `ModuleManager` /
* `PluginManager` / `TemplateManager` / `LanguagePackService`) 를 메모리에 로드한 상태
* 에서 update 메서드를 호출한다.
*
* 부모 프로세스의 stale memory 가 신규 sync 메서드(`syncDeclarativeArtifacts` 등) 를
* 호출하지 못하던 결함의 영구 차단 (beta.4 의 `Upgrade_7_0_0_beta_4` 사후 보정과는 별개의
* 구조적 fix — 향후 모든 코어 업그레이드에서 동일 패턴 회귀를 차단).
*
* 매니페스트 형식:
* ```json
* {
* "modules": [{"identifier": "...", "strategy": "overwrite|keep"}, ...],
* "plugins": [{"identifier": "...", "strategy": "overwrite|keep"}, ...],
* "templates": [{"identifier": "...", "strategy": "overwrite|keep"}, ...],
* "lang_packs": [{"identifier": "..."}, ...]
* }
* ```
*
* 출력:
* - stdout 에 진행 라인을 그대로 출력 (부모 콘솔이 forwarding)
* - 종료 직전 `[BUNDLED-RESULT] {json}` 표식으로 최종 카운트 페이로드를 1회 출력 →
* 부모가 stdout 파싱해 결과 복원.
*/
class ExecuteBundledUpdatesCommand extends Command
{
public const RESULT_PREFIX = '[BUNDLED-RESULT] ';
protected $signature = 'core:execute-bundled-updates
{--manifest= : 업데이트 매니페스트 JSON 파일 경로 (필수)}
{--force : 강제 업데이트 플래그 (확장 매니저 update 호출에 전달)}';
protected $description = '번들 확장 일괄 업데이트를 실행합니다 (CoreUpdateCommand 내부용 — fresh PHP 프로세스에서 호출)';
public function handle(): int
{
$manifestPath = (string) $this->option('manifest');
if ($manifestPath === '' || ! File::isFile($manifestPath)) {
$this->error('--manifest 옵션이 필수이며 존재하는 파일이어야 합니다.');
return self::INVALID;
}
// spawn 자식 진입 시 활성 모듈/플러그인의 PSR-4 매핑을 fresh 등록 — 자세한 배경은
// ExecuteUpgradeStepsCommand::handle 의 동일 호출 주석 참조.
// (Artisan::call 대신 직접 메서드 호출 — nested Artisan::call 이 outer 명령의
// output buffer 를 덮어쓰는 Laravel 동작 회피)
try {
app(\App\Extension\ExtensionManager::class)->updateComposerAutoload();
} catch (\Throwable $e) {
Log::warning('bundled update spawn 자식: updateComposerAutoload 호출 실패', [
'error' => $e->getMessage(),
]);
}
$manifest = json_decode(File::get($manifestPath), true);
if (! is_array($manifest)) {
$this->error('매니페스트 JSON 파싱 실패: '.$manifestPath);
return self::FAILURE;
}
$modules = $manifest['modules'] ?? [];
$plugins = $manifest['plugins'] ?? [];
$templates = $manifest['templates'] ?? [];
$langPacks = $manifest['lang_packs'] ?? [];
$moduleManager = app(ModuleManager::class);
$pluginManager = app(PluginManager::class);
$templateManager = app(TemplateManager::class);
$langPackService = app(LanguagePackService::class);
$success = 0;
$failed = 0;
$this->info('── 번들 일괄 업데이트 실행 (spawn child) ──');
foreach ($modules as $entry) {
$id = (string) ($entry['identifier'] ?? '');
$strategy = (string) ($entry['strategy'] ?? 'overwrite');
if ($id === '') {
continue;
}
$this->line("→ [모듈] {$id} ({$strategy})");
try {
$moduleManager->updateModule($id, true, null, VendorMode::Auto, $strategy, null, 'bundled');
$success++;
} catch (\Throwable $e) {
$failed++;
$this->warn(" 실패: {$e->getMessage()}");
Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'module', 'id' => $id, 'error' => $e->getMessage()]);
}
}
foreach ($plugins as $entry) {
$id = (string) ($entry['identifier'] ?? '');
$strategy = (string) ($entry['strategy'] ?? 'overwrite');
if ($id === '') {
continue;
}
$this->line("→ [플러그인] {$id} ({$strategy})");
try {
$pluginManager->updatePlugin($id, true, null, VendorMode::Auto, $strategy, null, 'bundled');
$success++;
} catch (\Throwable $e) {
$failed++;
$this->warn(" 실패: {$e->getMessage()}");
Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'plugin', 'id' => $id, 'error' => $e->getMessage()]);
}
}
foreach ($templates as $entry) {
$id = (string) ($entry['identifier'] ?? '');
$strategy = (string) ($entry['strategy'] ?? 'overwrite');
if ($id === '') {
continue;
}
$this->line("→ [템플릿] {$id} ({$strategy})");
try {
$templateManager->updateTemplate($id, true, null, $strategy, 'bundled');
$success++;
} catch (\Throwable $e) {
$failed++;
$this->warn(" 실패: {$e->getMessage()}");
Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'template', 'id' => $id, 'error' => $e->getMessage()]);
}
}
foreach ($langPacks as $entry) {
$id = (string) ($entry['identifier'] ?? '');
if ($id === '') {
continue;
}
$this->line("→ [언어팩] {$id}");
try {
$pack = $langPackService->findByIdentifier($id);
if (! $pack) {
throw new \RuntimeException(__('language_packs.errors.identifier_not_found', ['identifier' => $id]));
}
$langPackService->performUpdate($pack, true);
$success++;
} catch (\Throwable $e) {
$failed++;
$this->warn(" 실패: {$e->getMessage()}");
Log::error('번들 일괄 업데이트 실패 (spawn)', ['type' => 'lang_pack', 'id' => $id, 'error' => $e->getMessage()]);
}
}
$this->newLine();
$this->info("업데이트 완료: 성공 {$success}, 실패 {$failed}");
// 부모 프로세스가 결과를 복원할 수 있도록 표식 라인으로 페이로드 출력
$this->line(self::RESULT_PREFIX.json_encode([
'success' => $success,
'failed' => $failed,
], JSON_UNESCAPED_UNICODE));
return $failed > 0 ? self::FAILURE : self::SUCCESS;
}
}