From bda6bcd9c6d10b35759d8cb0765b6207ee522c20 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:57:40 +0400 Subject: [PATCH 01/10] [#535] Add Loader split baseline tests --- .../App/Stages/LoadEnvironmentStageTest.php | 1 + .../Unit/App/Stages/LoadHelpersStageTest.php | 46 ++++++++++++++++ tests/Unit/Config/ConfigTest.php | 53 +++++++++++++++++++ tests/Unit/Environment/EnvironmentTest.php | 18 +++++++ tests/Unit/Loader/SetupTest.php | 21 ++++++++ .../Uploads/UploadConfigProviderTest.php | 27 ++++++++++ 6 files changed, 166 insertions(+) diff --git a/tests/Unit/App/Stages/LoadEnvironmentStageTest.php b/tests/Unit/App/Stages/LoadEnvironmentStageTest.php index 18977ae52..53a41ae50 100644 --- a/tests/Unit/App/Stages/LoadEnvironmentStageTest.php +++ b/tests/Unit/App/Stages/LoadEnvironmentStageTest.php @@ -25,6 +25,7 @@ public function testLoadEnvironmentStageLoadsEnvVars(): void $stage = new LoadEnvironmentStage(); $stage->process($this->context); + $this->assertSame('testing', environment()->getAppEnv()); $this->assertNotEmpty(env('APP_KEY')); } } diff --git a/tests/Unit/App/Stages/LoadHelpersStageTest.php b/tests/Unit/App/Stages/LoadHelpersStageTest.php index dc9244635..daa70061d 100644 --- a/tests/Unit/App/Stages/LoadHelpersStageTest.php +++ b/tests/Unit/App/Stages/LoadHelpersStageTest.php @@ -25,5 +25,51 @@ public function testLoadHelpersStageLoadsComponentHelpers(): void $this->assertTrue(function_exists('config')); $this->assertTrue(function_exists('env')); $this->assertTrue(function_exists('base_dir')); + $stage->process($this->context); + + $this->assertTrue(function_exists('config')); + } + + public function testLoadHelpersStageLoadsAppAndModuleHelpersOnce(): void + { + $appDir = PROJECT_ROOT . DS . 'helpers'; + $moduleDir = PROJECT_ROOT . DS . 'modules' . DS . 'Test' . DS . 'helpers'; + $createdAppDir = !is_dir($appDir); + $createdModuleDir = !is_dir($moduleDir); + $appFile = $appDir . DS . uniqid('app_helper_') . '.php'; + $moduleFile = $moduleDir . DS . uniqid('module_helper_') . '.php'; + + try { + if ($createdAppDir) { + $this->assertTrue(mkdir($appDir)); + } + if ($createdModuleDir) { + $this->assertTrue(mkdir($moduleDir)); + } + + $this->assertNotFalse(file_put_contents($appFile, 'assertNotFalse(file_put_contents($moduleFile, 'process($this->context); + $stage->process($this->context); + + $this->assertSame(1, $GLOBALS['appHelperLoads'] ?? 0); + $this->assertSame(1, $GLOBALS['moduleHelperLoads'] ?? 0); + } finally { + if (is_file($appFile)) { + unlink($appFile); + } + if (is_file($moduleFile)) { + unlink($moduleFile); + } + if ($createdAppDir && is_dir($appDir)) { + rmdir($appDir); + } + if ($createdModuleDir && is_dir($moduleDir)) { + rmdir($moduleDir); + } + unset($GLOBALS['appHelperLoads'], $GLOBALS['moduleHelperLoads']); + } } } diff --git a/tests/Unit/Config/ConfigTest.php b/tests/Unit/Config/ConfigTest.php index 157ffc3ea..918049581 100644 --- a/tests/Unit/Config/ConfigTest.php +++ b/tests/Unit/Config/ConfigTest.php @@ -2,6 +2,8 @@ namespace Quantum\Tests\Unit\Config; +use Quantum\Tests\_root\modules\Test\Transformers\PostTransformer; +use Quantum\Transformer\Contracts\TransformerInterface; use Quantum\Config\Exceptions\ConfigException; use Quantum\Loader\Exceptions\LoaderException; use Quantum\Tests\Unit\AppTestCase; @@ -46,6 +48,57 @@ public function testConfigImport(): void $this->assertEquals('sqlite', $this->config->get('database.default')); } + public function testImportLoadsModuleConfig(): void + { + $this->config->import(new Setup('config', 'dependencies', true, 'Test')); + + $this->assertSame( + PostTransformer::class, + $this->config->get('dependencies.' . TransformerInterface::class) + ); + } + + public function testModuleConfigTakesPrecedenceOverSharedConfig(): void + { + $moduleConfig = PROJECT_ROOT . DS . 'modules' . DS . 'Test' . DS . 'config' . DS . 'app.php'; + $this->assertFileDoesNotExist($moduleConfig); + + try { + $this->createFile($moduleConfig, " 'Module app'];\n"); + $this->config->import(new Setup('config', 'app', true, 'Test')); + + $this->assertSame('Module app', $this->config->get('app.name')); + } finally { + $this->removeFile($moduleConfig); + } + } + + public function testImportFallsBackToSharedConfig(): void + { + $this->config->import(new Setup('config', 'app', true, 'Test')); + + $this->assertSame('Quantum PHP Framework', $this->config->get('app.name')); + } + + public function testImportDoesNotFallBackWhenHierarchyIsDisabled(): void + { + try { + $this->config->import(new Setup('config', 'app', false, 'Test')); + $this->fail('Expected a missing config exception'); + } catch (LoaderException $exception) { + $this->assertFalse($this->config->has('app')); + } + } + + public function testLoadDoesNotReplacePreviouslyLoadedConfig(): void + { + $this->config->load(new Setup('config', 'app')); + $this->config->load(new Setup('config', 'database')); + + $this->assertSame('Quantum PHP Framework', $this->config->get('name')); + $this->assertNull($this->config->get('default')); + } + public function testImportingNonExistingConfigFile(): void { $this->expectException(LoaderException::class); diff --git a/tests/Unit/Environment/EnvironmentTest.php b/tests/Unit/Environment/EnvironmentTest.php index 8ee614c69..01dab00cc 100644 --- a/tests/Unit/Environment/EnvironmentTest.php +++ b/tests/Unit/Environment/EnvironmentTest.php @@ -2,8 +2,11 @@ namespace Quantum\Tests\Unit\Environment; +use Quantum\Loader\Exceptions\LoaderException; use Quantum\Environment\Environment; use Quantum\Tests\Unit\AppTestCase; +use Quantum\App\AppContext; +use Quantum\Loader\Setup; use Quantum\App\App; class EnvironmentTest extends AppTestCase @@ -22,6 +25,21 @@ public function testEnvironmentGetAppEnv(): void $this->assertEquals('testing', $this->env->getAppEnv()); } + public function testLoadReportsMissingBootstrapConfig(): void + { + $context = App::getContext(); + + try { + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + + $this->expectException(LoaderException::class); + + (new Environment())->load(new Setup('config', 'env')); + } finally { + App::setContext($context); + } + } + public function testEnvironmentGetValue(): void { $this->assertNull($this->env->getValue('NON_EXISTING_KEY')); diff --git a/tests/Unit/Loader/SetupTest.php b/tests/Unit/Loader/SetupTest.php index 3dc94cbc6..59d3fe7cb 100644 --- a/tests/Unit/Loader/SetupTest.php +++ b/tests/Unit/Loader/SetupTest.php @@ -34,6 +34,27 @@ public function testSetupConstructor(): void $this->assertEquals('File `' . $setup->getPathPrefix() . DS . $setup->getFilename() . '` not found!', $setup->getExceptionMessage()); } + public function testSetupUsesCurrentRequestModuleWhenModuleIsOmitted(): void + { + $this->testRequest('/'); + + $setup = new Setup('config', 'dependencies'); + + request()->setMatchedRoute(null); + + $this->assertSame('Test', $setup->getModule()); + $this->assertSame('Meme', (new Setup('config', 'dependencies', true, 'Meme'))->getModule()); + } + + public function testDefaultExceptionMessageIsCapturedAtConstruction(): void + { + $setup = new Setup('config', 'app'); + + $setup->setPathPrefix('other')->setFilename('changed'); + + $this->assertSame('File `config' . DS . 'app` not found!', $setup->getExceptionMessage()); + } + public function testSetGetPathPrefix(): void { $this->setup->setPathPrefix('config'); diff --git a/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php b/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php index 321838dfd..ea13bdb1e 100644 --- a/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php +++ b/tests/Unit/Storage/Uploads/UploadConfigProviderTest.php @@ -4,9 +4,36 @@ use Quantum\Storage\Uploads\UploadConfigProvider; use Quantum\Tests\Unit\AppTestCase; +use Quantum\App\AppContext; +use Quantum\App\App; class UploadConfigProviderTest extends AppTestCase { + public function testLoadsSharedUploadConfigWhenNotPreloaded(): void + { + config()->flush(); + + $map = (new UploadConfigProvider())->getAllowedMimeTypesMap(); + + $this->assertSame(['jpg', 'jpeg'], $map['image/jpeg']); + $this->assertTrue(config()->has('uploads')); + } + + public function testMissingOptionalUploadConfigReturnsEmptyMap(): void + { + config()->flush(); + $context = App::getContext(); + + try { + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + + $this->assertSame([], (new UploadConfigProvider())->getAllowedMimeTypesMap()); + $this->assertFalse(config()->has('uploads')); + } finally { + App::setContext($context); + } + } + public function testReturnsConfiguredAllowedMimeTypesMap(): void { config()->set('uploads', ['allowed_mime_types' => ['text/plain' => ['txt']]]); From 93c842e29ce03778ba75958c3cc11f7da02e0db4 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:39:26 +0400 Subject: [PATCH 02/10] [#535] Move config resolution into Config --- src/App/Stages/LoadAppConfigStage.php | 2 +- src/App/Stages/LoadEnvironmentStage.php | 2 +- src/App/Traits/WebAppTrait.php | 2 +- src/Auth/Factories/AuthFactory.php | 2 +- src/Cache/Factories/CacheFactory.php | 2 +- src/Captcha/Factories/CaptchaFactory.php | 2 +- src/Config/Config.php | 66 +++++++++++++++---- src/Config/Contracts/ConfigInterface.php | 7 +- src/{Loader => Config}/Setup.php | 4 +- .../Commands/ResourceCacheClearCommand.php | 2 +- src/Cron/Helpers/cron.php | 2 +- src/Database/Database.php | 2 +- src/Environment/Environment.php | 2 +- src/Hook/HookManager.php | 2 +- src/Lang/Factories/LangFactory.php | 2 +- src/Loader/Loader.php | 1 + src/Logger/Factories/LoggerFactory.php | 2 +- src/Mailer/Factories/MailerFactory.php | 2 +- .../Toolkit/src/Middlewares/BasicAuth.php.tpl | 2 +- .../Factories/RateLimiterFactory.php | 2 +- src/Renderer/Factories/RendererFactory.php | 2 +- src/ResourceCache/ViewCache.php | 2 +- src/Session/Factories/SessionFactory.php | 2 +- src/Storage/Factories/FileSystemFactory.php | 2 +- src/Storage/Uploads/UploadConfigProvider.php | 21 +----- tests/Unit/Auth/AuthTestCase.php | 2 +- .../Cache/Adapters/DatabaseAdapterTest.php | 2 +- tests/Unit/Config/ConfigTest.php | 38 +++++++++-- tests/Unit/{Loader => Config}/SetupTest.php | 4 +- .../Adapters/Sleekdb/SleekDbalTestCase.php | 2 +- tests/Unit/Database/DatabaseTest.php | 2 +- .../Database/Factories/TableFactoryTest.php | 2 +- tests/Unit/Di/DiContainerTest.php | 4 +- tests/Unit/Di/DiRegistryTest.php | 6 +- tests/Unit/Di/DiTest.php | 2 +- tests/Unit/Environment/EnvironmentTest.php | 2 +- tests/Unit/Loader/LoaderTest.php | 2 +- .../Adapters/MailerAdapterTraitTest.php | 2 +- .../Unit/Mailer/Adapters/SmtpAdapterTest.php | 2 +- .../Mailer/Factories/MailerFactoryTest.php | 2 +- tests/Unit/Mailer/MailerTest.php | 2 +- tests/Unit/Migration/MigrationManagerTest.php | 2 +- .../Unit/Model/ModelSoftDeletesSleekTest.php | 2 +- .../Session/Factories/SessionFactoryTest.php | 2 +- tests/Unit/Session/SessionTest.php | 2 +- .../Validation/Traits/GeneralRuleTest.php | 2 +- 46 files changed, 143 insertions(+), 82 deletions(-) rename src/{Loader => Config}/Setup.php (97%) rename tests/Unit/{Loader => Config}/SetupTest.php (97%) diff --git a/src/App/Stages/LoadAppConfigStage.php b/src/App/Stages/LoadAppConfigStage.php index 1e8e51a27..fcd57d602 100644 --- a/src/App/Stages/LoadAppConfigStage.php +++ b/src/App/Stages/LoadAppConfigStage.php @@ -15,7 +15,7 @@ use Quantum\Loader\Exceptions\LoaderException; use Quantum\Di\Exceptions\DiException; use Quantum\App\AppContext; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; /** diff --git a/src/App/Stages/LoadEnvironmentStage.php b/src/App/Stages/LoadEnvironmentStage.php index 42d041717..2dc02d967 100644 --- a/src/App/Stages/LoadEnvironmentStage.php +++ b/src/App/Stages/LoadEnvironmentStage.php @@ -16,7 +16,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Environment\Environment; use Quantum\App\AppContext; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/App/Traits/WebAppTrait.php b/src/App/Traits/WebAppTrait.php index 8c88a2dcc..7ae754158 100644 --- a/src/App/Traits/WebAppTrait.php +++ b/src/App/Traits/WebAppTrait.php @@ -23,7 +23,7 @@ use Quantum\Router\RouteFinder; use Quantum\Debugger\Debugger; use Quantum\Http\Response; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; use Exception; diff --git a/src/Auth/Factories/AuthFactory.php b/src/Auth/Factories/AuthFactory.php index 5dcca8b1c..37d47db29 100644 --- a/src/Auth/Factories/AuthFactory.php +++ b/src/Auth/Factories/AuthFactory.php @@ -24,7 +24,7 @@ use Quantum\Service\Service; use Quantum\Hasher\Hasher; use Quantum\Jwt\JwtToken; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Auth\Auth; use Quantum\Di\Di; diff --git a/src/Cache/Factories/CacheFactory.php b/src/Cache/Factories/CacheFactory.php index 415d336d8..7c1373179 100644 --- a/src/Cache/Factories/CacheFactory.php +++ b/src/Cache/Factories/CacheFactory.php @@ -20,7 +20,7 @@ use Quantum\Di\Exceptions\DiException; use Psr\SimpleCache\CacheInterface; use Quantum\Cache\Enums\CacheType; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Cache\Cache; use Quantum\Di\Di; diff --git a/src/Captcha/Factories/CaptchaFactory.php b/src/Captcha/Factories/CaptchaFactory.php index d38951d96..f700f84e1 100644 --- a/src/Captcha/Factories/CaptchaFactory.php +++ b/src/Captcha/Factories/CaptchaFactory.php @@ -20,7 +20,7 @@ use Quantum\Captcha\Enums\CaptchaType; use Quantum\HttpClient\HttpClient; use Quantum\Captcha\Captcha; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Config/Config.php b/src/Config/Config.php index 02f89b4c5..1f48715ff 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -11,14 +11,8 @@ namespace Quantum\Config; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Contracts\ConfigInterface; -use Quantum\Di\Exceptions\DiException; use Dflydev\DotAccessData\Data; -use Quantum\Loader\Loader; -use Quantum\Loader\Setup; -use ReflectionException; -use Quantum\Di\Di; /** * Class Config @@ -30,7 +24,7 @@ class Config implements ConfigInterface /** * @inheritDoc - * @throws DiException|LoaderException|ReflectionException + * @throws ConfigException */ public function load(Setup $setup): void { @@ -44,8 +38,6 @@ public function load(Setup $setup): void /** * @inheritDoc * @throws ConfigException - * @throws DiException - * @throws ReflectionException|LoaderException */ public function import(Setup $setup): void { @@ -64,6 +56,21 @@ public function import(Setup $setup): void } } + /** + * @inheritDoc + * @throws ConfigException + */ + public function importIfExists(Setup $setup): bool + { + if ($this->resolveFilePath($setup) === null) { + return false; + } + + $this->import($setup); + + return true; + } + /** * @inheritDoc */ @@ -122,14 +129,47 @@ public function flush(): void /** * @return array - * @throws DiException|LoaderException|ReflectionException + * @throws ConfigException */ private function loadConfig(Setup $setup): array { - if (!Di::isRegistered(Loader::class)) { - Di::register(Loader::class); + $filePath = $this->resolveFilePath($setup); + + if ($filePath === null) { + throw new ConfigException(_message($setup->getExceptionMessage(), $setup->getFilename() ?? '')); + } + + return require $filePath; + } + + private function resolveFilePath(Setup $setup): ?string + { + $filePath = ''; + + if ($setup->getModule()) { + $filePath = modules_dir() . DS . $setup->getModule() . DS; + } + + if ($setup->getPathPrefix()) { + $filePath .= $setup->getPathPrefix() . DS; + } + + $filePath .= $setup->getFilename() . '.php'; + + if (file_exists($filePath)) { + return $filePath; + } + + if ($setup->getHierarchy()) { + $filePath = base_dir() . DS . 'shared' . DS + . strtolower($setup->getPathPrefix() ?? '') . DS + . $setup->getFilename() . '.php'; + + if (file_exists($filePath)) { + return $filePath; + } } - return Di::get(Loader::class)->setup($setup)->load(); + return null; } } diff --git a/src/Config/Contracts/ConfigInterface.php b/src/Config/Contracts/ConfigInterface.php index ad885d9f4..c48ff52c9 100644 --- a/src/Config/Contracts/ConfigInterface.php +++ b/src/Config/Contracts/ConfigInterface.php @@ -10,7 +10,7 @@ namespace Quantum\Config\Contracts; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; /** * Interface ConfigInterface @@ -61,4 +61,9 @@ public function load(Setup $setup): void; * Imports new config file */ public function import(Setup $setup): void; + + /** + * Imports new config file when it exists + */ + public function importIfExists(Setup $setup): bool; } diff --git a/src/Loader/Setup.php b/src/Config/Setup.php similarity index 97% rename from src/Loader/Setup.php rename to src/Config/Setup.php index e3ef1b739..20c1c0122 100644 --- a/src/Loader/Setup.php +++ b/src/Config/Setup.php @@ -8,14 +8,14 @@ * @link https://quantumphp.io */ -namespace Quantum\Loader; +namespace Quantum\Config; use Quantum\Di\Exceptions\DiException; use ReflectionException; /** * Class Setup - * @package Quantum\Loader + * @package Quantum\Config */ class Setup { diff --git a/src/Console/Commands/ResourceCacheClearCommand.php b/src/Console/Commands/ResourceCacheClearCommand.php index 80a9a004f..ca0fb54e9 100644 --- a/src/Console/Commands/ResourceCacheClearCommand.php +++ b/src/Console/Commands/ResourceCacheClearCommand.php @@ -17,7 +17,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Storage\FileSystem; use Quantum\Console\CliCommand; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Exception; diff --git a/src/Cron/Helpers/cron.php b/src/Cron/Helpers/cron.php index 76a9c35b6..5c81e668c 100644 --- a/src/Cron/Helpers/cron.php +++ b/src/Cron/Helpers/cron.php @@ -11,7 +11,7 @@ use Quantum\Cron\CronManager; use Quantum\Cron\CronTask; use Quantum\Cron\Schedule; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; if (!function_exists('cron_config')) { /** diff --git a/src/Database/Database.php b/src/Database/Database.php index 0fb73bfab..f1b14505f 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -19,7 +19,7 @@ use Quantum\App\Exceptions\BaseException; use Quantum\Database\Enums\DatabaseType; use Quantum\Di\Exceptions\DiException; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; /** diff --git a/src/Environment/Environment.php b/src/Environment/Environment.php index d90a8aac5..2632cb546 100644 --- a/src/Environment/Environment.php +++ b/src/Environment/Environment.php @@ -16,7 +16,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Environment\Enums\Env; use Quantum\Loader\Loader; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\App\App; use Dotenv\Dotenv; diff --git a/src/Hook/HookManager.php b/src/Hook/HookManager.php index b1d5c046e..4a9e065b3 100644 --- a/src/Hook/HookManager.php +++ b/src/Hook/HookManager.php @@ -14,7 +14,7 @@ use Quantum\Loader\Exceptions\LoaderException; use Quantum\Hook\Exceptions\HookException; use Quantum\Di\Exceptions\DiException; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; /** diff --git a/src/Lang/Factories/LangFactory.php b/src/Lang/Factories/LangFactory.php index a0eec250a..5ff4c3f5a 100644 --- a/src/Lang/Factories/LangFactory.php +++ b/src/Lang/Factories/LangFactory.php @@ -21,7 +21,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\HttpClient\HttpClient; use Quantum\Lang\Enums\LangType; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Lang\Lang; use Quantum\Di\Di; diff --git a/src/Loader/Loader.php b/src/Loader/Loader.php index dc6fb5979..fd9c7b6a2 100644 --- a/src/Loader/Loader.php +++ b/src/Loader/Loader.php @@ -11,6 +11,7 @@ namespace Quantum\Loader; use Quantum\Loader\Exceptions\LoaderException; +use Quantum\Config\Setup; use Quantum\App\App; /** diff --git a/src/Logger/Factories/LoggerFactory.php b/src/Logger/Factories/LoggerFactory.php index a9d431c16..fa82c5fee 100644 --- a/src/Logger/Factories/LoggerFactory.php +++ b/src/Logger/Factories/LoggerFactory.php @@ -21,7 +21,7 @@ use Quantum\Logger\Enums\LoggerType; use Quantum\Logger\LoggerConfig; use Quantum\Logger\Logger; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Mailer/Factories/MailerFactory.php b/src/Mailer/Factories/MailerFactory.php index 6b1b1919f..272364e8a 100644 --- a/src/Mailer/Factories/MailerFactory.php +++ b/src/Mailer/Factories/MailerFactory.php @@ -23,7 +23,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Mailer\Enums\MailerType; use Quantum\Mailer\Mailer; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl b/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl index f94202a81..84ddd21d7 100644 --- a/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl +++ b/src/Module/Templates/Toolkit/src/Middlewares/BasicAuth.php.tpl @@ -9,7 +9,7 @@ namespace Modules\Toolkit\Middlewares; use Quantum\Middleware\Middleware; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\Http\Response; use Quantum\Http\Request; use Closure; diff --git a/src/RateLimit/Factories/RateLimiterFactory.php b/src/RateLimit/Factories/RateLimiterFactory.php index 92b61e104..32e15276d 100644 --- a/src/RateLimit/Factories/RateLimiterFactory.php +++ b/src/RateLimit/Factories/RateLimiterFactory.php @@ -19,7 +19,7 @@ use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; use Quantum\RateLimit\RateLimiter; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Renderer/Factories/RendererFactory.php b/src/Renderer/Factories/RendererFactory.php index 5c1e86eef..c156b11a7 100644 --- a/src/Renderer/Factories/RendererFactory.php +++ b/src/Renderer/Factories/RendererFactory.php @@ -19,7 +19,7 @@ use Quantum\Renderer\Enums\RendererType; use Quantum\Di\Exceptions\DiException; use Quantum\Renderer\Renderer; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/ResourceCache/ViewCache.php b/src/ResourceCache/ViewCache.php index 2267ad96a..cb1ac1b98 100644 --- a/src/ResourceCache/ViewCache.php +++ b/src/ResourceCache/ViewCache.php @@ -18,7 +18,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Storage\FileSystem; use Quantum\Http\Response; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use voku\helper\HtmlMin; use Exception; diff --git a/src/Session/Factories/SessionFactory.php b/src/Session/Factories/SessionFactory.php index 4bbcb8b4e..934848562 100644 --- a/src/Session/Factories/SessionFactory.php +++ b/src/Session/Factories/SessionFactory.php @@ -19,7 +19,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Session\Enums\SessionType; use Quantum\Session\Session; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Storage/Factories/FileSystemFactory.php b/src/Storage/Factories/FileSystemFactory.php index f66528713..9098d6a97 100644 --- a/src/Storage/Factories/FileSystemFactory.php +++ b/src/Storage/Factories/FileSystemFactory.php @@ -27,7 +27,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\HttpClient\HttpClient; use Quantum\Storage\FileSystem; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionException; use Quantum\Di\Di; diff --git a/src/Storage/Uploads/UploadConfigProvider.php b/src/Storage/Uploads/UploadConfigProvider.php index 1ff1ab780..c30cac5bd 100644 --- a/src/Storage/Uploads/UploadConfigProvider.php +++ b/src/Storage/Uploads/UploadConfigProvider.php @@ -11,36 +11,21 @@ namespace Quantum\Storage\Uploads; use Quantum\Storage\Exceptions\FileUploadException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Di\Exceptions\DiException; -use Quantum\Loader\Loader; -use Quantum\Loader\Setup; -use ReflectionException; -use Quantum\Di\Di; +use Quantum\Config\Setup; class UploadConfigProvider { /** * @return array> - * @throws FileUploadException|LoaderException|ConfigException|DiException|ReflectionException + * @throws FileUploadException|ConfigException */ public function getAllowedMimeTypesMap(): array { if (!config()->has('uploads')) { - if (!Di::isRegistered(Loader::class)) { - Di::register(Loader::class); - } - - $loader = Di::get(Loader::class); - $setup = new Setup('config', 'uploads'); - $loader->setup($setup); - - if (!$loader->fileExists()) { + if (!config()->importIfExists(new Setup('config', 'uploads'))) { return []; } - - config()->import($setup); } $allowedMimeTypesMap = config()->get('uploads.allowed_mime_types') ?? []; diff --git a/tests/Unit/Auth/AuthTestCase.php b/tests/Unit/Auth/AuthTestCase.php index 6c40c0983..589f9afd8 100644 --- a/tests/Unit/Auth/AuthTestCase.php +++ b/tests/Unit/Auth/AuthTestCase.php @@ -14,7 +14,7 @@ function random_number(int $length = 10): int use Quantum\Database\Adapters\Sleekdb\SleekDbal; use Quantum\Tests\Unit\AppTestCase; use Quantum\Mailer\Mailer; - use Quantum\Loader\Setup; + use Quantum\Config\Setup; use Quantum\Auth\User; use Mockery; diff --git a/tests/Unit/Cache/Adapters/DatabaseAdapterTest.php b/tests/Unit/Cache/Adapters/DatabaseAdapterTest.php index 8b1bf1326..3b0356a3f 100644 --- a/tests/Unit/Cache/Adapters/DatabaseAdapterTest.php +++ b/tests/Unit/Cache/Adapters/DatabaseAdapterTest.php @@ -5,7 +5,7 @@ use Quantum\Database\Adapters\Sleekdb\SleekDbal; use Quantum\Cache\Adapters\DatabaseAdapter; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use DateInterval; class DatabaseAdapterTest extends AppTestCase diff --git a/tests/Unit/Config/ConfigTest.php b/tests/Unit/Config/ConfigTest.php index 918049581..78a28e56c 100644 --- a/tests/Unit/Config/ConfigTest.php +++ b/tests/Unit/Config/ConfigTest.php @@ -5,11 +5,10 @@ use Quantum\Tests\_root\modules\Test\Transformers\PostTransformer; use Quantum\Transformer\Contracts\TransformerInterface; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Tests\Unit\AppTestCase; use Dflydev\DotAccessData\Data; use Quantum\Config\Config; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class ConfigTest extends AppTestCase { @@ -80,12 +79,43 @@ public function testImportFallsBackToSharedConfig(): void $this->assertSame('Quantum PHP Framework', $this->config->get('app.name')); } + public function testImportIfExistsReturnsFalseWithoutChangingConfig(): void + { + $this->assertFalse($this->config->importIfExists(new Setup('config', 'missing_optional'))); + $this->assertNull($this->config->all()); + } + + public function testImportIfExistsTreatsEmptyConfigAsPresent(): void + { + $filePath = PROJECT_ROOT . DS . 'shared' . DS . 'config' . DS . 'empty_optional.php'; + $this->assertFileDoesNotExist($filePath); + + try { + $this->createFile($filePath, "assertTrue($this->config->importIfExists(new Setup('config', 'empty_optional'))); + $this->assertSame([], $this->config->get('empty_optional')); + } finally { + $this->removeFile($filePath); + } + } + + public function testImportIfExistsPreservesCollision(): void + { + $this->config->import(new Setup('config', 'app')); + + $this->expectException(ConfigException::class); + $this->expectExceptionMessage('Config key `app` is already in use'); + + $this->config->importIfExists(new Setup('config', 'app')); + } + public function testImportDoesNotFallBackWhenHierarchyIsDisabled(): void { try { $this->config->import(new Setup('config', 'app', false, 'Test')); $this->fail('Expected a missing config exception'); - } catch (LoaderException $exception) { + } catch (ConfigException $exception) { $this->assertFalse($this->config->has('app')); } } @@ -101,7 +131,7 @@ public function testLoadDoesNotReplacePreviouslyLoadedConfig(): void public function testImportingNonExistingConfigFile(): void { - $this->expectException(LoaderException::class); + $this->expectException(ConfigException::class); $this->expectExceptionMessage('File `config' . DS . 'somefile` not found!'); diff --git a/tests/Unit/Loader/SetupTest.php b/tests/Unit/Config/SetupTest.php similarity index 97% rename from tests/Unit/Loader/SetupTest.php rename to tests/Unit/Config/SetupTest.php index 59d3fe7cb..c9753bc8d 100644 --- a/tests/Unit/Loader/SetupTest.php +++ b/tests/Unit/Config/SetupTest.php @@ -1,9 +1,9 @@ expectException(DiException::class); - $this->expectExceptionMessage('The dependency `Quantum\Loader\Setup` is already registered.'); + $this->expectExceptionMessage('The dependency `Quantum\Config\Setup` is already registered.'); $this->container->register(Setup::class); $this->container->register(Setup::class); diff --git a/tests/Unit/Di/DiRegistryTest.php b/tests/Unit/Di/DiRegistryTest.php index 633e6db84..b91b1fdce 100644 --- a/tests/Unit/Di/DiRegistryTest.php +++ b/tests/Unit/Di/DiRegistryTest.php @@ -5,7 +5,7 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Di\DiRegistry; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\Http\Request; class DiRegistryTest extends AppTestCase @@ -40,7 +40,7 @@ public function testRegisterDependencies(): void public function testAttemptingToRegisterAlreadyRegisteredDependency(): void { $this->expectException(DiException::class); - $this->expectExceptionMessage('The dependency `Quantum\Loader\Setup` is already registered.'); + $this->expectExceptionMessage('The dependency `Quantum\Config\Setup` is already registered.'); $this->registry->register(Setup::class); $this->registry->register(Setup::class); @@ -73,7 +73,7 @@ public function testAttemptingToRegisterNonExistentAbstract(): void public function testGetConcreteThrowsForUnregisteredDependency(): void { $this->expectException(DiException::class); - $this->expectExceptionMessage('The dependency `Quantum\Loader\Setup` is not registered.'); + $this->expectExceptionMessage('The dependency `Quantum\Config\Setup` is not registered.'); $this->registry->getConcrete(Setup::class); } diff --git a/tests/Unit/Di/DiTest.php b/tests/Unit/Di/DiTest.php index d15a0445f..aeb7198cb 100644 --- a/tests/Unit/Di/DiTest.php +++ b/tests/Unit/Di/DiTest.php @@ -53,7 +53,7 @@ public function __construct(CircularDependencyA $a) use Quantum\Service\DummyService; use Quantum\Http\Response; use Quantum\Http\Request; - use Quantum\Loader\Setup; + use Quantum\Config\Setup; use Quantum\App\App; use Quantum\Di\Di; diff --git a/tests/Unit/Environment/EnvironmentTest.php b/tests/Unit/Environment/EnvironmentTest.php index 01dab00cc..29227c245 100644 --- a/tests/Unit/Environment/EnvironmentTest.php +++ b/tests/Unit/Environment/EnvironmentTest.php @@ -6,7 +6,7 @@ use Quantum\Environment\Environment; use Quantum\Tests\Unit\AppTestCase; use Quantum\App\AppContext; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\App\App; class EnvironmentTest extends AppTestCase diff --git a/tests/Unit/Loader/LoaderTest.php b/tests/Unit/Loader/LoaderTest.php index 6491db7c8..edc1c88f7 100644 --- a/tests/Unit/Loader/LoaderTest.php +++ b/tests/Unit/Loader/LoaderTest.php @@ -4,7 +4,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\Loader\Loader; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class LoaderTest extends AppTestCase { diff --git a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php index e29ab74eb..59db521d7 100644 --- a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php +++ b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php @@ -5,7 +5,7 @@ use Quantum\Mailer\Adapters\SendinblueAdapter; use Quantum\Mailer\Adapters\SmtpAdapter; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class MailerAdapterTraitTest extends AppTestCase { diff --git a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php index d91dd8d01..ff1c582c9 100644 --- a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php +++ b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php @@ -5,7 +5,7 @@ use Quantum\Tests\Unit\Mailer\MailerTestCase; use Quantum\Mailer\Contracts\MailerInterface; use Quantum\Mailer\Adapters\SmtpAdapter; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class SmtpAdapterTest extends MailerTestCase { diff --git a/tests/Unit/Mailer/Factories/MailerFactoryTest.php b/tests/Unit/Mailer/Factories/MailerFactoryTest.php index 48fd8ccf4..c3aff5f64 100644 --- a/tests/Unit/Mailer/Factories/MailerFactoryTest.php +++ b/tests/Unit/Mailer/Factories/MailerFactoryTest.php @@ -13,7 +13,7 @@ use Quantum\Mailer\Enums\MailerType; use Quantum\Tests\Unit\AppTestCase; use Quantum\Mailer\Mailer; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\Di\Di; class MailerFactoryTest extends AppTestCase diff --git a/tests/Unit/Mailer/MailerTest.php b/tests/Unit/Mailer/MailerTest.php index 972603474..bc9737abf 100644 --- a/tests/Unit/Mailer/MailerTest.php +++ b/tests/Unit/Mailer/MailerTest.php @@ -12,7 +12,7 @@ use Quantum\Mailer\Adapters\SmtpAdapter; use Quantum\Tests\Unit\AppTestCase; use Quantum\Mailer\Mailer; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use ReflectionMethod; class MailerTest extends AppTestCase diff --git a/tests/Unit/Migration/MigrationManagerTest.php b/tests/Unit/Migration/MigrationManagerTest.php index 20b5ede0b..b4229eb66 100644 --- a/tests/Unit/Migration/MigrationManagerTest.php +++ b/tests/Unit/Migration/MigrationManagerTest.php @@ -8,7 +8,7 @@ use Quantum\Migration\MigrationTable; use Quantum\Tests\Unit\AppTestCase; use Quantum\Database\Database; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Mockery; class MigrationManagerTest extends AppTestCase diff --git a/tests/Unit/Model/ModelSoftDeletesSleekTest.php b/tests/Unit/Model/ModelSoftDeletesSleekTest.php index df9cb667c..4c0a86743 100644 --- a/tests/Unit/Model/ModelSoftDeletesSleekTest.php +++ b/tests/Unit/Model/ModelSoftDeletesSleekTest.php @@ -8,7 +8,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\Model\ModelCollection; use Quantum\Paginator\Paginator; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\Model\Model; class ModelSoftDeletesSleekTest extends AppTestCase diff --git a/tests/Unit/Session/Factories/SessionFactoryTest.php b/tests/Unit/Session/Factories/SessionFactoryTest.php index 93cbc6ca7..8117c8849 100644 --- a/tests/Unit/Session/Factories/SessionFactoryTest.php +++ b/tests/Unit/Session/Factories/SessionFactoryTest.php @@ -12,7 +12,7 @@ use Quantum\Session\Enums\SessionType; use Quantum\Tests\Unit\AppTestCase; use Quantum\Session\Session; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; use Quantum\Di\Di; class SessionFactoryTest extends AppTestCase diff --git a/tests/Unit/Session/SessionTest.php b/tests/Unit/Session/SessionTest.php index 56c3b787a..62e92d8ef 100644 --- a/tests/Unit/Session/SessionTest.php +++ b/tests/Unit/Session/SessionTest.php @@ -8,7 +8,7 @@ use Quantum\Session\Exceptions\SessionException; use Quantum\Tests\Unit\AppTestCase; use Quantum\Session\Session; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class SessionTest extends AppTestCase { diff --git a/tests/Unit/Validation/Traits/GeneralRuleTest.php b/tests/Unit/Validation/Traits/GeneralRuleTest.php index 27b75633e..f0d21dd97 100644 --- a/tests/Unit/Validation/Traits/GeneralRuleTest.php +++ b/tests/Unit/Validation/Traits/GeneralRuleTest.php @@ -8,7 +8,7 @@ use Quantum\Validation\Validator; use Quantum\Database\Database; use Quantum\Validation\Rule; -use Quantum\Loader\Setup; +use Quantum\Config\Setup; class GeneralRuleTest extends AppTestCase { From b6c0bc7c7180142ea8bb12b3dcd4dfb50a1e532e Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:45:42 +0400 Subject: [PATCH 03/10] [#535] Load environment bootstrap without Loader --- src/App/Stages/LoadEnvironmentStage.php | 6 +- src/Environment/Environment.php | 72 ++++++++++++---------- tests/Unit/Environment/EnvironmentTest.php | 43 +++++++++++-- 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/App/Stages/LoadEnvironmentStage.php b/src/App/Stages/LoadEnvironmentStage.php index 2dc02d967..6fd49c311 100644 --- a/src/App/Stages/LoadEnvironmentStage.php +++ b/src/App/Stages/LoadEnvironmentStage.php @@ -16,8 +16,6 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Environment\Environment; use Quantum\App\AppContext; -use Quantum\Config\Setup; -use ReflectionException; use Quantum\Di\Di; /** @@ -27,13 +25,13 @@ class LoadEnvironmentStage implements BootStageInterface { /** - * @throws EnvException|DiException|BaseException|ReflectionException + * @throws EnvException|DiException|BaseException */ public function process(AppContext $context): void { $environment = new Environment(); - $environment->load(new Setup('config', 'env')); + $environment->load(); Di::set(Environment::class, $environment); } diff --git a/src/Environment/Environment.php b/src/Environment/Environment.php index 2632cb546..2f3d4d702 100644 --- a/src/Environment/Environment.php +++ b/src/Environment/Environment.php @@ -15,12 +15,9 @@ use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; use Quantum\Environment\Enums\Env; -use Quantum\Loader\Loader; -use Quantum\Config\Setup; use ReflectionException; use Quantum\App\App; use Dotenv\Dotenv; -use Quantum\Di\Di; /** * Class Environment @@ -29,11 +26,6 @@ */ class Environment { - /** - * Environment file - */ - private string $envFile = '.env'; - private bool $isMutable = false; /** @@ -54,32 +46,20 @@ public function setMutable(bool $isMutable): Environment /** * Loads environment variables from file - * @throws EnvException|DiException|BaseException|ReflectionException + * @throws EnvException|DiException|BaseException */ - public function load(Setup $setup): void + public function load(): void { if ($this->loaded) { return; } - if (!Di::isRegistered(Loader::class)) { - Di::register(Loader::class); - } - - $envConfig = Di::get(Loader::class)->setup($setup)->load(); - - $appEnv = $envConfig['app_env'] ?? Env::PRODUCTION; - - $this->envFile = '.env' . ($appEnv !== Env::PRODUCTION ? ".$appEnv" : ''); - - if (!file_exists($this->getEnvFilePath())) { - throw EnvException::fileNotFound($this->envFile); - } - - $this->envContent = $this->loadDotenvFile(); + [$appEnv, $envFile] = $this->resolveEnvironmentFile(); + $envContent = $this->loadDotenvFile($envFile); - $this->loaded = true; $this->appEnv = $appEnv; + $this->envContent = $envContent; + $this->loaded = true; } /** @@ -117,11 +97,9 @@ public function isLocal(): bool /** * Gets the environment variable value - * @param null|mixed $default - * @return mixed * @throws EnvException */ - public function getValue(string $key, $default = null) + public function getValue(string $key, mixed $default = null): mixed { if (!$this->loaded) { throw EnvException::environmentNotLoaded(); @@ -174,7 +152,7 @@ public function updateRow(string $key, ?string $value): void $envFileContent = fs()->get($envFilePath); if (!is_string($envFileContent)) { - throw EnvException::fileNotFound($this->envFile); + throw EnvException::fileNotFound($this->getEnvFileName($this->appEnv)); } $pattern = '/^' . preg_quote($key . '=' . $this->envContent[$key], '/') . '/m'; @@ -188,18 +166,46 @@ public function updateRow(string $key, ?string $value): void $this->envContent[$key] = $value; } + /** + * @return array{string, string} + * @throws BaseException + */ + private function resolveEnvironmentFile(): array + { + $filePath = base_dir() . DS . 'shared' . DS . 'config' . DS . 'env.php'; + + if (!file_exists($filePath)) { + throw EnvException::fileNotFound($filePath); + } + + $envConfig = require $filePath; + $appEnv = $envConfig['app_env'] ?? Env::PRODUCTION; + $envFile = $this->getEnvFileName($appEnv); + + if (!file_exists(App::getBaseDir() . DS . $envFile)) { + throw EnvException::fileNotFound($envFile); + } + + return [$appEnv, $envFile]; + } + /** * @return array */ - private function loadDotenvFile(): array + private function loadDotenvFile(string $envFile): array { - $loadedVars = Dotenv::createArrayBacked(App::getBaseDir(), $this->envFile)->load(); + $loadedVars = Dotenv::createArrayBacked(App::getBaseDir(), $envFile)->load(); return is_array($loadedVars) ? $loadedVars : []; } private function getEnvFilePath(): string { - return App::getBaseDir() . DS . $this->envFile; + return App::getBaseDir() . DS . $this->getEnvFileName($this->appEnv); + } + + private function getEnvFileName(string $appEnv): string + { + return '.env' . ($appEnv !== Env::PRODUCTION ? ".$appEnv" : ''); } } diff --git a/tests/Unit/Environment/EnvironmentTest.php b/tests/Unit/Environment/EnvironmentTest.php index 29227c245..b9a13a8f1 100644 --- a/tests/Unit/Environment/EnvironmentTest.php +++ b/tests/Unit/Environment/EnvironmentTest.php @@ -2,11 +2,10 @@ namespace Quantum\Tests\Unit\Environment; -use Quantum\Loader\Exceptions\LoaderException; +use Quantum\Environment\Exceptions\EnvException; use Quantum\Environment\Environment; use Quantum\Tests\Unit\AppTestCase; use Quantum\App\AppContext; -use Quantum\Config\Setup; use Quantum\App\App; class EnvironmentTest extends AppTestCase @@ -32,9 +31,45 @@ public function testLoadReportsMissingBootstrapConfig(): void try { App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); - $this->expectException(LoaderException::class); + $this->expectException(EnvException::class); + $this->expectExceptionMessage('shared' . DS . 'config' . DS . 'env.php'); - (new Environment())->load(new Setup('config', 'env')); + (new Environment())->load(); + } finally { + App::setContext($context); + } + } + + public function testLoadIgnoresModuleBootstrapConfig(): void + { + $moduleConfig = PROJECT_ROOT . DS . 'modules' . DS . 'Test' . DS . 'config' . DS . 'env.php'; + $this->assertFileDoesNotExist($moduleConfig); + + try { + $this->createFile($moduleConfig, " 'staging'];\n"); + $this->testRequest('/'); + + $environment = new Environment(); + $environment->load(); + + $this->assertSame('testing', $environment->getAppEnv()); + } finally { + $this->removeFile($moduleConfig); + } + } + + public function testLoadDoesNotReadBootstrapAgain(): void + { + $environment = new Environment(); + $environment->load(); + $context = App::getContext(); + + try { + App::setContext(new AppContext(PROJECT_ROOT . DS . 'cron-command-tests-empty', $context->getContainer())); + + $environment->load(); + + $this->assertSame('testing', $environment->getAppEnv()); } finally { App::setContext($context); } From 7d8365ddc4013943910ee0101f40ed787c12bca1 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:56:10 +0400 Subject: [PATCH 04/10] [#535] Load helpers during App boot --- src/App/Stages/LoadHelpersStage.php | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/App/Stages/LoadHelpersStage.php b/src/App/Stages/LoadHelpersStage.php index 6c9c64fd7..6f8b78218 100644 --- a/src/App/Stages/LoadHelpersStage.php +++ b/src/App/Stages/LoadHelpersStage.php @@ -11,12 +11,8 @@ namespace Quantum\App\Stages; use Quantum\App\Contracts\BootStageInterface; -use Quantum\Di\Exceptions\DiException; use Quantum\App\AppContext; -use Quantum\Loader\Loader; -use ReflectionException; use Quantum\App\App; -use Quantum\Di\Di; /** * Class LoadHelpersStage @@ -24,23 +20,14 @@ */ class LoadHelpersStage implements BootStageInterface { - /** - * @throws DiException|ReflectionException - */ public function process(AppContext $context): void { - if (!Di::isRegistered(Loader::class)) { - Di::register(Loader::class); - } - - $loader = Di::get(Loader::class); - - $this->loadComponentHelpers($loader); - $this->loadAppHelpers($loader); - $this->loadModuleHelpers($loader); + $this->loadComponentHelpers(); + $this->loadAppHelpers(); + $this->loadModuleHelpers(); } - private function loadComponentHelpers(Loader $loader): void + private function loadComponentHelpers(): void { $srcDir = dirname(__DIR__, 2); @@ -49,18 +36,31 @@ private function loadComponentHelpers(Loader $loader): void foreach (is_array($componentDirs) ? $componentDirs : [] as $componentDir) { $helperPath = $componentDir . DS . 'Helpers'; if (is_dir($helperPath)) { - $loader->loadDir($helperPath); + $this->loadHelperFiles($helperPath); } } } - private function loadAppHelpers(Loader $loader): void + private function loadAppHelpers(): void + { + $this->loadHelperFiles(App::getBaseDir() . DS . 'helpers'); + } + + private function loadModuleHelpers(): void { - $loader->loadDir(App::getBaseDir() . DS . 'helpers'); + $this->loadHelperFiles(App::getBaseDir() . DS . 'modules' . DS . '*' . DS . 'helpers'); } - private function loadModuleHelpers(Loader $loader): void + private function loadHelperFiles(string $dir): void { - $loader->loadDir(App::getBaseDir() . DS . 'modules' . DS . '*' . DS . 'helpers'); + $files = glob($dir . DS . '*.php'); + + if (!is_array($files)) { + return; + } + + foreach ($files as $filename) { + require_once $filename; + } } } From 7636162d2670eb9063730fd2fcba9d6b36001c18 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:22:44 +0400 Subject: [PATCH 05/10] [#535] Remove Loader and migrate remaining references --- src/App/Adapters/WebAppAdapter.php | 3 +- src/App/Stages/LoadAppConfigStage.php | 3 +- src/App/Traits/WebAppTrait.php | 9 +- .../Commands/ResourceCacheClearCommand.php | 3 +- src/Hook/HookManager.php | 3 +- src/Lang/Adapters/FileAdapter.php | 4 +- src/Lang/Factories/LangFactory.php | 5 +- src/Lang/Helpers/lang.php | 11 +- src/Lang/Traits/RemoteAdapterTrait.php | 9 +- src/Loader/Exceptions/LoaderException.php | 24 --- src/Loader/Loader.php | 154 ------------------ src/Paginator/Traits/PaginatorTrait.php | 13 +- src/ResourceCache/ViewCache.php | 3 +- src/Storage/UploadedFile.php | 11 +- src/Validation/Validator.php | 14 +- tests/Unit/Loader/LoaderTest.php | 42 ----- tests/Unit/Mailer/MailerTestCase.php | 1 - 17 files changed, 31 insertions(+), 281 deletions(-) delete mode 100644 src/Loader/Exceptions/LoaderException.php delete mode 100644 src/Loader/Loader.php delete mode 100644 tests/Unit/Loader/LoaderTest.php diff --git a/src/App/Adapters/WebAppAdapter.php b/src/App/Adapters/WebAppAdapter.php index 1f629231a..90d533ad2 100644 --- a/src/App/Adapters/WebAppAdapter.php +++ b/src/App/Adapters/WebAppAdapter.php @@ -11,7 +11,6 @@ namespace Quantum\App\Adapters; use Quantum\Middleware\Exceptions\MiddlewareException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Stages\SetupErrorHandlerStage; use Quantum\Router\Exceptions\RouteException; @@ -63,7 +62,7 @@ public function __construct(AppContext $context) /** * Starts the web app - * @throws MiddlewareException|LangException|RouteException|CsrfException|ConfigException|DiException|BaseException|LoaderException|ReflectionException + * @throws MiddlewareException|LangException|RouteException|CsrfException|ConfigException|DiException|BaseException|ReflectionException */ public function start(): ?int { diff --git a/src/App/Stages/LoadAppConfigStage.php b/src/App/Stages/LoadAppConfigStage.php index fcd57d602..156b37316 100644 --- a/src/App/Stages/LoadAppConfigStage.php +++ b/src/App/Stages/LoadAppConfigStage.php @@ -12,7 +12,6 @@ use Quantum\App\Contracts\BootStageInterface; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Di\Exceptions\DiException; use Quantum\App\AppContext; use Quantum\Config\Setup; @@ -25,7 +24,7 @@ class LoadAppConfigStage implements BootStageInterface { /** - * @throws LoaderException|ConfigException|DiException|ReflectionException + * @throws ConfigException|DiException|ReflectionException */ public function process(AppContext $context): void { diff --git a/src/App/Traits/WebAppTrait.php b/src/App/Traits/WebAppTrait.php index 7ae754158..e2ee5cd92 100644 --- a/src/App/Traits/WebAppTrait.php +++ b/src/App/Traits/WebAppTrait.php @@ -11,7 +11,6 @@ namespace Quantum\App\Traits; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Router\Exceptions\RouteException; use Quantum\Lang\Exceptions\LangException; use Quantum\App\Exceptions\BaseException; @@ -54,7 +53,7 @@ private function resolveRoute(): ?MatchedRoute /** * Resolve lang config and current locale before controller hooks run. - * @throws LangException|ConfigException|LoaderException|DiException|ReflectionException + * @throws LangException|ConfigException|DiException|ReflectionException */ private function loadLanguage(): void { @@ -74,7 +73,7 @@ private function logDebugInfo(): void } /** - * @throws ConfigException|DiException|ReflectionException|LoaderException + * @throws ConfigException|DiException|ReflectionException */ private function setupViewCache(): ViewCache { @@ -92,7 +91,7 @@ private function setupViewCache(): ViewCache } /** - * @throws ConfigException|LoaderException|DiException|ReflectionException + * @throws ConfigException|DiException|ReflectionException */ private function handleCors(Response $response): void { @@ -106,7 +105,7 @@ private function handleCors(Response $response): void } /** - * @throws ConfigException|LoaderException|DiException|ReflectionException|Exception + * @throws ConfigException|DiException|ReflectionException|Exception */ private function sendResponse(Response $response): void { diff --git a/src/Console/Commands/ResourceCacheClearCommand.php b/src/Console/Commands/ResourceCacheClearCommand.php index ca0fb54e9..fca33e9c1 100644 --- a/src/Console/Commands/ResourceCacheClearCommand.php +++ b/src/Console/Commands/ResourceCacheClearCommand.php @@ -11,7 +11,6 @@ namespace Quantum\Console\Commands; use Quantum\Storage\Factories\FileSystemFactory; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; @@ -124,7 +123,7 @@ private function importModules(): void } /** - * @throws LoaderException|ConfigException|DiException|ReflectionException + * @throws ConfigException|DiException|ReflectionException */ private function importConfig(): void { diff --git a/src/Hook/HookManager.php b/src/Hook/HookManager.php index 4a9e065b3..c17a478af 100644 --- a/src/Hook/HookManager.php +++ b/src/Hook/HookManager.php @@ -11,7 +11,6 @@ namespace Quantum\Hook; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Hook\Exceptions\HookException; use Quantum\Di\Exceptions\DiException; use Quantum\Config\Setup; @@ -35,7 +34,7 @@ class HookManager private array $store = []; /** - * @throws HookException|ConfigException|DiException|LoaderException|ReflectionException + * @throws HookException|ConfigException|DiException|ReflectionException */ public function __construct() { diff --git a/src/Lang/Adapters/FileAdapter.php b/src/Lang/Adapters/FileAdapter.php index e13a21521..424b80696 100644 --- a/src/Lang/Adapters/FileAdapter.php +++ b/src/Lang/Adapters/FileAdapter.php @@ -12,7 +12,6 @@ use Quantum\Lang\Contracts\LangAdapterInterface; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Lang\Exceptions\LangException; use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; @@ -50,7 +49,7 @@ public function setLang(string $lang): LangAdapterInterface } /** - * @throws LangException|LoaderException|ConfigException|DiException|BaseException|ReflectionException + * @throws LangException|ConfigException|DiException|BaseException|ReflectionException */ public function loadTranslations(): void { @@ -84,6 +83,7 @@ public function loadTranslations(): void /** * @param array|string|null $params + * @throws LangException|ConfigException|DiException|BaseException|ReflectionException */ public function get(string $key, array|string $params = null): string { diff --git a/src/Lang/Factories/LangFactory.php b/src/Lang/Factories/LangFactory.php index 5ff4c3f5a..83d71fce5 100644 --- a/src/Lang/Factories/LangFactory.php +++ b/src/Lang/Factories/LangFactory.php @@ -12,7 +12,6 @@ use Quantum\Lang\Adapters\GoogleTranslateAdapter; use Quantum\Lang\Contracts\LangAdapterInterface; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Exceptions\ConfigException; use Quantum\Lang\Exceptions\LangException; use Quantum\App\Exceptions\BaseException; @@ -44,7 +43,7 @@ class LangFactory private array $instances = []; /** - * @throws LangException|ConfigException|LoaderException|DiException|ReflectionException + * @throws LangException|ConfigException|DiException|ReflectionException */ public static function get(?string $adapter = null): Lang { @@ -56,7 +55,7 @@ public static function get(?string $adapter = null): Lang } /** - * @throws LangException|ConfigException|DiException|ReflectionException|LoaderException|BaseException + * @throws LangException|ConfigException|DiException|ReflectionException|BaseException */ public function resolve(?string $adapter = null): Lang { diff --git a/src/Lang/Helpers/lang.php b/src/Lang/Helpers/lang.php index 9e79f89ac..f638e397b 100644 --- a/src/Lang/Helpers/lang.php +++ b/src/Lang/Helpers/lang.php @@ -7,14 +7,13 @@ */ use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Lang\Exceptions\LangException; use Quantum\Lang\Factories\LangFactory; use Quantum\Di\Exceptions\DiException; /** * Gets the current lang - * @throws LangException|LoaderException|ConfigException|DiException|ReflectionException + * @throws LangException|ConfigException|DiException|ReflectionException */ function current_lang(): ?string { @@ -24,9 +23,9 @@ function current_lang(): ?string /** * Gets translation * @param array|string|null $params - * @throws LangException|LoaderException|ConfigException|DiException|ReflectionException + * @throws LangException|ConfigException|DiException|ReflectionException */ -function t(string $key, $params = null): ?string +function t(string $key, array|string $params = null): ?string { return LangFactory::get()->getTranslation($key, $params); } @@ -34,9 +33,9 @@ function t(string $key, $params = null): ?string /** * Outputs the translation * @param array|string|null $params - * @throws LangException|LoaderException|ConfigException|DiException|ReflectionException + * @throws LangException|ConfigException|DiException|ReflectionException */ -function _t(string $key, $params = null): void +function _t(string $key, array|string $params = null): void { echo t($key, $params); } diff --git a/src/Lang/Traits/RemoteAdapterTrait.php b/src/Lang/Traits/RemoteAdapterTrait.php index 8942b09b2..ba827c1f4 100644 --- a/src/Lang/Traits/RemoteAdapterTrait.php +++ b/src/Lang/Traits/RemoteAdapterTrait.php @@ -13,7 +13,6 @@ use Quantum\HttpClient\Exceptions\HttpClientException; use Quantum\Lang\Adapters\GoogleTranslateAdapter; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Lang\Exceptions\LangException; use Quantum\App\Exceptions\BaseException; use Quantum\Lang\Adapters\DeepLAdapter; @@ -34,7 +33,7 @@ trait RemoteAdapterTrait /** * @param array|string|null $params - * @throws LangException|LoaderException|ConfigException|DiException|BaseException|ReflectionException + * @throws LangException|ConfigException|DiException|BaseException|ReflectionException */ protected function buildSourceText(string $key, array|string $params = null): string { @@ -102,11 +101,7 @@ protected function setCachedTranslation(string $adapter, string $text, string $t * @param array $headers * @param array $options * @param string $method - * @return mixed - * @throws BaseException - * @throws ErrorException - * @throws LangException - * @throws HttpClientException + * @throws LangException|HttpClientException|ErrorException|BaseException */ protected function sendRequest(string $url, $data = null, array $headers = [], array $options = [], string $method = 'POST'): mixed { diff --git a/src/Loader/Exceptions/LoaderException.php b/src/Loader/Exceptions/LoaderException.php deleted file mode 100644 index 1e91016d7..000000000 --- a/src/Loader/Exceptions/LoaderException.php +++ /dev/null @@ -1,24 +0,0 @@ -hierarchical = $setup->getHierarchy(); - $this->module = $setup->getModule(); - $this->pathPrefix = $setup->getPathPrefix(); - $this->fileName = $setup->getFilename(); - $this->exceptionMessage = $setup->getExceptionMessage(); - - return $this; - } - - /** - * Sets new value - * @param mixed $value - */ - public function set(string $property, $value): Loader - { - $this->$property = $value; - return $this; - } - - /** - * Loads .php files from given directory - */ - public function loadDir(string $dir): void - { - $files = glob($dir . DS . '*.php'); - - if (!is_array($files)) { - return; - } - - foreach ($files as $filename) { - require_once $filename; - } - } - - /** - * Loads the content - * @return mixed - * @throws LoaderException - */ - public function load() - { - return require $this->getFilePath(); - } - - /** - * Checks if the setup points to valid file - */ - public function fileExists(): bool - { - $filePath = $this->resolveFilePath(); - - if (file_exists($filePath)) { - return true; - } - - if ($this->hierarchical) { - $filePath = App::getBaseDir() . DS . 'shared' . DS . strtolower($this->pathPrefix ?? '') . DS . $this->fileName . '.php'; - return file_exists($filePath); - } - - return false; - } - - /** - * Gets the file path - * @throws LoaderException - */ - public function getFilePath(): string - { - $filePath = $this->resolveFilePath(); - - if (!file_exists($filePath)) { - if ($this->hierarchical) { - $filePath = App::getBaseDir() . DS . 'shared' . DS . strtolower($this->pathPrefix ?? '') . DS . $this->fileName . '.php'; - - if (!file_exists($filePath)) { - throw new LoaderException(_message($this->exceptionMessage, $this->fileName ?? '')); - } - } else { - throw new LoaderException(_message($this->exceptionMessage, $this->fileName ?? '')); - } - } - - return $filePath; - } - - private function resolveFilePath(): string - { - $filePath = ''; - - if ($this->module) { - $filePath = modules_dir() . DS . $this->module . DS; - } - - if ($this->pathPrefix) { - $filePath .= $this->pathPrefix . DS; - } - - return $filePath . ($this->fileName . '.php'); - } -} diff --git a/src/Paginator/Traits/PaginatorTrait.php b/src/Paginator/Traits/PaginatorTrait.php index 5bd25856d..d64fe81a8 100644 --- a/src/Paginator/Traits/PaginatorTrait.php +++ b/src/Paginator/Traits/PaginatorTrait.php @@ -11,7 +11,6 @@ namespace Quantum\Paginator\Traits; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Lang\Exceptions\LangException; use Quantum\Paginator\Enums\Pagination; use Quantum\Di\Exceptions\DiException; @@ -207,10 +206,6 @@ public function getPagination(bool $withBaseUrl = false, ?int $pageItemsCount = return implode('', $pagination); } - /** - * Get the URI for pagination - * @throws DiException|ReflectionException - */ protected function getUri(bool $withBaseUrl = false): string { $routeUrl = preg_replace('/([?&](page|per_page)=\d+)/', '', request()->getUri() ?? ''); @@ -226,10 +221,6 @@ protected function getUri(bool $withBaseUrl = false): string return $url . $delimiter; } - /** - * Get page link - * @throws DiException|ReflectionException - */ protected function getPageLink(?int $pageNumber, bool $withBaseUrl = false): ?string { if ($pageNumber !== null && $pageNumber !== 0) { @@ -241,7 +232,7 @@ protected function getPageLink(?int $pageNumber, bool $withBaseUrl = false): ?st /** * Get next page item HTML - * @throws LoaderException|ConfigException|DiException|ReflectionException|LangException + * @throws ConfigException|DiException|ReflectionException|LangException */ protected function getNextPageItem(?string $nextPageLink): string { @@ -261,7 +252,7 @@ protected function getNextPageItem(?string $nextPageLink): string */ /** * Get previous page item HTML - * @throws LoaderException|ConfigException|DiException|LangException|ReflectionException + * @throws ConfigException|DiException|LangException|ReflectionException */ protected function getPreviousPageItem(?string $previousPageLink): string { diff --git a/src/ResourceCache/ViewCache.php b/src/ResourceCache/ViewCache.php index cb1ac1b98..c7528647a 100644 --- a/src/ResourceCache/ViewCache.php +++ b/src/ResourceCache/ViewCache.php @@ -12,7 +12,6 @@ use Quantum\ResourceCache\Exceptions\ResourceCacheException; use Quantum\Storage\Factories\FileSystemFactory; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Exceptions\BaseException; use Quantum\Di\Exceptions\DiException; @@ -51,7 +50,7 @@ public function __construct() } /** - * @throws ConfigException|DiException|LoaderException|ReflectionException + * @throws ConfigException|DiException|ReflectionException */ public function setup(): void { diff --git a/src/Storage/UploadedFile.php b/src/Storage/UploadedFile.php index ac42d606b..072c36965 100644 --- a/src/Storage/UploadedFile.php +++ b/src/Storage/UploadedFile.php @@ -16,7 +16,6 @@ use Quantum\Storage\Exceptions\FileUploadException; use Quantum\Storage\Uploads\UploadConfigProvider; use Quantum\Config\Exceptions\ConfigException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Lang\Exceptions\LangException; use Quantum\Storage\Uploads\UploadStorage; use Quantum\App\Exceptions\BaseException; @@ -147,6 +146,7 @@ public function setAllowedMimeTypes(array $allowedMimeTypes, bool $merge = true) /** * Get name + * @throws ConfigException|DiException|BaseException|ReflectionException */ public function getName(): string { @@ -197,6 +197,7 @@ public function getExtension(): string /** * Get file name with extension + * @throws ConfigException|DiException|BaseException|ReflectionException */ public function getNameWithExtension(): string { @@ -312,7 +313,7 @@ public function save(string $dest, bool $overwrite = false): bool } /** - * Sets modification function on image + * Sets a modification function on an image * @param array $params * @throws FileUploadException|LangException|BaseException */ @@ -388,7 +389,7 @@ private function getLocalFileSystem(): LocalFilesystemAdapterInterface } /** - * @throws FileUploadException|LoaderException|ConfigException|DiException|ReflectionException + * @throws FileUploadException|ConfigException */ private function ensureAllowedMimeTypesLoaded(): void { @@ -404,11 +405,9 @@ private function ensureAllowedMimeTypesLoaded(): void /** * Applies modifications on image - * @param string $filePath - * @return void * @throws ImageResizeException */ - protected function applyModifications(string $filePath) + protected function applyModifications(string $filePath): void { $image = new ImageResize($filePath); $callable = [$image, $this->imageModifierFuncName ?? '']; diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index eb0269ce5..065c7a3bd 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -12,7 +12,6 @@ use Quantum\Config\Exceptions\ConfigException; use Quantum\Lang\Exceptions\LangException; -use Quantum\Loader\Exceptions\LoaderException; use Quantum\Validation\Traits\Resource; use Quantum\Di\Exceptions\DiException; use Quantum\Validation\Traits\General; @@ -189,7 +188,7 @@ public function addRule(string $rule, Closure $function): void /** * Gets validation errors with translations * @return array> - * @throws ConfigException|LoaderException|LangException|DiException|ReflectionException + * @throws ConfigException|LangException|DiException|ReflectionException */ public function getErrors(): array { @@ -215,9 +214,8 @@ public function getErrors(): array /** * Adds an error for a field and rule - * @param mixed|null $param */ - protected function addError(string $field, string $rule, $param = null): void + protected function addError(string $field, string $rule, mixed $param = null): void { if (!isset($this->errors[$field])) { $this->errors[$field] = []; @@ -236,10 +234,9 @@ public function flushErrors(): void /** * Executes user defined rule - * @param mixed $value * @param mixed ...$params */ - protected function executeCustomRule(string $rule, $value, ...$params): bool + protected function executeCustomRule(string $rule, mixed $value, ...$params): bool { $function = $this->customRules[$rule]; @@ -250,10 +247,7 @@ protected function executeCustomRule(string $rule, $value, ...$params): bool return (bool) $function($value, ...$params); } - /** - * @param mixed $ruleParam - */ - private function setOrUpdateRule(string $field, string $ruleName, $ruleParam): void + private function setOrUpdateRule(string $field, string $ruleName, mixed $ruleParam): void { if (!isset($this->rules[$field])) { $this->rules[$field] = []; diff --git a/tests/Unit/Loader/LoaderTest.php b/tests/Unit/Loader/LoaderTest.php deleted file mode 100644 index edc1c88f7..000000000 --- a/tests/Unit/Loader/LoaderTest.php +++ /dev/null @@ -1,42 +0,0 @@ -loader = new Loader(); - } - - public function testLoaderFileExists(): void - { - $this->loader->setup(new Setup('config', 'app')); - - $this->assertTrue($this->loader->fileExists()); - - $this->loader->setup(new Setup('config', 'unknown')); - - $this->assertFalse($this->loader->fileExists()); - } - - public function testLoaderSetupAndLoad(): void - { - $this->loader->setup(new Setup('config', 'app')); - - $content = $this->loader->load(); - - $this->assertEquals(base_dir() . DS . 'shared' . DS . 'config' . DS . 'app.php', $this->loader->getFilePath()); - - $this->assertIsArray($content); - } - -} diff --git a/tests/Unit/Mailer/MailerTestCase.php b/tests/Unit/Mailer/MailerTestCase.php index c40604e56..92700108f 100644 --- a/tests/Unit/Mailer/MailerTestCase.php +++ b/tests/Unit/Mailer/MailerTestCase.php @@ -11,7 +11,6 @@ abstract class MailerTestCase extends AppTestCase public function tearDown(): void { $coreDependencies = [ - \Quantum\Loader\Loader::class => \Quantum\Loader\Loader::class, \Quantum\Http\Request::class => \Quantum\Http\Request::class, \Quantum\Http\Response::class => \Quantum\Http\Response::class, ]; From 25b6f73d9199d229cf62fba16a9201d48391346b Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:53:55 +0400 Subject: [PATCH 06/10] [#535] Document Loader API migration --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ba6d2b8..cddda92fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on Keep a Changelog. - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) ### Changed +- **BREAKING:** Removed `Quantum\Loader` and moved its responsibilities to their owning packages (#535): + - Moved `Setup` to `Quantum\Config\Setup`; required config failures now use `ConfigException`, and optional imports use `Config::importIfExists()`. + - `Environment::load()` no longer accepts `Setup`, reads only `shared/config/env.php`, and reports missing files with `EnvException`. + - App boot now loads component, app, and module helpers directly. - Corrected cache adapter deserialization exception handling and `ModelCollection` key type declarations (#570) - Refactored `HttpClient` internals behind explicit `CurlAdapter` and `MultiCurlAdapter` wrappers while preserving the existing facade methods and keeping `php-curl-class` as the underlying transport for this phase (#534) - Replaced the single-request `HttpClient` `CurlAdapter` execution path with native PHP cURL (#566) From 853264e748380891ea77ce8c333527f8a0e44dd3 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:00:13 +0400 Subject: [PATCH 07/10] [#535] Resolve shared config from app base --- src/Config/Config.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config/Config.php b/src/Config/Config.php index 1f48715ff..17d626058 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -13,6 +13,7 @@ use Quantum\Config\Exceptions\ConfigException; use Quantum\Config\Contracts\ConfigInterface; use Dflydev\DotAccessData\Data; +use Quantum\App\App; /** * Class Config @@ -161,7 +162,7 @@ private function resolveFilePath(Setup $setup): ?string } if ($setup->getHierarchy()) { - $filePath = base_dir() . DS . 'shared' . DS + $filePath = App::getBaseDir() . DS . 'shared' . DS . strtolower($setup->getPathPrefix() ?? '') . DS . $setup->getFilename() . '.php'; From ddeaa02f8158968fe4796d445d33054c890524b8 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:13:39 +0400 Subject: [PATCH 08/10] [#535] Fix Redis cache TTL analysis --- src/Cache/Adapters/RedisAdapter.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cache/Adapters/RedisAdapter.php b/src/Cache/Adapters/RedisAdapter.php index 19aacd374..9fccf36df 100644 --- a/src/Cache/Adapters/RedisAdapter.php +++ b/src/Cache/Adapters/RedisAdapter.php @@ -99,10 +99,10 @@ public function has($key): bool */ public function set($key, $value, $ttl = null): bool { - return (bool) $this->redis->set( + return (bool) $this->redis->setex( $this->keyHash($key), - serialize($value), - $this->normalizeTtl($ttl) + $this->normalizeTtl($ttl), + serialize($value) ); } From 83dbd932467f8376994f54fa1bfc33b67f2e64ff Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:26:32 +0400 Subject: [PATCH 09/10] [#535] Resolve app config paths from base directory --- CHANGELOG.md | 1 + src/Config/Config.php | 2 +- tests/Unit/Config/ConfigTest.php | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cddda92fa..cddbbaa50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on Keep a Changelog. ### Changed - **BREAKING:** Removed `Quantum\Loader` and moved its responsibilities to their owning packages (#535): - Moved `Setup` to `Quantum\Config\Setup`; required config failures now use `ConfigException`, and optional imports use `Config::importIfExists()`. + - Config paths resolve from the application base directory, independent of the process working directory. - `Environment::load()` no longer accepts `Setup`, reads only `shared/config/env.php`, and reports missing files with `EnvException`. - App boot now loads component, app, and module helpers directly. - Corrected cache adapter deserialization exception handling and `ModelCollection` key type declarations (#570) diff --git a/src/Config/Config.php b/src/Config/Config.php index 17d626058..6cc2a360c 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -145,7 +145,7 @@ private function loadConfig(Setup $setup): array private function resolveFilePath(Setup $setup): ?string { - $filePath = ''; + $filePath = App::getBaseDir() . DS; if ($setup->getModule()) { $filePath = modules_dir() . DS . $setup->getModule() . DS; diff --git a/tests/Unit/Config/ConfigTest.php b/tests/Unit/Config/ConfigTest.php index 78a28e56c..6d55afa86 100644 --- a/tests/Unit/Config/ConfigTest.php +++ b/tests/Unit/Config/ConfigTest.php @@ -79,6 +79,41 @@ public function testImportFallsBackToSharedConfig(): void $this->assertSame('Quantum PHP Framework', $this->config->get('app.name')); } + public function testNonModuleConfigIsResolvedFromApplicationBase(): void + { + $filename = 'config_resolution_probe_' . uniqid(); + $applicationConfigDirectory = PROJECT_ROOT . DS . 'config'; + $applicationConfig = $applicationConfigDirectory . DS . $filename . '.php'; + $workingDirectory = sys_get_temp_dir() . DS . 'quantum-config-' . uniqid(); + $workingConfig = $workingDirectory . DS . 'config'; + $workingConfigFile = $workingConfig . DS . $filename . '.php'; + $currentDirectory = getcwd(); + $createdApplicationConfigDirectory = !is_dir($applicationConfigDirectory); + + if ($createdApplicationConfigDirectory) { + mkdir($applicationConfigDirectory, 0777, true); + } + mkdir($workingConfig, 0777, true); + $this->createFile($applicationConfig, " 'application'];\n"); + file_put_contents($workingConfigFile, " 'working directory'];\n"); + + try { + chdir($workingDirectory); + $this->config->import(new Setup('config', $filename, false)); + + $this->assertSame('application', $this->config->get($filename . '.source')); + } finally { + chdir($currentDirectory); + $this->removeFile($applicationConfig); + if ($createdApplicationConfigDirectory) { + rmdir($applicationConfigDirectory); + } + unlink($workingConfigFile); + rmdir($workingConfig); + rmdir($workingDirectory); + } + } + public function testImportIfExistsReturnsFalseWithoutChangingConfig(): void { $this->assertFalse($this->config->importIfExists(new Setup('config', 'missing_optional'))); From e51269e349e18bf6fae901eb60bafed7be15db43 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:23:24 +0400 Subject: [PATCH 10/10] [#535] Handle non-positive Redis cache TTL --- src/Cache/Adapters/RedisAdapter.php | 13 +++++++++++-- tests/Unit/Cache/Adapters/RedisAdapterTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Cache/Adapters/RedisAdapter.php b/src/Cache/Adapters/RedisAdapter.php index 9fccf36df..f134e9e52 100644 --- a/src/Cache/Adapters/RedisAdapter.php +++ b/src/Cache/Adapters/RedisAdapter.php @@ -99,9 +99,18 @@ public function has($key): bool */ public function set($key, $value, $ttl = null): bool { + $ttl = $this->normalizeTtl($ttl); + $key = $this->keyHash($key); + + if ($ttl <= 0) { + $this->redis->del($key); + + return true; + } + return (bool) $this->redis->setex( - $this->keyHash($key), - $this->normalizeTtl($ttl), + $key, + $ttl, serialize($value) ); } diff --git a/tests/Unit/Cache/Adapters/RedisAdapterTest.php b/tests/Unit/Cache/Adapters/RedisAdapterTest.php index f944ab629..5d1340d07 100644 --- a/tests/Unit/Cache/Adapters/RedisAdapterTest.php +++ b/tests/Unit/Cache/Adapters/RedisAdapterTest.php @@ -163,6 +163,17 @@ public function testRedisAdapterSetWithCustomTtl(): void $this->assertEquals('Test value', $this->redis->get('test')); } + public function testRedisAdapterSetWithNonPositiveTtlDeletesExistingKey(): void + { + foreach ([0, -1] as $ttl) { + $key = 'non_positive_ttl_' . $ttl; + $this->redis->set($key, 'Existing value'); + + $this->assertTrue($this->redis->set($key, 'Expired value', $ttl)); + $this->assertFalse($this->redis->has($key)); + } + } + public function testRedisAdapterSetWithDateIntervalTtl(): void { $this->redis->set('test', 'Test value', new DateInterval('PT60S'));