Files
2026-05-14 11:13:08 +09:00

183 lines
6.4 KiB
PHP

<?php
namespace App\Upgrades\Data\V7_0_0_beta_6\Migrations;
use App\Extension\Upgrade\DataMigration;
use App\Extension\UpgradeContext;
use Illuminate\Support\Facades\Log;
/**
* 부팅 부정합 ServiceProvider 진단 로그 — 자동 삭제 없음.
*
* beta.5 이전의 자동 롤백 결함으로 인해 활성 디렉토리 `app/Providers/` 또는 `app/Services/`
* 등에 신 버전 ServiceProvider 파일이 잔존했을 수 있다. 본 진단은 다음 부정합을 식별한다:
*
* 1. `bootstrap/providers.php` 가 register 하는 클래스 FQCN 배열 추출
* 2. `app/Providers/` 의 모든 *.php 파일 스캔 + 클래스명 변환
* 3. 디스크에 있지만 register 되지 않은 ServiceProvider 후보 목록 산출
* 4. `storage/logs/core_upgrade_beta_6_stale_providers.log` 로 기록 (자동 삭제 없음)
*
* 자동 삭제하지 않는 이유: 운영자가 직접 추가한 커스텀 ServiceProvider 가 같은 패턴으로
* 식별되므로 안전 우선. 운영자 검토 후 수동 정리.
*
* 격리 원칙: 코어의 신 메서드 (CoreBackupHelper, ExtensionManager 등) 호출 금지. 표준
* 파일 시스템 API (scandir / file_get_contents) + `Illuminate\Support\Facades\Log` 만 사용.
*/
final class LogStaleServiceProviders implements DataMigration
{
private const LOG_FILENAME = 'core_upgrade_beta_6_stale_providers.log';
public function name(): string
{
return 'LogStaleServiceProviders';
}
public function run(UpgradeContext $context): void
{
try {
$this->runInternal($context);
} catch (\Throwable $e) {
$context->logger->warning(sprintf(
'[7.0.0-beta.6] LogStaleServiceProviders 실패 (계속 진행): %s',
$e->getMessage(),
));
Log::warning('beta.6 LogStaleServiceProviders 실패', [
'error' => $e->getMessage(),
]);
}
}
private function runInternal(UpgradeContext $context): void
{
$providersFile = base_path('bootstrap'.DIRECTORY_SEPARATOR.'providers.php');
$providersDir = base_path('app'.DIRECTORY_SEPARATOR.'Providers');
if (! file_exists($providersFile)) {
$context->logger->info('[7.0.0-beta.6] bootstrap/providers.php 부재 — stale providers 진단 skip');
return;
}
if (! is_dir($providersDir)) {
$context->logger->info('[7.0.0-beta.6] app/Providers 부재 — stale providers 진단 skip');
return;
}
$registered = $this->loadRegisteredProviders($providersFile);
$registeredSet = [];
foreach ($registered as $fqcn) {
$registeredSet[strtolower($fqcn)] = true;
}
$stale = [];
foreach ($this->collectProviderFiles($providersDir) as $file) {
$relative = ltrim(substr($file, strlen($providersDir)), DIRECTORY_SEPARATOR.'/');
$relative = str_replace('\\', '/', $relative);
// app/Providers/Sub/Foo.php → App\Providers\Sub\Foo
$classRelative = substr($relative, 0, -4); // .php strip
$fqcn = 'App\\Providers\\'.str_replace('/', '\\', $classRelative);
if (isset($registeredSet[strtolower($fqcn)])) {
continue;
}
$stale[] = [
'fqcn' => $fqcn,
'file' => 'app/Providers/'.$relative,
];
}
if ($stale === []) {
$context->logger->info('[7.0.0-beta.6] 부팅 부정합 ServiceProvider 후보 없음');
return;
}
$logPath = storage_path('logs'.DIRECTORY_SEPARATOR.self::LOG_FILENAME);
$header = implode("\n", [
'=== beta.6 부팅 부정합 ServiceProvider 진단 로그 ===',
'날짜: '.date('Y-m-d H:i:s'),
sprintf('업그레이드: %s → %s', $context->fromVersion, $context->toVersion),
'',
'아래 ServiceProvider 클래스 파일이 디스크에는 존재하지만 bootstrap/providers.php',
'의 등록 목록에 없습니다. beta.5 이전의 자동 롤백 결함으로 인해 잔존한 신 버전',
'파일이거나, 운영자가 직접 추가한 커스텀 ServiceProvider 일 수 있습니다.',
'',
'자동 삭제는 수행하지 않습니다. 운영자가 검토 후 수동 정리하세요:',
' - 신 버전 잔존 파일 → 삭제',
' - 커스텀 ServiceProvider → bootstrap/providers.php 에 등록',
'',
'=== 후보 목록 ===',
'',
]);
$body = '';
foreach ($stale as $entry) {
$body .= " - {$entry['fqcn']}\n";
$body .= " 파일: {$entry['file']}\n";
}
@file_put_contents($logPath, $header.$body);
$context->logger->warning(sprintf(
'[7.0.0-beta.6] 부팅 부정합 ServiceProvider %d개 진단 — 운영자 검토 필요. 로그: %s',
count($stale),
$logPath,
));
}
/**
* bootstrap/providers.php 를 require 하여 등록된 ServiceProvider FQCN 배열 반환.
*
* @return array<int, string>
*/
private function loadRegisteredProviders(string $providersFile): array
{
$data = require $providersFile;
if (! is_array($data)) {
return [];
}
return array_values(array_filter(
$data,
fn ($v) => is_string($v) && $v !== '',
));
}
/**
* app/Providers/ 의 모든 *.php 파일을 재귀 수집.
*
* @return array<int, string>
*/
private function collectProviderFiles(string $providersDir): array
{
$files = [];
$stack = [$providersDir];
while ($stack !== []) {
$cur = array_pop($stack);
$entries = @scandir($cur);
if ($entries === false) {
continue;
}
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$full = $cur.DIRECTORY_SEPARATOR.$entry;
if (is_dir($full) && ! is_link($full)) {
$stack[] = $full;
} elseif (is_file($full) && str_ends_with($entry, '.php')) {
$files[] = $full;
}
}
}
sort($files, SORT_STRING);
return $files;
}
}