2026-03-04 15:56:58 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Tests\Architecture;
|
|
|
|
|
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
use RecursiveDirectoryIterator;
|
|
|
|
|
use RecursiveIteratorIterator;
|
|
|
|
|
|
|
|
|
|
class CoreStarterkitContractTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use ProjectFileAssertionSupport;
|
|
|
|
|
|
|
|
|
|
public function testPagesDoNotInstantiateServicesRepositoriesOrGatewaysDirectly(): void
|
|
|
|
|
{
|
|
|
|
|
$violations = $this->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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 08:26:51 +01:00
|
|
|
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 = '/<script\b(?=[^>]*\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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
/**
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
private function findPatternMatchesInPhpFiles(string $relativeDirectory, string $pattern): array
|
2026-03-05 08:26:51 +01:00
|
|
|
{
|
|
|
|
|
return $this->findPatternMatchesInFiles($relativeDirectory, $pattern, ['php']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param list<string> $extensions
|
|
|
|
|
* @return list<string>
|
|
|
|
|
*/
|
|
|
|
|
private function findPatternMatchesInFiles(string $relativeDirectory, string $pattern, array $extensions): array
|
2026-03-04 15:56:58 +01:00
|
|
|
{
|
|
|
|
|
$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) {
|
2026-03-05 08:26:51 +01:00
|
|
|
if (!$file->isFile() || !in_array($file->getExtension(), $extensions, true)) {
|
2026-03-04 15:56:58 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|