refactor(config): remove runtime config files and centralize route/bootstrap
This commit is contained in:
@@ -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);
|
||||
|
||||
107
tests/Architecture/ConfigContractsTest.php
Normal file
107
tests/Architecture/ConfigContractsTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
90
tests/Http/RouteCatalogTest.php
Normal file
90
tests/Http/RouteCatalogTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user