makePartial() → File::swap() 순서 * - finally 블록에서 반드시 File::swap($original)로 원래 인스턴스 복원 * * 차단 대상 경로 패턴: /(modules|plugins|templates)/[활성확장] * - modules/sirsoft-board → 차단 ✅ * - plugins/sirsoft-daum_postcode → 차단 ✅ * - templates/sirsoft-admin_basic → 차단 ✅ * - modules/_bundled/sirsoft-board → 통과 (_ 접두사) * - modules/_pending/xxx → 통과 (_ 접두사) * - /tmp/any-other-path → 통과 * * 사용법: * ```php * use Tests\Helpers\ProtectsExtensionDirectories; * * class MyTest extends TestCase * { * use ProtectsExtensionDirectories; * * protected function setUp(): void * { * parent::setUp(); * // ... 정당한 파일 작업 (좀비 정리, 복사 등) * $this->setUpExtensionProtection(); * // ... Manager 로드, DB 시딩 등 * } * * protected function tearDown(): void * { * $this->tearDownExtensionProtection(); * // ... 정리 작업 * parent::tearDown(); * } * } * ``` */ trait ProtectsExtensionDirectories { private Filesystem $originalFilesystem; /** * File 파사드 spy를 설정하여 확장 활성 디렉토리 삭제를 차단합니다. * * 차단 대상: modules/xxx, plugins/xxx, templates/xxx * (xxx가 _ 또는 . 으로 시작하지 않는 경우) * 통과 대상: modules/_bundled/xxx, modules/_pending/xxx 등 */ protected function setUpExtensionProtection(): void { $this->originalFilesystem = File::getFacadeRoot(); $mock = Mockery::mock($this->originalFilesystem)->makePartial(); // 확장 활성 디렉토리 삭제 차단 $mock->shouldReceive('deleteDirectory') ->zeroOrMoreTimes() ->andReturnUsing(function (string $directory, bool $preserve = false) { $normalized = str_replace('\\', '/', $directory); if (preg_match('#/(modules|plugins|templates)/(?![\._])[^/]+$#', $normalized)) { return true; } return $this->originalFilesystem->deleteDirectory($directory, $preserve); }); // 확장 활성 디렉토리 이동(rename) 차단 // copyToActive()의 원자적 교체가 활성 디렉토리를 _old_xxx로 rename하는 것을 방지 $mock->shouldReceive('moveDirectory') ->zeroOrMoreTimes() ->andReturnUsing(function (string $from, string $to, bool $overwrite = false) { $normalized = str_replace('\\', '/', $from); // 활성 디렉토리에서 밖으로 이동하는 것 차단 (예: templates/sirsoft-xxx → _pending/sirsoft-xxx_old_xxx) if (preg_match('#/(modules|plugins|templates)/(?![\._])[^/]+$#', $normalized)) { return true; } return $this->originalFilesystem->moveDirectory($from, $to, $overwrite); }); File::swap($mock); } /** * File 파사드를 원래 인스턴스로 복원합니다. */ protected function tearDownExtensionProtection(): void { File::swap($this->originalFilesystem); } }