refactor(config): remove runtime config files and centralize route/bootstrap

This commit is contained in:
2026-04-01 20:27:42 +02:00
parent dba589b495
commit 5699bc6c5b
20 changed files with 487 additions and 118 deletions

View File

@@ -4,6 +4,8 @@ namespace MintyPHP\Tests\App\Module;
use MintyPHP\App\AppContainer;
use MintyPHP\App\Module\ModuleRegistry;
use MintyPHP\Http\RouteCatalog;
use MintyPHP\Http\RouteRegistrar;
use MintyPHP\Tests\Support\AppContainerIsolationTrait;
use PHPUnit\Framework\TestCase;
@@ -27,6 +29,12 @@ final class RouterConfigCollisionTest extends TestCase
],
], true) . ';'
);
file_put_contents(
$fixturesDir . '/core-routes.php',
'<?php return ' . var_export([
['path' => 'login', 'target' => 'auth/login'],
], true) . ';'
);
try {
$registry = ModuleRegistry::boot($fixturesDir, ['mod-a']);
@@ -35,18 +43,14 @@ final class RouterConfigCollisionTest extends TestCase
$container->set(ModuleRegistry::class, static fn (): ModuleRegistry => $registry);
$this->pushAppContainer($container);
if (!defined('APP_ROUTE_DEFINITIONS')) {
define('APP_ROUTE_DEFINITIONS', [
['path' => 'login', 'target' => 'auth/login'],
]);
}
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Module route path conflict');
require dirname(__DIR__, 3) . '/config/router.php';
$catalog = RouteCatalog::fromConfigFile($fixturesDir . '/core-routes.php');
(new RouteRegistrar($registry))->register($catalog);
} finally {
$this->restoreAppContainer();
@unlink($fixturesDir . '/core-routes.php');
@unlink($fixturesDir . '/mod-a/module.php');
@rmdir($fixturesDir . '/mod-a');
@rmdir($fixturesDir);

View File

@@ -0,0 +1,107 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
final class ConfigContractsTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testConfigDirectoryContainsOnlyDeclarativeConfigFiles(): void
{
$required = [
'config/assets.php',
'config/routes.php',
'config/modules.php',
'config/themes.php',
'config/config.php.example',
];
foreach ($required as $file) {
$this->assertFileExists($this->projectRootPath() . '/' . $file, 'Missing required config file: ' . $file);
}
$forbidden = [
'config/router.php',
'config/settings.php',
];
foreach ($forbidden as $file) {
$this->assertFileDoesNotExist($this->projectRootPath() . '/' . $file, 'Forbidden runtime config file exists: ' . $file);
}
}
public function testConfigExampleDoesNotDefineLegacyRouteConstants(): void
{
$content = $this->readProjectFile('config/config.php.example');
$this->assertStringNotContainsString('APP_ROUTE_DEFINITIONS', $content);
$this->assertStringNotContainsString('APP_PUBLIC_PATHS', $content);
}
public function testRuntimePathsDoNotReferenceLegacyRouteConstants(): void
{
$runtimeFiles = $this->runtimeSourceFiles();
$patterns = [
'/\bAPP_ROUTE_DEFINITIONS\b/' => 'APP_ROUTE_DEFINITIONS',
'/\bAPP_PUBLIC_PATHS\b/' => 'APP_PUBLIC_PATHS',
];
$violations = [];
foreach ($runtimeFiles as $relativePath) {
$content = $this->readProjectFile($relativePath);
foreach ($patterns as $regex => $label) {
if (preg_match($regex, $content)) {
$violations[] = $relativePath . ': ' . $label;
}
}
}
$violations = array_values(array_unique($violations));
sort($violations);
$this->assertSame(
[],
$violations,
"Legacy route constants referenced in runtime paths:\n" . implode("\n", $violations)
);
}
/**
* @return list<string>
*/
private function runtimeSourceFiles(): array
{
$root = $this->projectRootPath();
$scanTargets = ['lib', 'pages', 'templates', 'web'];
$extensions = ['php', 'phtml', 'js'];
$files = [];
foreach ($scanTargets as $target) {
$fullPath = $root . '/' . $target;
if (!is_dir($fullPath)) {
continue;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($fullPath, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
$extension = strtolower((string) $file->getExtension());
if (!in_array($extension, $extensions, true)) {
continue;
}
$files[] = str_replace($root . '/', '', $file->getPathname());
}
}
$files = array_values(array_unique($files));
sort($files);
return $files;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace MintyPHP\Tests\Http;
use MintyPHP\Http\RouteCatalog;
use PHPUnit\Framework\TestCase;
use RuntimeException;
final class RouteCatalogTest extends TestCase
{
/** @var list<string> */
private array $tempFiles = [];
/** @var list<string> */
private array $tempDirs = [];
protected function tearDown(): void
{
foreach ($this->tempFiles as $file) {
@unlink($file);
}
foreach ($this->tempDirs as $dir) {
@rmdir($dir);
}
}
public function testLoadsCoreRoutesAndPublicPathsFromConfigFile(): void
{
$routesFile = $this->createRoutesFile([
['path' => 'login', 'target' => 'auth/login', 'public' => true],
['path' => 'profile', 'target' => 'account/profile', 'public' => false],
]);
$catalog = RouteCatalog::fromConfigFile($routesFile);
self::assertSame(
[
['path' => 'login', 'target' => 'auth/login', 'public' => true],
['path' => 'profile', 'target' => 'account/profile', 'public' => false],
],
$catalog->coreRoutes()
);
self::assertSame(['login'], $catalog->corePublicPaths());
}
public function testThrowsWhenRouteConfigDoesNotReturnArray(): void
{
$routesFile = $this->createRawConfigFile("<?php return 'invalid';\n");
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('must return an array');
RouteCatalog::fromConfigFile($routesFile);
}
public function testThrowsForInvalidRouteShape(): void
{
$routesFile = $this->createRoutesFile([
['path' => 'login'],
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Invalid route definition');
RouteCatalog::fromConfigFile($routesFile);
}
/**
* @param list<array<string, mixed>> $routes
*/
private function createRoutesFile(array $routes): string
{
return $this->createRawConfigFile(
'<?php return ' . var_export($routes, true) . ";\n"
);
}
private function createRawConfigFile(string $contents): string
{
$dir = sys_get_temp_dir() . '/route-catalog-test-' . uniqid('', true);
mkdir($dir, 0777, true);
$file = $dir . '/routes.php';
file_put_contents($file, $contents);
$this->tempFiles[] = $file;
$this->tempDirs[] = $dir;
return $file;
}
}

View File

@@ -3,6 +3,8 @@
namespace MintyPHP\Tests\Service\Settings;
use MintyPHP\Service\Settings\SettingCacheService;
use MintyPHP\Service\Settings\SettingKeys;
use MintyPHP\Service\Settings\SettingsMetadataGateway;
use PHPUnit\Framework\TestCase;
class SettingCacheServiceTest extends TestCase
@@ -31,6 +33,42 @@ class SettingCacheServiceTest extends TestCase
$this->assertNull($service->get('app_locale'));
}
public function testDefaultFilePathUsesStorageRuntimeSettingsFile(): void
{
$service = new SettingCacheService();
$this->assertStringEndsWith('/storage/runtime/settings.php', $service->filePath());
}
public function testColdStartHydratesHotPathKeysFromDatabaseAndWritesCacheFile(): void
{
$file = $this->tempFilePath();
$metadataGateway = $this->createMock(SettingsMetadataGateway::class);
$metadataGateway->expects($this->exactly(6))
->method('getValue')
->willReturnCallback(static function (string $key): ?string {
return match ($key) {
SettingKeys::APP_TITLE_KEY => 'Demo App',
SettingKeys::APP_LOCALE_KEY => 'de',
SettingKeys::APP_THEME_KEY => 'dark',
SettingKeys::APP_THEME_USER_KEY => '1',
SettingKeys::APP_REGISTRATION_KEY => '0',
SettingKeys::APP_PRIMARY_COLOR_KEY => '#112233',
default => null,
};
});
$service = new SettingCacheService($file, $metadataGateway);
$this->assertSame('Demo App', $service->get('app_title'));
$this->assertFileExists($file);
/** @var array<string, string> $cache */
$cache = include $file;
$this->assertSame('de', $cache[SettingKeys::APP_LOCALE_KEY] ?? null);
$this->assertSame('#112233', $cache[SettingKeys::APP_PRIMARY_COLOR_KEY] ?? null);
}
private function tempFilePath(): string
{
$dir = sys_get_temp_dir() . '/mintyphp_settings_cache_test_' . bin2hex(random_bytes(6));