Files
Gnuboard7/app/Extension/AbstractUpgradeStep.php
T
2026-05-12 17:58:34 +09:00

137 lines
4.9 KiB
PHP

<?php
namespace App\Extension;
use App\Contracts\Extension\UpgradeStepInterface;
use App\Extension\Upgrade\DataMigration;
use App\Extension\Upgrade\DataSnapshot;
use ReflectionClass;
/**
* 버전별 데이터 스냅샷 규약을 따르는 업그레이드 스텝 기본 클래스.
*
* 7.0.0-beta.5 부터 신규 업그레이드 스텝은 본 추상 클래스를 상속해야 한다.
* runUpgradeSteps 가 인스턴스 검증 직후 상속 여부를 확인하고 미상속 시 fatal.
*
* 격리 원칙 ("각 스텝별 동작 100% 동일 보장"):
* - 카탈로그 시드는 data/{version}/*.delta.json 으로 동결
* - Applier 는 data/{version}/appliers/ 안에 버전 namespace 로 동결
* - 변환 / 단발성 핫픽스는 data/{version}/migrations/ 안에 버전 namespace 로 동결
* - 스텝 파일 자체는 본 추상 클래스를 상속한 빈 껍데기 (모든 비즈니스 로직은 data/ 로 위임)
*
* 사용 예:
*
* class Upgrade_7_0_0_beta_5 extends AbstractUpgradeStep
* {
* // 모든 default 사용 — data/7.0.0-beta.5/ 의 manifest/appliers/migrations 가 SSoT
* }
*
* 상세: docs/extension/upgrade-step-guide.md §12
*/
abstract class AbstractUpgradeStep implements UpgradeStepInterface
{
/**
* data/{currentStep}/manifest.json 을 로드 + Applier 동적 로드.
*
* 카탈로그 변동이 없는 스텝은 manifest 파일 없이 자동 빈 스냅샷 반환.
*/
protected function dataSnapshot(UpgradeContext $context): DataSnapshot
{
return DataSnapshot::fromManifest($this->dataDir($context), $context);
}
/**
* data/{currentStep}/migrations/*.php 를 require_once + 버전 namespace 의 DataMigration
* 구현체를 인스턴스화하여 배열로 반환. 디렉토리 부재 시 빈 배열.
*
* 파일명은 정렬용 prefix (예: `01_RecoverActiveExtensionDirs.php`, `02_...php`) 를 허용한다.
* prefix 는 정규식 `^\d{2,}_` 로 클래스명 매핑 시 제거되며, 클래스명 자체는 prefix 없이
* 선언한다 (PHP 식별자 제약). 파일 정렬 결과가 실행 순서 — 명시적 순서가 필요하면 prefix 사용.
*
* @return array<int, DataMigration>
*/
protected function dataMigrations(UpgradeContext $context): array
{
$dir = $this->dataDir($context).'/migrations';
if (! is_dir($dir)) {
return [];
}
$files = glob($dir.'/*.php') ?: [];
sort($files);
$migrations = [];
$namespace = DataSnapshot::versionedNamespace($context, $dir).'\\Migrations';
foreach ($files as $file) {
require_once $file;
$baseName = basename($file, '.php');
$className = preg_replace('/^\d{2,}_/', '', $baseName);
$fqcn = $namespace.'\\'.$className;
if (class_exists($fqcn) && is_subclass_of($fqcn, DataMigration::class)) {
$migrations[] = new $fqcn;
}
}
return $migrations;
}
/**
* 카탈로그/변환 외 프레임워크 레벨 부수 작업 (선택).
*
* 본 PR 의 beta.5 재작성에서는 사용 안 함 — 모든 비즈니스 로직은 data/ 로 격리.
* 미래에 *모든 버전 공통* 인 부수 작업이 필요하면 override.
*
* default: no-op.
*/
protected function postRun(UpgradeContext $context): void {}
/**
* 업그레이드 스텝 실행 — 순서: dataSnapshot → dataMigrations → postRun.
*
* 본 메서드는 final — 하위 클래스가 순서 무력화 못 함.
*/
final public function run(UpgradeContext $context): void
{
$snapshot = $this->dataSnapshot($context);
if ($snapshot->appliers !== []) {
$context->logger->info(sprintf(
'[%s] DataSnapshot 적용 — Applier %d개',
$context->currentStep,
count($snapshot->appliers),
));
}
$snapshot->apply($context);
$migrations = $this->dataMigrations($context);
if ($migrations !== []) {
$context->logger->info(sprintf(
'[%s] DataMigration 실행 — %d개',
$context->currentStep,
count($migrations),
));
}
foreach ($migrations as $migration) {
$context->logger->info(sprintf(
'[%s] migration: %s',
$context->currentStep,
$migration->name(),
));
$migration->run($context);
}
$this->postRun($context);
}
/**
* data/{currentStep}/ 절대 경로 계산. 스텝 파일 위치 기반 — 코어/확장 무관.
*/
protected function dataDir(UpgradeContext $context): string
{
$stepFile = (new ReflectionClass($this))->getFileName();
return dirname($stepFile).'/data/'.$context->currentStep;
}
}