diff --git a/psalm-baseline.xml b/psalm-baseline.xml index dcef743f607f..3ede420d002c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,10 @@ + + + $val + + Memcache diff --git a/public/index.php b/public/index.php index d031ea10944a..1cc4710549d5 100644 --- a/public/index.php +++ b/public/index.php @@ -43,6 +43,16 @@ require_once SYSTEMPATH . 'Config/DotEnv.php'; (new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); +// Define ENVIRONMENT +if (! defined('ENVIRONMENT')) { + define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); +} + +// Load Config Cache +// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); +// $factoriesCache->load('config'); +// ^^^ Uncomment these lines if you want to use Config Caching. + /* * --------------------------------------------------------------- * GRAB OUR CODEIGNITER INSTANCE @@ -68,6 +78,10 @@ $app->run(); +// Save Config Cache +// $factoriesCache->save('config'); +// ^^^ Uncomment this line if you want to use Config Caching. + // Exits the application, setting the exit code for CLI-based applications // that might be watching. exit(EXIT_SUCCESS); diff --git a/spark b/spark index f2ba3f305ceb..2ea79d5ccdaf 100755 --- a/spark +++ b/spark @@ -78,6 +78,11 @@ require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstra require_once SYSTEMPATH . 'Config/DotEnv.php'; (new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); +// Define ENVIRONMENT +if (! defined('ENVIRONMENT')) { + define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production')); +} + // Grab our CodeIgniter $app = Config\Services::codeigniter(); $app->initialize(); diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php new file mode 100644 index 000000000000..d78d0b1b02a4 --- /dev/null +++ b/system/Cache/FactoriesCache.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; + +final class FactoriesCache +{ + /** + * @var CacheInterface|FileVarExportHandler + */ + private $cache; + + /** + * @param CacheInterface|FileVarExportHandler|null $cache + */ + public function __construct($cache = null) + { + $this->cache = $cache ?? new FileVarExportHandler(); + } + + public function save(string $component): void + { + if (! Factories::isUpdated($component)) { + return; + } + + $data = Factories::getComponentInstances($component); + + $this->cache->save($this->getCacheKey($component), $data, 3600 * 24); + } + + private function getCacheKey(string $component): string + { + return 'FactoriesCache_' . $component; + } + + public function load(string $component): bool + { + $key = $this->getCacheKey($component); + + if (! $data = $this->cache->get($key)) { + return false; + } + + Factories::setComponentInstances($component, $data); + + return true; + } + + public function delete(string $component): void + { + $this->cache->delete($this->getCacheKey($component)); + } +} diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php new file mode 100644 index 000000000000..f7cee5ef6248 --- /dev/null +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache\FactoriesCache; + +final class FileVarExportHandler +{ + private string $path = WRITEPATH . 'cache'; + + /** + * @param array|bool|float|int|object|string|null $val + */ + public function save(string $key, $val): void + { + $val = var_export($val, true); + + // Write to temp file first to ensure atomicity + $tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp'; + file_put_contents($tmp, 'path . "/{$key}"); + } + + public function delete(string $key): void + { + @unlink($this->path . "/{$key}"); + } + + /** + * @return array|bool|float|int|object|string|null + */ + public function get(string $key) + { + return @include $this->path . "/{$key}"; + } +} diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 8bdcc15bc16e..3664c0321fa2 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -200,7 +200,6 @@ public function __construct(App $config) public function initialize() { // Define environment variables - $this->detectEnvironment(); $this->bootstrapEnvironment(); // Setup Exception Handling @@ -560,6 +559,8 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache * production * * @codeCoverageIgnore + * + * @deprecated 4.4.0 No longer used. Moved to index.php and spark. */ protected function detectEnvironment() { diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 0958d7ae7f75..d2c396dc36ba 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -26,6 +26,8 @@ * from the environment. * * These can be set within the .env file. + * + * @phpstan-consistent-constructor */ class BaseConfig { @@ -37,6 +39,11 @@ class BaseConfig */ public static $registrars = []; + /** + * Whether to override properties by Env vars and Registrars. + */ + public static bool $override = true; + /** * Has module discovery happened yet? * @@ -51,6 +58,21 @@ class BaseConfig */ protected static $moduleConfig; + public static function __set_state(array $array) + { + static::$override = false; + $obj = new static(); + static::$override = true; + + $properties = array_keys(get_object_vars($obj)); + + foreach ($properties as $property) { + $obj->{$property} = $array[$property]; + } + + return $obj; + } + /** * Will attempt to get environment variables with names * that match the properties of the child class. @@ -61,6 +83,10 @@ public function __construct() { static::$moduleConfig = config(Modules::class); + if (! static::$override) { + return; + } + $this->registerProperties(); $properties = array_keys(get_object_vars($this)); diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 5bf7afebd431..0eb6d2443e44 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -80,6 +80,15 @@ class Factories */ protected static $instances = []; + /** + * Whether the component instances are updated? + * + * @var array [component => true] + * + * @internal For caching only + */ + protected static $updated = []; + /** * Define the class to load. You can *override* the concrete class. * @@ -153,6 +162,7 @@ public static function __callStatic(string $component, array $arguments) self::$instances[$options['component']][$class] = new $class(...$arguments); self::$aliases[$options['component']][$alias] = $class; + self::$updated[$options['component']] = true; // If a short classname is specified, also register FQCN to share the instance. if (! isset(self::$aliases[$options['component']][$class])) { @@ -383,7 +393,8 @@ public static function reset(?string $component = null) unset( static::$options[$component], static::$aliases[$component], - static::$instances[$component] + static::$instances[$component], + static::$updated[$component] ); return; @@ -392,6 +403,7 @@ public static function reset(?string $component = null) static::$options = []; static::$aliases = []; static::$instances = []; + static::$updated = []; } /** @@ -440,4 +452,46 @@ public static function getBasename(string $alias): string return $alias; } + + /** + * Gets component data for caching. + * + * @internal For caching only + */ + public static function getComponentInstances(string $component): array + { + if (! isset(static::$aliases[$component])) { + return [ + 'aliases' => [], + 'instances' => [], + ]; + } + + return [ + 'aliases' => static::$aliases[$component], + 'instances' => self::$instances[$component], + ]; + } + + /** + * Sets component data + * + * @internal For caching only + */ + public static function setComponentInstances(string $component, array $data): void + { + static::$aliases[$component] = $data['aliases']; + self::$instances[$component] = $data['instances']; + unset(self::$updated[$component]); + } + + /** + * Whether the component instances are updated? + * + * @internal For caching only + */ + public static function isUpdated(string $component): bool + { + return isset(self::$updated[$component]); + } } diff --git a/system/Config/Services.php b/system/Config/Services.php index ffca4fa804ff..6bd16cc30213 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -117,7 +117,7 @@ public static function cache(?Cache $config = null, bool $getShared = true) return static::getSharedInstance('cache', $config); } - $config ??= new Cache(); + $config ??= config(Cache::class); return CacheFactory::getHandler($config); } diff --git a/system/HTTP/UserAgent.php b/system/HTTP/UserAgent.php index 4b579229d88f..1ccc2109c119 100644 --- a/system/HTTP/UserAgent.php +++ b/system/HTTP/UserAgent.php @@ -102,7 +102,7 @@ class UserAgent */ public function __construct(?UserAgents $config = null) { - $this->config = $config ?? new UserAgents(); + $this->config = $config ?? config(UserAgents::class); if (isset($_SERVER['HTTP_USER_AGENT'])) { $this->agent = trim($_SERVER['HTTP_USER_AGENT']); diff --git a/system/Modules/Modules.php b/system/Modules/Modules.php index ade3e693ceea..f99e7215e733 100644 --- a/system/Modules/Modules.php +++ b/system/Modules/Modules.php @@ -15,6 +15,8 @@ * Modules Class * * @see https://codeigniter.com/user_guide/general/modules.html + * + * @phpstan-consistent-constructor */ class Modules { @@ -39,6 +41,11 @@ class Modules */ public $aliases = []; + public function __construct() + { + // For @phpstan-consistent-constructor + } + /** * Should the application auto-discover the requested resource. */ @@ -50,4 +57,17 @@ public function shouldDiscover(string $alias): bool return in_array(strtolower($alias), $this->aliases, true); } + + public static function __set_state(array $array) + { + $obj = new static(); + + $properties = array_keys(get_object_vars($obj)); + + foreach ($properties as $property) { + $obj->{$property} = $array[$property]; + } + + return $obj; + } } diff --git a/tests/system/Cache/FactoriesCacheFileHandlerTest.php b/tests/system/Cache/FactoriesCacheFileHandlerTest.php new file mode 100644 index 000000000000..b5434bbb1b1b --- /dev/null +++ b/tests/system/Cache/FactoriesCacheFileHandlerTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use Config\Cache as CacheConfig; + +/** + * @internal + * + * @group Others + */ +final class FactoriesCacheFileHandlerTest extends FactoriesCacheFileVarExportHandlerTest +{ + /** + * @var @var FileVarExportHandler|CacheInterface + */ + protected $handler; + + protected function createFactoriesCache(): void + { + $this->handler = CacheFactory::getHandler(new CacheConfig(), 'file'); + $this->cache = new FactoriesCache($this->handler); + } +} diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php new file mode 100644 index 000000000000..a3aff35b000e --- /dev/null +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; +use Config\Modules; + +/** + * @internal + * @no-final + * + * @group Others + */ +class FactoriesCacheFileVarExportHandlerTest extends CIUnitTestCase +{ + protected FactoriesCache $cache; + + /** + * @var CacheInterface|FileVarExportHandler + */ + protected $handler; + + protected function createFactoriesCache(): void + { + $this->handler = new FileVarExportHandler(); + $this->cache = new FactoriesCache($this->handler); + } + + public function testInstantiate() + { + $this->createFactoriesCache(); + + $this->assertInstanceOf(FactoriesCache::class, $this->cache); + } + + public function testSave() + { + Factories::reset(); + Factories::config('App'); + + $this->createFactoriesCache(); + + $this->cache->save('config'); + + $cachedData = $this->handler->get('FactoriesCache_config'); + + $this->assertArrayHasKey('aliases', $cachedData); + $this->assertArrayHasKey('instances', $cachedData); + $this->assertArrayHasKey(Modules::class, $cachedData['aliases']); + $this->assertArrayHasKey('App', $cachedData['aliases']); + } + + public function testLoad() + { + Factories::reset(); + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; + + $this->createFactoriesCache(); + $this->cache->save('config'); + + Factories::reset(); + + $this->cache->load('config'); + + $appConfig = Factories::config('App'); + $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); + } + + public function testDelete() + { + $this->createFactoriesCache(); + + $this->cache->delete('config'); + + $this->assertFalse($this->cache->load('config')); + } +} diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 3750113bbf73..c5936a7c341f 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Config; use CodeIgniter\Test\CIUnitTestCase; +use Config\Database; use InvalidArgumentException; use ReflectionClass; use stdClass; @@ -398,4 +399,59 @@ public function testDefineAndLoad() $this->assertInstanceOf(EntityModel::class, $model); } + + public function testGetComponentInstances() + { + Factories::config('App'); + Factories::config(Database::class); + + $data = Factories::getComponentInstances('config'); + + $this->assertIsArray($data); + $this->assertArrayHasKey('aliases', $data); + $this->assertArrayHasKey('instances', $data); + + return $data; + } + + /** + * @depends testGetComponentInstances + */ + public function testSetComponentInstances(array $data) + { + $before = Factories::getComponentInstances('config'); + $this->assertSame(['aliases' => [], 'instances' => []], $before); + + Factories::setComponentInstances('config', $data); + + $data = Factories::getComponentInstances('config'); + + $this->assertIsArray($data); + $this->assertArrayHasKey('aliases', $data); + $this->assertArrayHasKey('instances', $data); + + return $data; + } + + /** + * @depends testSetComponentInstances + */ + public function testIsUpdated(array $data) + { + Factories::reset(); + + $updated = $this->getFactoriesStaticProperty('updated'); + + $this->assertSame([], $updated); + $this->assertFalse(Factories::isUpdated('config')); + + Factories::config('App'); + + $this->assertTrue(Factories::isUpdated('config')); + $this->assertFalse(Factories::isUpdated('models')); + + Factories::setComponentInstances('config', $data); + + $this->assertFalse(Factories::isUpdated('config')); + } } diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 4b7b065cd6e2..2be5278fe786 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -222,8 +222,10 @@ Others - It can also take an object that implements ``ResponseInterface`` as its first argument. - It implements ``ResponsableInterface``. - **DebugBar:** Now :ref:`view-routes` are displayed in *DEFINED ROUTES* on the *Routes* tab. -- **Factories:** You can now define the classname that will actually be loaded. - See :ref:`factories-defining-classname-to-be-loaded`. +- **Factories:** + - You can now define the classname that will actually be loaded. + See :ref:`factories-defining-classname-to-be-loaded`. + - Config Caching implemented. See :ref:`factories-config-caching` for details. Message Changes *************** diff --git a/user_guide_src/source/concepts/factories.rst b/user_guide_src/source/concepts/factories.rst index 881f0fa1b1f1..f4872327f082 100644 --- a/user_guide_src/source/concepts/factories.rst +++ b/user_guide_src/source/concepts/factories.rst @@ -262,3 +262,81 @@ that single call will return a new or shared instance: .. literalinclude:: factories/007.php :lines: 2- + +.. _factories-config-caching: + +Config Caching +************** + +.. versionadded:: 4.4.0 + +To improve performance, Config Caching has been implemented. + +Prerequisite +============ + +.. important:: Using this feature when the prerequisites are not met will prevent + CodeIgniter from operating properly. Do not use this feature in such cases. + +- To use this feature, the properties of all Config objects instantiated in + Factories must not be modified after instantiation. Put another way, the Config + classes must be an immutable or readonly classes. +- By default, every Config class that is cached must implement ``__set_state()`` + method. + +How It Works +============ + +- Save the all Config instances in Factories into a cache file before shutdown, + if the state of the Config instances in Factories changes. +- Restore cached Config instances before CodeIgniter initialization if a cache + is available. + +Simply put, all Config instances held by Factories are cached immediately prior +to shutdown, and the cached instances are used permanently. + +How to Update Config Values +=========================== + +Once stored, the cached versions never expire. Changing a existing Config file +(or changing Environment Variables for it) will not update the cache nor the Config +values. + +So if you want to update Config values, update Config files or Environment Variables +for them, and you must manually delete the cache file. + +You can use the ``spark cache:clear`` command: + +.. code-block:: console + + php spark cache:clear + +Or simply delete the **writable/cache/FactoriesCache_config** file. + +How to Enable Config Caching +============================ + +Uncomment the following code in **public/index.php**:: + + --- a/public/index.php + +++ b/public/index.php + @@ -49,8 +49,8 @@ if (! defined('ENVIRONMENT')) { + } + + // Load Config Cache + -// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + -// $factoriesCache->load('config'); + +$factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + +$factoriesCache->load('config'); + // ^^^ Uncomment these lines if you want to use Config Caching. + + /* + @@ -79,7 +79,7 @@ $app->setContext($context); + $app->run(); + + // Save Config Cache + -// $factoriesCache->save('config'); + +$factoriesCache->save('config'); + // ^^^ Uncomment this line if you want to use Config Caching. + + // Exits the application, setting the exit code for CLI-based applications diff --git a/user_guide_src/source/installation/upgrade_440.rst b/user_guide_src/source/installation/upgrade_440.rst index ffb8751c5206..1d7eeca2d9f8 100644 --- a/user_guide_src/source/installation/upgrade_440.rst +++ b/user_guide_src/source/installation/upgrade_440.rst @@ -117,15 +117,16 @@ match the new array structure. Mandatory File Changes ********************** -index.php -========= +index.php and spark +=================== -The following file received significant changes and +The following files received significant changes and **you must merge the updated versions** with your application: - ``public/index.php`` (see also :ref:`v440-codeigniter-and-exit`) +- ``spark`` -.. important:: If you don't update the above file, CodeIgniter will not work +.. important:: If you don't update the above files, CodeIgniter will not work properly after running ``composer update``. The upgrade procedure, for example, is as follows: @@ -134,6 +135,7 @@ The following file received significant changes and composer update cp vendor/codeigniter4/framework/public/index.php public/index.php + cp vendor/codeigniter4/framework/spark spark Config Files ============