findPatternMatchesInPhpFiles('pages', '/\bnew\s+[\\\\A-Za-z0-9_]+(?:Service|Gateway|Repository)\s*\(/'); $this->assertSame([], $violations, "Direct service/repository/gateway instantiation found in pages/:\n" . implode("\n", $violations)); } public function testPagesDoNotUseDatabaseFacadeDirectly(): void { $violations = $this->findPatternMatchesInPhpFiles('pages', '/\bDB::/'); $this->assertSame([], $violations, "Direct DB facade usage found in pages/:\n" . implode("\n", $violations)); } public function testAdminPagesDoNotUseFactoryCreateChains(): void { $violations = $this->findPatternMatchesInPhpFiles('pages/admin', '/app\([^\n]*Factory::class\)->create/'); $this->assertSame([], $violations, "Factory create-chain usage found in pages/admin/:\n" . implode("\n", $violations)); } public function testAdminPagesDoNotReferenceFactoryClasses(): void { $violations = $this->findPatternMatchesInPhpFiles('pages/admin', '/Factory::class/'); $this->assertSame([], $violations, "Factory class usage found in pages/admin/:\n" . implode("\n", $violations)); } public function testPagesUseRequestInputInsteadOfSuperglobals(): void { $patterns = [ '/\$_POST/' => '$_POST', '/\$_GET/' => '$_GET', '/\$_FILES/' => '$_FILES', '/REQUEST_METHOD/' => 'REQUEST_METHOD', ]; foreach ($patterns as $pattern => $label) { $violations = $this->findPatternMatchesInPhpFiles('pages', $pattern); $this->assertSame([], $violations, "Superglobal {$label} usage found in pages/:\n" . implode("\n", $violations)); } } public function testApiPagesDoNotReadJsonBodyDirectly(): void { $violations = $this->findPatternMatchesInPhpFiles('pages/api/v1', '/ApiResponse::readJsonBody\s*\(/'); $this->assertSame([], $violations, "ApiResponse::readJsonBody usage found in pages/api/v1/:\n" . implode("\n", $violations)); } public function testLegacyAndTemplateModulesAreRemoved(): void { $root = $this->projectRootPath(); $this->assertFileDoesNotExist( $root . '/web/js/components/app-filter-overflow.js', 'Legacy filter overflow module must remain removed.' ); $this->assertFileDoesNotExist( $root . '/web/js/components/app-toggle-toolbar.js', 'Legacy toolbar toggle module must remain removed.' ); $this->assertFileDoesNotExist( $root . '/web/js/components/_template.js', 'Component template module must remain removed from production JS path.' ); } public function testTemplatesDoNotUseLegacyFilterToggleMarkup(): void { $pattern = '/data-(?:toolbar-toggle|filter-overflow|filter-toggle)/'; $violations = array_merge( $this->findPatternMatchesInFiles('templates', $pattern, ['phtml']), $this->findPatternMatchesInFiles('pages', $pattern, ['phtml']) ); sort($violations); $this->assertSame([], $violations, "Legacy filter toggle markup found in templates/pages:\n" . implode("\n", $violations)); } public function testPagesAndTemplatesDoNotUseInlineModuleScripts(): void { $pattern = '/]*\btype=["\']module["\'])(?![^>]*\bsrc=)[^>]*>/i'; $violations = array_merge( $this->findPatternMatchesInFiles('templates', $pattern, ['phtml']), $this->findPatternMatchesInFiles('pages', $pattern, ['phtml']) ); sort($violations); $this->assertSame([], $violations, "Inline module scripts found in templates/pages:\n" . implode("\n", $violations)); } public function testPagesAndTemplatesUseAssetVersionForJsUrls(): void { $pattern = '/asset\(\s*[\'"][^\'"]+\.js/i'; $violations = array_merge( $this->findPatternMatchesInFiles('templates', $pattern, ['phtml']), $this->findPatternMatchesInFiles('pages', $pattern, ['phtml']) ); sort($violations); $this->assertSame([], $violations, "asset(...) JS URLs found in templates/pages:\n" . implode("\n", $violations)); } /** * @return list */ private function findPatternMatchesInPhpFiles(string $relativeDirectory, string $pattern): array { return $this->findPatternMatchesInFiles($relativeDirectory, $pattern, ['php']); } /** * @param list $extensions * @return list */ private function findPatternMatchesInFiles(string $relativeDirectory, string $pattern, array $extensions): array { $root = $this->projectRootPath(); $basePath = $root . '/' . $relativeDirectory; $this->assertDirectoryExists($basePath, 'Directory not found: ' . $relativeDirectory); $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath)); $violations = []; /** @var \SplFileInfo $file */ foreach ($iterator as $file) { if (!$file->isFile() || !in_array($file->getExtension(), $extensions, true)) { continue; } $content = file_get_contents($file->getPathname()); $this->assertNotFalse($content, 'Could not read file: ' . $file->getPathname()); if (!preg_match($pattern, $content)) { continue; } $relativePath = str_replace($root . '/', '', $file->getPathname()); $violations[] = $relativePath; } sort($violations); return $violations; } }