'storage/framework/testing/packages.php', 'APP_SERVICES_CACHE' => 'storage/framework/testing/services.php', ] as $manifestEnvKey => $manifestRelativePath) { putenv($manifestEnvKey.'='.$manifestRelativePath); $_ENV[$manifestEnvKey] = $manifestRelativePath; $_SERVER[$manifestEnvKey] = $manifestRelativePath; } unset($testingManifestDir, $manifestEnvKey, $manifestRelativePath); // Composer 오토로더 로드 $loader = require __DIR__.'/../vendor/autoload.php'; // Extension 오토로드 중복 등록 방지 define('G7_EXTENSION_AUTOLOAD_REGISTERED', true); /* |-------------------------------------------------------------------------- | _bundled 확장 우선 오토로드 (테스트 환경 전용) |-------------------------------------------------------------------------- | | _bundled 디렉토리의 확장을 활성 디렉토리보다 먼저 등록(prepend)합니다. | 이를 통해 _bundled에서 직접 테스트를 실행할 수 있습니다. | | 실행 순서: | 1. _bundled classmap (module.php/plugin.php) require_once → 선점 | 2. _bundled PSR-4 addPsr4(prepend=true) → 우선 검색 | 3. autoload-extensions.php → 활성 디렉토리는 후순위 | */ // _bundled 확장 식별자 목록 (classmap 중복 로드 방지용) $bundledExtensionIdentifiers = []; // _bundled 모듈 오토로드 (prepend) $bundledModulesDir = __DIR__.'/../modules/_bundled'; if (is_dir($bundledModulesDir)) { $bundledModules = array_filter(scandir($bundledModulesDir), function ($dir) use ($bundledModulesDir) { return $dir !== '.' && $dir !== '..' && is_dir($bundledModulesDir.'/'.$dir); }); foreach ($bundledModules as $moduleDir) { $modulePath = $bundledModulesDir.'/'.$moduleDir; // _bundled 식별자 기록 (classmap 중복 방지용) $bundledExtensionIdentifiers[] = $moduleDir; // Classmap 선점 (module.php) // class_exists 가드: artisan 초기 부팅에서 활성 디렉토리 버전이 로드되었을 수 있음 $modulePhp = $modulePath.'/module.php'; if (file_exists($modulePhp)) { $parts = explode('-', $moduleDir); $vendor = ucfirst($parts[0]); $modName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; $moduleClass = "Modules\\{$vendor}\\{$modName}\\Module"; if (! class_exists($moduleClass, false)) { require_once $modulePhp; } } // composer.json에서 오토로드 정보 읽기 $composerJson = $modulePath.'/composer.json'; if (file_exists($composerJson)) { $composerData = json_decode(file_get_contents($composerJson), true); // PSR-4 네임스페이스 prepend 등록 if (! empty($composerData['autoload']['psr-4'])) { foreach ($composerData['autoload']['psr-4'] as $namespace => $paths) { $paths = (array) $paths; foreach ($paths as $path) { $absolutePath = $modulePath.'/'.$path; if (is_dir($absolutePath)) { $loader->addPsr4($namespace, $absolutePath, true); } } } } // files 오토로드 (helpers.php 등) if (! empty($composerData['autoload']['files'])) { foreach ($composerData['autoload']['files'] as $file) { $absoluteFile = $modulePath.'/'.$file; if (file_exists($absoluteFile)) { require_once $absoluteFile; } } } } } } // _bundled 플러그인 오토로드 (prepend) $bundledPluginsDir = __DIR__.'/../plugins/_bundled'; if (is_dir($bundledPluginsDir)) { $bundledPlugins = array_filter(scandir($bundledPluginsDir), function ($dir) use ($bundledPluginsDir) { return $dir !== '.' && $dir !== '..' && is_dir($bundledPluginsDir.'/'.$dir); }); foreach ($bundledPlugins as $pluginDir) { $pluginPath = $bundledPluginsDir.'/'.$pluginDir; // _bundled 식별자 기록 (classmap 중복 방지용) $bundledExtensionIdentifiers[] = $pluginDir; // Classmap 선점 (plugin.php) // class_exists 가드: artisan 초기 부팅에서 활성 디렉토리 버전이 로드되었을 수 있음 $pluginPhp = $pluginPath.'/plugin.php'; if (file_exists($pluginPhp)) { $parts = explode('-', $pluginDir); $vendor = ucfirst($parts[0]); $plgName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; $pluginClass = "Plugins\\{$vendor}\\{$plgName}\\Plugin"; if (! class_exists($pluginClass, false)) { require_once $pluginPhp; } } // composer.json에서 오토로드 정보 읽기 $composerJson = $pluginPath.'/composer.json'; if (file_exists($composerJson)) { $composerData = json_decode(file_get_contents($composerJson), true); // PSR-4 네임스페이스 prepend 등록 if (! empty($composerData['autoload']['psr-4'])) { foreach ($composerData['autoload']['psr-4'] as $namespace => $paths) { $paths = (array) $paths; foreach ($paths as $path) { $absolutePath = $pluginPath.'/'.$path; if (is_dir($absolutePath)) { $loader->addPsr4($namespace, $absolutePath, true); } } } } // files 오토로드 (helpers.php 등) if (! empty($composerData['autoload']['files'])) { foreach ($composerData['autoload']['files'] as $file) { $absoluteFile = $pluginPath.'/'.$file; if (file_exists($absoluteFile)) { require_once $absoluteFile; } } } } } } // 확장(모듈/플러그인) 오토로드 등록 $extensionAutoloadFile = __DIR__.'/../bootstrap/cache/autoload-extensions.php'; if (file_exists($extensionAutoloadFile)) { $extensionAutoloads = require $extensionAutoloadFile; // PSR-4 네임스페이스 등록 // _bundled에서 이미 prepend로 등록된 확장의 활성 디렉토리 PSR-4 경로는 스킵 // (Windows에서 대소문자 미구분으로 plugin.php = Plugin.php가 되어 중복 로드 발생 방지) if (! empty($extensionAutoloads['psr4'])) { foreach ($extensionAutoloads['psr4'] as $namespace => $paths) { $paths = (array) $paths; foreach ($paths as $path) { // _bundled 식별자와 일치하는 경로는 스킵 $skip = false; foreach ($bundledExtensionIdentifiers as $identifier) { if (str_contains($path, '/'.$identifier.'/')) { $skip = true; break; } } if ($skip) { continue; } $absolutePath = __DIR__.'/../'.$path; if (is_dir($absolutePath)) { $loader->addPsr4($namespace, $absolutePath); } } } } // Classmap 파일 로드 (module.php, plugin.php) // _bundled에서 이미 로드된 확장의 classmap은 스킵 (클래스 중복 선언 방지) if (! empty($extensionAutoloads['classmap'])) { foreach ($extensionAutoloads['classmap'] as $file) { // _bundled 식별자와 일치하는 경로인지 확인 // 예: "modules/sirsoft-board/module.php" → 식별자 "sirsoft-board" $skip = false; foreach ($bundledExtensionIdentifiers as $identifier) { if (str_contains($file, '/'.$identifier.'/')) { $skip = true; break; } } if ($skip) { continue; } $absolutePath = __DIR__.'/../'.$file; if (file_exists($absolutePath)) { require_once $absolutePath; } } } } /* |-------------------------------------------------------------------------- | 확장 vendor(제3자 composer 패키지) 오토로드 등록 |-------------------------------------------------------------------------- | | `autoload-extensions.php` 의 `vendor_autoloads` 를 소비하는 진입점은 `public/index.php` | 와 `artisan` 뿐이라, PHPUnit 프로세스에서는 확장 전용 composer 패키지(`\HTMLPurifier` 등)가 | 오토로드되지 않았다. 그 패키지를 쓰는 코드 경로가 통째로 테스트 불가였고, 그 결손은 아무도 | 그 경로를 테스트하지 않는 동안 드러나지 않는다 (공개 #125). | | 확장 vendor 의 `autoload.php` 를 그대로 require 하지 않고 생성된 맵만 읽어 자체 로더를 만든다. | 그 오토로더를 그대로 쓰면 두 가지가 깨진다. | | 1. 확장 자신의 PSR-4 와 files 를 **활성 디렉토리**로 매핑하고 자신을 prepend 로 걸어, 위에서 | _bundled 를 prepend 한 등록을 이긴다 — 테스트가 `_bundled` 가 아니라 활성 디렉토리 사본을 | 검증하게 되어 "_bundled 에서만 작업한다" 는 규율이 조용히 깨진다. | 2. Composer 로더는 `vendorDir` 를 갖고 자신을 `ClassLoader::getRegisteredLoaders()` 맨 앞에 | 넣는데, `Illuminate\Foundation\Testing\TestCase::createApplication()` 이 | `Application::inferBasePath()` 로 그 첫 항목에서 base path 를 유추한다. 이후 테스트의 앱 | 부팅이 `modules/{id}/bootstrap/app.php` 를 찾다 실패한다. (운영 진입점은 basePath 를 명시 | 전달하므로 영향이 없다.) | | 그래서 `Modules\` / `Plugins\` 로 시작하는 항목은 전부 제외하고 — 확장 자기 코드는 위에서 | 이미 등록했다 — `vendorDir` 없는 단일 로더로 제3자 패키지만 등록한다. 루트 오토로더가 먼저 | 등록되어 있으므로 append 로 걸어 루트 vendor 가 우선하게 둔다(같은 패키지 중복 선언 방지). | */ $extensionVendorLoader = new ClassLoader; $extensionVendorClassMap = []; $extensionVendorFiles = []; $isExtensionOwnSymbol = static fn (string $symbol): bool => str_starts_with($symbol, 'Modules\\') || str_starts_with($symbol, 'Plugins\\'); foreach (['modules', 'plugins'] as $extensionType) { foreach ([__DIR__.'/../'.$extensionType, __DIR__.'/../'.$extensionType.'/_bundled'] as $searchDir) { if (! is_dir($searchDir)) { continue; } foreach (scandir($searchDir) ?: [] as $entry) { if ($entry === '.' || $entry === '..' || $entry === '_bundled' || $entry === '_pending') { continue; } $vendorDir = $searchDir.'/'.$entry.'/vendor'; $classmapFile = $vendorDir.'/composer/autoload_classmap.php'; if (! is_dir($vendorDir) || ! file_exists($classmapFile)) { continue; } // 먼저 발견한 것(활성 디렉토리)이 이긴다 — 같은 클래스를 뒤에서 덮어쓰지 않는다. foreach (require $classmapFile as $fqcn => $path) { if (! $isExtensionOwnSymbol($fqcn) && ! isset($extensionVendorClassMap[$fqcn])) { $extensionVendorClassMap[$fqcn] = $path; } } $psr4File = $vendorDir.'/composer/autoload_psr4.php'; if (file_exists($psr4File)) { foreach (require $psr4File as $prefix => $paths) { if (! $isExtensionOwnSymbol($prefix)) { $extensionVendorLoader->addPsr4($prefix, $paths); } } } // 패키지가 요구하는 부트스트랩 파일(HTMLPurifier 의 HTMLPURIFIER_PREFIX 정의 등). // vendor 디렉토리 바깥의 항목(확장 자신의 helpers)은 제외한다. $filesFile = $vendorDir.'/composer/autoload_files.php'; if (file_exists($filesFile)) { $vendorReal = realpath($vendorDir) ?: $vendorDir; foreach (require $filesFile as $file) { if (str_starts_with(realpath($file) ?: $file, $vendorReal)) { $extensionVendorFiles[] = $file; } } } } } } if ($extensionVendorClassMap !== []) { $extensionVendorLoader->addClassMap($extensionVendorClassMap); } // vendorDir 를 주지 않았으므로 register() 가 registeredLoaders 를 건드리지 않는다. $extensionVendorLoader->register(); foreach ($extensionVendorFiles as $extensionVendorFile) { require_once $extensionVendorFile; } // 모듈/플러그인 테스트 네임스페이스 등록 (tests/ 디렉토리) $modulesDir = __DIR__.'/../modules'; if (is_dir($modulesDir)) { $modules = array_filter(scandir($modulesDir), function ($dir) use ($modulesDir) { return $dir !== '.' && $dir !== '..' && is_dir($modulesDir.'/'.$dir.'/tests'); }); foreach ($modules as $moduleDir) { // sirsoft-ecommerce -> Modules\Sirsoft\Ecommerce\Tests $parts = explode('-', $moduleDir); $vendor = ucfirst($parts[0]); $moduleName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; $namespace = "Modules\\{$vendor}\\{$moduleName}\\Tests\\"; $testsPath = $modulesDir.'/'.$moduleDir.'/tests/'; if (is_dir($testsPath)) { $loader->addPsr4($namespace, $testsPath); } } } // _bundled 모듈 테스트 네임스페이스 등록 (prepend) $bundledModulesDir = __DIR__.'/../modules/_bundled'; if (is_dir($bundledModulesDir)) { $bundledModules = array_filter(scandir($bundledModulesDir), function ($dir) use ($bundledModulesDir) { return $dir !== '.' && $dir !== '..' && is_dir($bundledModulesDir.'/'.$dir.'/tests'); }); foreach ($bundledModules as $moduleDir) { $parts = explode('-', $moduleDir); $vendor = ucfirst($parts[0]); $moduleName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; $namespace = "Modules\\{$vendor}\\{$moduleName}\\Tests\\"; $testsPath = $bundledModulesDir.'/'.$moduleDir.'/tests/'; if (is_dir($testsPath)) { $loader->addPsr4($namespace, $testsPath, true); } } } // 플러그인 소스 및 테스트 네임스페이스 등록 // (미설치 플러그인도 테스트할 수 있도록 autoload-extensions.php 보완) $pluginsDir = __DIR__.'/../plugins'; if (is_dir($pluginsDir)) { $plugins = array_filter(scandir($pluginsDir), function ($dir) use ($pluginsDir) { return $dir !== '.' && $dir !== '..' && is_dir($pluginsDir.'/'.$dir); }); foreach ($plugins as $pluginDir) { // sirsoft-tosspayments -> Plugins\Sirsoft\Tosspayments $parts = explode('-', $pluginDir); $vendor = ucfirst($parts[0]); $pluginName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; // 소스 네임스페이스 등록 (autoload-extensions.php에 없을 수 있음) $srcPath = $pluginsDir.'/'.$pluginDir.'/src/'; if (is_dir($srcPath)) { $srcNamespace = "Plugins\\{$vendor}\\{$pluginName}\\"; $loader->addPsr4($srcNamespace, $srcPath); } // 테스트 네임스페이스 등록 $testsPath = $pluginsDir.'/'.$pluginDir.'/tests/'; if (is_dir($testsPath)) { $testNamespace = "Plugins\\{$vendor}\\{$pluginName}\\Tests\\"; $loader->addPsr4($testNamespace, $testsPath); } } } // _bundled 플러그인 테스트 네임스페이스 등록 (prepend) $bundledPluginsDir = __DIR__.'/../plugins/_bundled'; if (is_dir($bundledPluginsDir)) { $bundledPlugins = array_filter(scandir($bundledPluginsDir), function ($dir) use ($bundledPluginsDir) { return $dir !== '.' && $dir !== '..' && is_dir($bundledPluginsDir.'/'.$dir.'/tests'); }); foreach ($bundledPlugins as $pluginDir) { $parts = explode('-', $pluginDir); $vendor = ucfirst($parts[0]); $pluginName = isset($parts[1]) ? str_replace('_', '', ucwords($parts[1], '_')) : ''; $testsPath = $bundledPluginsDir.'/'.$pluginDir.'/tests/'; if (is_dir($testsPath)) { $testNamespace = "Plugins\\{$vendor}\\{$pluginName}\\Tests\\"; $loader->addPsr4($testNamespace, $testsPath, true); } } }