2026-03-22 14:15:41 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace MintyPHP\Tests\Architecture;
|
|
|
|
|
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Repository layer must not access HTTP superglobals or emit HTTP responses.
|
|
|
|
|
* Repositories are pure data access — no session, request, or response handling.
|
|
|
|
|
*/
|
|
|
|
|
class RepositoryLayerIsolationContractTest extends TestCase
|
|
|
|
|
{
|
|
|
|
|
use ProjectFileAssertionSupport;
|
|
|
|
|
|
|
|
|
|
/** @var array<string, string> */
|
|
|
|
|
private const FORBIDDEN_PATTERNS = [
|
|
|
|
|
'/\$_SESSION\b/' => '$_SESSION',
|
|
|
|
|
'/\$_GET\b/' => '$_GET',
|
|
|
|
|
'/\$_POST\b/' => '$_POST',
|
|
|
|
|
'/\$_COOKIE\b/' => '$_COOKIE',
|
|
|
|
|
'/\$_REQUEST\b/' => '$_REQUEST',
|
|
|
|
|
'/\bheader\s*\(/' => 'header()',
|
|
|
|
|
'/\bhttp_response_code\s*\(/' => 'http_response_code()',
|
|
|
|
|
'/\bsetcookie\s*\(/' => 'setcookie()',
|
|
|
|
|
'/\bsession_start\s*\(/' => 'session_start()',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public function testCoreRepositoriesDoNotAccessHttpSuperglobals(): void
|
|
|
|
|
{
|
2026-04-13 23:20:05 +02:00
|
|
|
$this->assertRepositoryIsolation('core/Repository');
|
2026-03-22 14:15:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testModuleRepositoriesDoNotAccessHttpSuperglobals(): void
|
|
|
|
|
{
|
|
|
|
|
$root = $this->projectRootPath();
|
|
|
|
|
$modulesDir = $root . '/modules';
|
|
|
|
|
if (!is_dir($modulesDir)) {
|
|
|
|
|
self::markTestSkipped('modules/ directory missing.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (scandir($modulesDir) ?: [] as $entry) {
|
|
|
|
|
if ($entry === '.' || $entry === '..' || !is_dir($modulesDir . '/' . $entry)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$libRel = 'modules/' . $entry . '/lib';
|
|
|
|
|
if (!is_dir($root . '/' . $libRel)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scan only Repository directories within module lib/
|
|
|
|
|
$repoDir = $libRel . '/Repository';
|
|
|
|
|
if (is_dir($root . '/' . $repoDir)) {
|
|
|
|
|
$this->assertRepositoryIsolation($repoDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also scan files named *Repository.php anywhere in module lib/
|
|
|
|
|
$this->assertRepositoryFilesIsolated($libRel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertRepositoryIsolation(string $relativeDirectory): void
|
|
|
|
|
{
|
|
|
|
|
$violations = [];
|
|
|
|
|
|
|
|
|
|
foreach (self::FORBIDDEN_PATTERNS as $pattern => $label) {
|
|
|
|
|
foreach ($this->findPatternMatchesInPhpFiles($relativeDirectory, $pattern) as $file) {
|
|
|
|
|
$violations[] = "{$file} uses {$label}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self::assertSame(
|
|
|
|
|
[],
|
|
|
|
|
$violations,
|
|
|
|
|
"Repository layer must not access HTTP state. Violations:\n" . implode("\n", $violations)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertRepositoryFilesIsolated(string $relativeDirectory): void
|
|
|
|
|
{
|
|
|
|
|
$root = $this->projectRootPath();
|
|
|
|
|
$basePath = $root . '/' . $relativeDirectory;
|
|
|
|
|
$violations = [];
|
|
|
|
|
|
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
|
|
|
new \RecursiveDirectoryIterator($basePath, \FilesystemIterator::SKIP_DOTS),
|
|
|
|
|
\RecursiveIteratorIterator::LEAVES_ONLY
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
foreach ($iterator as $file) {
|
|
|
|
|
/** @var \SplFileInfo $file */
|
|
|
|
|
if ($file->getExtension() !== 'php') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!str_ends_with($file->getFilename(), 'Repository.php')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (str_ends_with($file->getFilename(), 'RepositoryInterface.php')) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$content = file_get_contents($file->getPathname());
|
|
|
|
|
if ($content === false) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$relativePath = str_replace($root . '/', '', $file->getPathname());
|
|
|
|
|
foreach (self::FORBIDDEN_PATTERNS as $pattern => $label) {
|
|
|
|
|
if (preg_match($pattern, $content)) {
|
|
|
|
|
$violations[] = "{$relativePath} uses {$label}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self::assertSame(
|
|
|
|
|
[],
|
|
|
|
|
$violations,
|
|
|
|
|
"Repository files must not access HTTP state. Violations:\n" . implode("\n", $violations)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|