diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index 241399a12035..d1363707667d 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -13,9 +13,18 @@ namespace CodeIgniter\Cache\FactoriesCache; -final class FileVarExportHandler +final readonly class FileVarExportHandler { - private string $path = WRITEPATH . 'cache'; + private string $path; + + /** + * @param string|null $path The directory where the cache files are stored. + * Defaults to the "writable/cache" directory. + */ + public function __construct(?string $path = null) + { + $this->path = rtrim($path ?? WRITEPATH . 'cache', '/'); + } public function save(string $key, mixed $val): void { diff --git a/tests/_support/Cache/IsolatedCacheTrait.php b/tests/_support/Cache/IsolatedCacheTrait.php new file mode 100644 index 000000000000..19ee60f55ae9 --- /dev/null +++ b/tests/_support/Cache/IsolatedCacheTrait.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Cache; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; + +/** + * Gives the test class its own directory for the files created + * by the FileVarExportHandler. + * + * The handler stores its files in the shared "writable/cache" directory by + * default. Since the random test execution runs the components in parallel, + * one component may delete the cache file that another component is + * asserting on. A dedicated directory per test class avoids that. + */ +trait IsolatedCacheTrait +{ + /** + * The directory used to store the cache files of the test class. + */ + private static string $isolatedCachePath; + + public static function setUpBeforeClassIsolatedCache(): void + { + helper('filesystem'); + + self::$isolatedCachePath = WRITEPATH . 'cache_test_' . str_replace('\\', '_', static::class); + + // Remove the files left over by a previous run. + if (is_dir(self::$isolatedCachePath)) { + delete_files(self::$isolatedCachePath, true); + @rmdir(self::$isolatedCachePath); + } + } + + public static function tearDownAfterClassIsolatedCache(): void + { + if (! isset(self::$isolatedCachePath) || ! is_dir(self::$isolatedCachePath)) { + return; + } + + delete_files(self::$isolatedCachePath, true); + @rmdir(self::$isolatedCachePath); + } + + /** + * Returns a handler that stores its files in the isolated directory. + */ + protected function getIsolatedCacheHandler(): FileVarExportHandler + { + return new FileVarExportHandler(self::$isolatedCachePath); + } + + /** + * Returns the path of the isolated cache directory. + */ + protected static function getIsolatedCachePath(): string + { + return self::$isolatedCachePath; + } +} diff --git a/tests/system/Autoloader/FileLocatorCachedTest.php b/tests/system/Autoloader/FileLocatorCachedTest.php index 9700f88bdb33..1fe97b834dd3 100644 --- a/tests/system/Autoloader/FileLocatorCachedTest.php +++ b/tests/system/Autoloader/FileLocatorCachedTest.php @@ -17,6 +17,7 @@ use Config\Autoload; use Config\Modules; use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Cache\IsolatedCacheTrait; /** * @internal @@ -24,18 +25,29 @@ #[Group('Others')] final class FileLocatorCachedTest extends FileLocatorTest { + use IsolatedCacheTrait; + private FileVarExportHandler $handler; - public static function tearDownAfterClass(): void + public static function setUpBeforeClass(): void { - parent::tearDownAfterClass(); + parent::setUpBeforeClass(); + self::setUpBeforeClassIsolatedCache(); + } + + public static function tearDownAfterClass(): void + { // Delete cache file. $autoloader = new Autoloader(); - $handler = new FileVarExportHandler(); + $handler = new FileVarExportHandler(self::getIsolatedCachePath()); $fileLocator = new FileLocator($autoloader); $locator = new FileLocatorCached($fileLocator, $handler); $locator->deleteCache(); + + self::tearDownAfterClassIsolatedCache(); + + parent::tearDownAfterClass(); } protected function setUp(): void @@ -61,7 +73,7 @@ protected function setUp(): void 'Acme\Sample' => TESTPATH . '_support/does/not/exists', ]); - $this->handler = new FileVarExportHandler(); + $this->handler = $this->getIsolatedCacheHandler(); $fileLocator = new FileLocator($autoloader); $this->locator = new FileLocatorCached($fileLocator, $this->handler); } @@ -70,6 +82,10 @@ protected function tearDown(): void { $this->locator->__destruct(); + // Prevent the destructor from writing the cache file + // when the test object is destroyed. + $this->locator->deleteCache(); + parent::tearDown(); } diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php index e09816dba2fa..a6103350283b 100644 --- a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Cache\IsolatedCacheTrait; /** * @internal @@ -22,27 +23,44 @@ #[Group('Others')] final class FactoriesCacheFileVarExportHandlerTest extends AbstractFactoriesCacheHandlerTestCase { + use IsolatedCacheTrait; + public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); + self::setUpBeforeClassIsolatedCache(); + helper('filesystem'); } + public static function tearDownAfterClass(): void + { + self::tearDownAfterClassIsolatedCache(); + + parent::tearDownAfterClass(); + } + protected function createFactoriesCache(): void { - $this->handler = new FileVarExportHandler(); + $this->handler = $this->getIsolatedCacheHandler(); $this->cache = new FactoriesCache($this->handler); } + public function testUsesTheWritableCacheDirectoryByDefault(): void + { + $handler = new FileVarExportHandler(); + + $this->assertSame(WRITEPATH . 'cache', $this->getPrivateProperty($handler, 'path')); + } + public function testSaveCreatesDirectoryAndFileWithCorrectPermissions(): void { $dir = WRITEPATH . 'cache_test_dir_' . uniqid('', true); $oldUmask = umask(0000); try { - $handler = new FileVarExportHandler(); - $this->setPrivateProperty($handler, 'path', $dir); + $handler = new FileVarExportHandler($dir); $handler->save('test_key', ['data']); diff --git a/tests/system/Commands/Cache/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php index 6674fff5a2c8..ca93f215bc13 100644 --- a/tests/system/Commands/Cache/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -16,7 +16,6 @@ use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Autoloader\FileLocatorCached; use CodeIgniter\Cache\CacheFactory; -use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\CLI\CLI; use CodeIgniter\Config\Factories; @@ -25,6 +24,7 @@ use Config\Cache; use Config\Services; use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Cache\IsolatedCacheTrait; /** * @internal @@ -32,10 +32,25 @@ #[Group('Others')] final class ClearCacheTest extends CIUnitTestCase { + use IsolatedCacheTrait; use StreamFilterTrait; private Cache $config; + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + self::setUpBeforeClassIsolatedCache(); + } + + public static function tearDownAfterClass(): void + { + self::tearDownAfterClassIsolatedCache(); + + parent::tearDownAfterClass(); + } + protected function setUp(): void { parent::setUp(); @@ -98,7 +113,7 @@ public function testClearCacheWorks(): void public function testClearCacheDiscardsSharedLocatorCache(): void { - $handler = new FileVarExportHandler(); + $handler = $this->getIsolatedCacheHandler(); $handler->delete('FileLocatorCache'); $locator = new FileLocatorCached(new FileLocator(service('autoloader')), $handler); @@ -117,6 +132,10 @@ public function testClearCacheDiscardsSharedLocatorCache(): void $this->assertArrayHasKey('Config/Cache', $cached['search']); $handler->delete('FileLocatorCache'); + + // Prevent the destructor from writing the cache file + // when the locator is destroyed. + $locator->deleteCache(); } public function testClearCacheFails(): void diff --git a/tests/system/Commands/Utilities/OptimizeTest.php b/tests/system/Commands/Utilities/OptimizeTest.php index ccaf9403c613..03fe20d2d3e7 100644 --- a/tests/system/Commands/Utilities/OptimizeTest.php +++ b/tests/system/Commands/Utilities/OptimizeTest.php @@ -22,6 +22,7 @@ use CodeIgniter\Test\StreamFilterTrait; use Config\Services; use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Cache\IsolatedCacheTrait; /** * @internal @@ -29,16 +30,31 @@ #[Group('Others')] final class OptimizeTest extends CIUnitTestCase { + use IsolatedCacheTrait; use ReflectionHelper; use StreamFilterTrait; private FileVarExportHandler $handler; + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + self::setUpBeforeClassIsolatedCache(); + } + + public static function tearDownAfterClass(): void + { + self::tearDownAfterClassIsolatedCache(); + + parent::tearDownAfterClass(); + } + protected function setUp(): void { parent::setUp(); - $this->handler = new FileVarExportHandler(); + $this->handler = $this->getIsolatedCacheHandler(); $this->handler->delete('FileLocatorCache'); } @@ -74,15 +90,52 @@ public function testClearCacheDiscardsSharedLocatorCache(): void $this->assertArrayNotHasKey('Config/App', $cached['search']); $this->assertArrayHasKey('Config/Cache', $cached['search']); $this->assertStringContainsString('Removed FileLocatorCache.', $this->getStreamFilterBuffer()); + + // Prevent the destructor from writing the cache file + // when the locator is destroyed. + $locator->deleteCache(); } public function testClearCacheDeletesTheFileWithoutSharedLocatorCache(): void { - $this->handler->save('FileLocatorCache', ['search' => []]); - - ($this->getClearCache())(); - - $this->assertFalse($this->handler->get('FileLocatorCache')); - $this->assertStringContainsString('Removed FileLocatorCache.', $this->getStreamFilterBuffer()); + $writePath = rtrim(sys_get_temp_dir(), '\\/') . DIRECTORY_SEPARATOR . 'ci_test_optimize_' . uniqid('', true) . DIRECTORY_SEPARATOR; + mkdir($writePath . 'cache', 0755, true); + + $escapedWritePath = var_export($writePath, true); + $escapedBootstrap = var_export(SYSTEMPATH . 'Test/bootstrap.php', true); + + try { + $code = <<save('FileLocatorCache', ['search' => []]); + + \$clearCache = ReflectionHelper::getPrivateMethodInvoker( + new Optimize(service('logger'), service('commands')), + 'clearCache' + ); + \$clearCache(); + + if (\$handler->get('FileLocatorCache') !== false) { + exit(1); + } + PHP; + + $cmd = sprintf('%s -r %s 2>&1', escapeshellcmd(PHP_BINARY), escapeshellarg($code)); + exec($cmd, $output, $status); + + $this->assertSame(0, $status, implode(PHP_EOL, $output)); + $this->assertStringContainsString('Removed FileLocatorCache.', implode(PHP_EOL, $output)); + } finally { + helper('filesystem'); + delete_files($writePath, true); + @rmdir($writePath); + } } } diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 8739e9546d4c..5fcc0901f365 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -24,6 +24,10 @@ Message Changes Changes ******* +- **Cache:** Added an optional ``$path`` parameter to + ``FileVarExportHandler::__construct()`` so the cache files can be stored in + a custom directory. + ************ Deprecations ************