From 1f01cd2ae04ab4b676385959f0628ca7f7e4af8d Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 3 Sep 2025 15:05:32 +0100 Subject: [PATCH 1/3] Added support for generating env specific config caches --- src/Config/Repository.php | 4 +- src/Foundation/Application.php | 2 +- .../Bootstrap/LoadConfiguration.php | 12 +- src/Foundation/Console/ConfigCacheCommand.php | 118 +++++++++++++++++- src/Foundation/Console/ConfigClearCommand.php | 28 ++++- 5 files changed, 158 insertions(+), 6 deletions(-) diff --git a/src/Config/Repository.php b/src/Config/Repository.php index 719802dfe..4b66f05cf 100644 --- a/src/Config/Repository.php +++ b/src/Config/Repository.php @@ -63,10 +63,12 @@ class Repository extends BaseRepository implements ArrayAccess, RepositoryContra * @param string $environment * @return void */ - public function __construct(LoaderInterface $loader, $environment) + public function __construct(LoaderInterface $loader, $environment, array $items = []) { $this->loader = $loader; $this->environment = $environment; + + parent::__construct($items); } /** diff --git a/src/Foundation/Application.php b/src/Foundation/Application.php index ab7032994..b0942fdc0 100644 --- a/src/Foundation/Application.php +++ b/src/Foundation/Application.php @@ -503,7 +503,7 @@ public function registerCoreContainerAliases() */ public function getCachedConfigPath() { - return PathResolver::join($this->storagePath(), '/framework/config.php'); + return PathResolver::join($this->storagePath(), '/framework/' . ($this['env'] ?? 'production') . '.config.php'); } /** diff --git a/src/Foundation/Bootstrap/LoadConfiguration.php b/src/Foundation/Bootstrap/LoadConfiguration.php index 3c26852f3..30ff8f24a 100644 --- a/src/Foundation/Bootstrap/LoadConfiguration.php +++ b/src/Foundation/Bootstrap/LoadConfiguration.php @@ -19,7 +19,17 @@ public function bootstrap(Application $app): void return $this->getEnvironmentFromHost(); }); - $app->instance('config', $config = new Repository($fileLoader, $app['env'])); + $items = []; + + // First we will see if we have a cache configuration file. If we do, we'll load + // the configuration items from that file so that it is very quick. Otherwise + // we will need to spin through every configuration file and load them all. + if (empty($app['disableConfigCacheLoading']) && file_exists($cached = $app->getCachedConfigPath())) { + $items = require $cached; + $items['loadedFromCache'] = true; + } + + $app->instance('config', $config = new Repository($fileLoader, $app['env'], $items)); date_default_timezone_set($config['app.timezone']); diff --git a/src/Foundation/Console/ConfigCacheCommand.php b/src/Foundation/Console/ConfigCacheCommand.php index 14bdb04ec..1dda5c868 100644 --- a/src/Foundation/Console/ConfigCacheCommand.php +++ b/src/Foundation/Console/ConfigCacheCommand.php @@ -2,12 +2,128 @@ namespace Winter\Storm\Foundation\Console; +use Exception; +use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; use Illuminate\Foundation\Console\ConfigCacheCommand as BaseCommand; +use LogicException; +use Throwable; class ConfigCacheCommand extends BaseCommand { + /** + * @var string The console command signature. + */ + protected $signature = 'config:cache + {env? : Which environment should be cached?} + '; + + /** + * Execute the console command. + * + * @return void + * + * @throws \LogicException + */ public function handle() { - $this->components->warn('Caching configuration files is not supported in Winter CMS. See https://github.com/wintercms/winter/issues/1297#issuecomment-2624578966'); + $args = []; + if ($this->argument('env')) { + $args['env'] = $this->argument('env'); + } + + // This is the only change to the parent, it allows us to only clear the requested config + $this->callSilent('config:clear', $args); + + $config = $this->getFreshConfiguration(); + + $configPath = $this->laravel->getCachedConfigPath(); + + $this->files->put( + $configPath, 'files->delete($configPath); + + throw new LogicException('Your configuration files are not serializable.', 0, $e); + } + + $this->components->info('Configuration cached successfully.'); + } + + /** + * Boot a fresh copy of the application configuration. + * + * @return array + */ + protected function getFreshConfiguration() + { + // This allows us to detect and override the "env by subdomain" feature of Winter + if ($this->argument('env') && ($environment = $this->getEnvironmentConfiguration())) { + // Grab hosts as env => domain + $hosts = isset($environment['hosts']) + ? array_flip($environment['hosts']) + : []; + + // if we have env, set the host to the domain to "trick" the system into registering the correct config + if (isset($hosts[$this->argument('env')])) { + $_SERVER['HTTP_HOST'] = $hosts[$this->argument('env')]; + } + } + + $app = require $this->laravel->bootstrapPath() . '/app.php'; + + // This allows us to inform the LoadConfiguration class to not load from cache on fresh load + $app['disableConfigCacheLoading'] = true; + + // This overrides the new app and existing app's env + if ($this->argument('env')) { + $this->laravel->detectEnvironment(fn() => $this->argument('env') ?? $app['env']); + $app->detectEnvironment(fn() => $this->argument('env') ?? $app['env']); + } + + // Stolen stuff from the Laravel command + $app->useStoragePath($this->laravel->storagePath()); + $app->make(ConsoleKernelContract::class)->bootstrap(); + + // Force preload all registered configs + foreach ($app['config']->getNamespaces() as $namespace => $path) { + foreach (glob($path . DIRECTORY_SEPARATOR . '*.php') as $file) { + $app['config']->get($namespace . '::' . pathinfo($file, PATHINFO_FILENAME)); + } + } + + return $app['config']->all(); + } + + /** + * Load the environment configuration. + * @TODO: This is copied from LoadConfiguration, should be exposed somewhere... + * @see storm/src/Foundation/Bootstrap/LoadConfiguration.php + */ + protected function getEnvironmentConfiguration(): array + { + $config = []; + $environment = env('APP_ENV'); + if ($environment && file_exists($configPath = base_path() . '/config/' . $environment . '/environment.php')) { + try { + $config = require $configPath; + } + catch (Exception $ex) { + // + } + } + elseif (file_exists($configPath = base_path() . '/config/environment.php')) { + try { + $config = require $configPath; + } + catch (Exception $ex) { + // + } + } + + return $config; } } diff --git a/src/Foundation/Console/ConfigClearCommand.php b/src/Foundation/Console/ConfigClearCommand.php index f56002fd9..1f8ea6e8e 100644 --- a/src/Foundation/Console/ConfigClearCommand.php +++ b/src/Foundation/Console/ConfigClearCommand.php @@ -6,8 +6,32 @@ class ConfigClearCommand extends BaseCommand { - public function handle() + /** + * @var string The console command signature. + */ + protected $signature = 'config:clear + {env? : Which environment should be cleared?} + '; + + /** + * Execute the console command. + * + * @return void + */ + public function handle(): void { - $this->components->warn('Caching configuration files is not supported in Winter CMS. See https://github.com/wintercms/winter/issues/1297#issuecomment-2624578966'); + $configPath = $this->laravel->getCachedConfigPath(); + + if ($this->argument('env')) { + $configPath = realpath( + dirname($this->laravel->getCachedConfigPath()) + . DIRECTORY_SEPARATOR + . $this->argument('env') + . '.config.php' + ); + } + + $this->files->delete($configPath); + $this->components->info('Configuration cache cleared successfully.'); } } From 1291e7ff3386a08ebb2ffe0e9c67da9d074d4c72 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 3 Sep 2025 15:29:06 +0100 Subject: [PATCH 2/3] Added test case and fixed issue with overwriting app from parent --- tests/Foundation/ApplicationTest.php | 82 +++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/tests/Foundation/ApplicationTest.php b/tests/Foundation/ApplicationTest.php index f138de05a..f940a7335 100644 --- a/tests/Foundation/ApplicationTest.php +++ b/tests/Foundation/ApplicationTest.php @@ -5,31 +5,85 @@ class ApplicationTest extends TestCase { + protected Application $fakeApp; protected string $basePath; protected function setUp(): void { // Mock application $this->basePath = '/tmp/custom-path'; - $this->app = new Application($this->basePath); + $this->fakeApp = new Application($this->basePath); } public function testPathMethods() { - $this->assertEquals(PathResolver::join($this->basePath, '/plugins'), $this->app->pluginsPath()); - $this->assertEquals(PathResolver::join($this->basePath, '/themes'), $this->app->themesPath()); - $this->assertEquals(PathResolver::join($this->basePath, '/storage/temp'), $this->app->tempPath()); - $this->assertEquals(PathResolver::join($this->basePath, '/storage/app/uploads'), $this->app->uploadsPath()); - $this->assertEquals(PathResolver::join($this->basePath, '/storage/app/media'), $this->app->mediaPath()); + $this->assertEquals( + PathResolver::join($this->basePath, '/plugins'), + $this->fakeApp->pluginsPath() + ); + $this->assertEquals( + PathResolver::join($this->basePath, '/themes'), + $this->fakeApp->themesPath() + ); + $this->assertEquals( + PathResolver::join($this->basePath, '/storage/temp'), + $this->fakeApp->tempPath() + ); + $this->assertEquals( + PathResolver::join($this->basePath, '/storage/app/uploads'), + $this->fakeApp->uploadsPath() + ); + $this->assertEquals( + PathResolver::join($this->basePath, '/storage/app/media'), + $this->fakeApp->mediaPath() + ); $storagePath = $this->basePath . '/storage'; - $this->assertEquals(PathResolver::join($storagePath, '/framework/config.php'), $this->app->getCachedConfigPath()); - $this->assertEquals(PathResolver::join($storagePath, '/framework/routes.php'), $this->app->getCachedRoutesPath()); - $this->assertEquals(PathResolver::join($storagePath, '/framework/compiled.php'), $this->app->getCachedCompilePath()); - $this->assertEquals(PathResolver::join($storagePath, '/framework/services.php'), $this->app->getCachedServicesPath()); - $this->assertEquals(PathResolver::join($storagePath, '/framework/packages.php'), $this->app->getCachedPackagesPath()); - $this->assertEquals(PathResolver::join($storagePath, '/framework/classes.php'), $this->app->getCachedClassesPath()); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/production.config.php'), + $this->fakeApp->getCachedConfigPath() + ); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/routes.php'), + $this->fakeApp->getCachedRoutesPath() + ); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/compiled.php'), + $this->fakeApp->getCachedCompilePath() + ); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/services.php'), + $this->fakeApp->getCachedServicesPath() + ); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/packages.php'), + $this->fakeApp->getCachedPackagesPath() + ); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/classes.php'), + $this->fakeApp->getCachedClassesPath() + ); + } + + public function testCachedConfigPath() + { + $storagePath = $this->basePath . '/storage'; + + // No env set + $this->assertEquals( + PathResolver::join($storagePath, '/framework/production.config.php'), + $this->fakeApp->getCachedConfigPath() + ); + + // Test that setting the app env to each value results in the correct config file being returned + foreach (['test', 'prod', 'local', 'dev'] as $env) { + $this->fakeApp->detectEnvironment(fn() => $env); + $this->assertEquals( + PathResolver::join($storagePath, '/framework/' . $env . '.config.php'), + $this->fakeApp->getCachedConfigPath() + ); + } } public function testSetPathMethods() @@ -40,9 +94,9 @@ public function testSetPathMethods() $path = PathResolver::join('/my'.ucfirst($type), '/custom/path'); $expected = PathResolver::standardize($path); - $this->app->{$setter}($path); + $this->fakeApp->{$setter}($path); - $this->assertEquals($expected, $this->app->{$getter}()); + $this->assertEquals($expected, $this->fakeApp->{$getter}()); } } } From d2ca28dc9a9e7cc675e04e06408684d76d4e37e5 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 3 Sep 2025 15:30:21 +0100 Subject: [PATCH 3/3] Added fix for laravel style code --- src/Foundation/Console/ConfigCacheCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Foundation/Console/ConfigCacheCommand.php b/src/Foundation/Console/ConfigCacheCommand.php index 1dda5c868..a04d112be 100644 --- a/src/Foundation/Console/ConfigCacheCommand.php +++ b/src/Foundation/Console/ConfigCacheCommand.php @@ -39,7 +39,8 @@ public function handle() $configPath = $this->laravel->getCachedConfigPath(); $this->files->put( - $configPath, '