PHP Version
8.5
CodeIgniter4 Version
4.7.4
CodeIgniter4 Installation Method
Manual (zip or tar.gz)
Which operating systems have you tested for this bug?
macOS, Linux
Which server did you use?
cli
Environment
production
Database
Not relevant to this bug.
What happened?
FileLocatorCached::saveCache() passes a 24-hour TTL to the cache handler:
// system/Autoloader/FileLocatorCached.php:75-80 (v4.7.4)
private function saveCache(): void
{
if ($this->cacheUpdated) {
$this->cacheHandler->save($this->cacheKey, $this->cache, 3600 * 24);
}
}
But FileVarExportHandler::save() declares only two parameters:
// system/Cache/FactoriesCache/FileVarExportHandler.php:20 (v4.7.4)
public function save(string $key, mixed $val): void
PHP silently discards an extra argument passed to a non-variadic userland function, so the
TTL never reaches the handler — no error, no warning, no log entry. Nothing about expiry is
written to the cache file, and get() is a bare include with no expiry check at all:
// system/Cache/FactoriesCache/FileVarExportHandler.php:53-56 (v4.7.4)
public function get(string $key): mixed
{
return @include $this->path . "/{$key}";
}
The result is that the locator cache never expires, while the calling code states that it
should expire after 24 hours. A file discovered by the locator — for instance a
Config/Registrar.php shipped by a package or added by the application — stays invisible for
as long as the cache file exists on disk, not for the 24 hours the call site intends.
Both files are identical in v4.7.3, v4.7.4 and on the default branch at the time of writing.
Steps to Reproduce
- Enable the locator cache:
php spark optimize, which sets $locatorCacheEnabled = true
in app/Config/Optimize.php.
- Run any command or request, so that the cache file is written to
writable/cache/FactoriesCache_FileLocatorCache.
- Open that file. It contains
<?php return [ ... ]; and nothing else: no timestamp, no
TTL, no field that could ever make it expire.
- Add a new file that the locator is meant to discover — for example a
Config/Registrar.php in a namespace registered in Config\Autoload.
- Run the application again. The new file is not discovered, and it stays undiscovered
indefinitely: waiting more than 24 hours changes nothing, because the 24 hours were
never recorded anywhere.
Expected Output
Either of the two would be consistent; today neither holds:
- the TTL passed by
FileLocatorCached::saveCache() is honoured, so the entry expires after
24 hours and is rebuilt on the next run; or
FileVarExportHandler does not support a TTL, and the caller stops passing one — ideally
with the handler's contract saying so, so that the next caller is not misled either.
What makes this hard to notice is that the call site says "24 hours", the storage says
"forever", and nothing in between reports the disagreement.
Anything else?
A related detail we ran into, in case it is useful when fixing this: deleting the cache
entry does not necessarily survive the request. FileLocatorCached::deleteCache()
(system/Autoloader/FileLocatorCached.php:85-88) removes the file and sets
$this->cacheUpdated = false, but any locator lookup afterwards in the same process sets
$this->cacheUpdated = true again, and __destruct() (:70-73) then writes the old
in-memory state back to disk. So a "clear the cache, then continue working" sequence inside
one process can end with the stale file restored.
We hit the original bug in a deployment: a Config/Registrar.php added to the disk after the
cache had been warmed stayed invisible, and every route protected by the filter aliases it
declares answered HTTP 500. Our workaround is to delete the cache file from a separate
process (a plain rm -f in the deploy script) rather than from PHP, which is a workaround
for the second detail above rather than for the missing TTL.
This replaces #10537, opened by the same account and closed automatically by
mergeable[bot] because it did not follow this template. It is the same defect, refiled in
the required form: the account cannot reopen the original issue, so this is a new one rather
than a duplicate report.
PHP Version
8.5
CodeIgniter4 Version
4.7.4
CodeIgniter4 Installation Method
Manual (zip or tar.gz)
Which operating systems have you tested for this bug?
macOS, Linux
Which server did you use?
cli
Environment
production
Database
Not relevant to this bug.
What happened?
FileLocatorCached::saveCache()passes a 24-hour TTL to the cache handler:But
FileVarExportHandler::save()declares only two parameters:PHP silently discards an extra argument passed to a non-variadic userland function, so the
TTL never reaches the handler — no error, no warning, no log entry. Nothing about expiry is
written to the cache file, and
get()is a bare include with no expiry check at all:The result is that the locator cache never expires, while the calling code states that it
should expire after 24 hours. A file discovered by the locator — for instance a
Config/Registrar.phpshipped by a package or added by the application — stays invisible foras long as the cache file exists on disk, not for the 24 hours the call site intends.
Both files are identical in v4.7.3, v4.7.4 and on the default branch at the time of writing.
Steps to Reproduce
php spark optimize, which sets$locatorCacheEnabled = truein
app/Config/Optimize.php.writable/cache/FactoriesCache_FileLocatorCache.<?php return [ ... ];and nothing else: no timestamp, noTTL, no field that could ever make it expire.
Config/Registrar.phpin a namespace registered inConfig\Autoload.indefinitely: waiting more than 24 hours changes nothing, because the 24 hours were
never recorded anywhere.
Expected Output
Either of the two would be consistent; today neither holds:
FileLocatorCached::saveCache()is honoured, so the entry expires after24 hours and is rebuilt on the next run; or
FileVarExportHandlerdoes not support a TTL, and the caller stops passing one — ideallywith the handler's contract saying so, so that the next caller is not misled either.
What makes this hard to notice is that the call site says "24 hours", the storage says
"forever", and nothing in between reports the disagreement.
Anything else?
A related detail we ran into, in case it is useful when fixing this: deleting the cache
entry does not necessarily survive the request.
FileLocatorCached::deleteCache()(
system/Autoloader/FileLocatorCached.php:85-88) removes the file and sets$this->cacheUpdated = false, but any locator lookup afterwards in the same process sets$this->cacheUpdated = trueagain, and__destruct()(:70-73) then writes the oldin-memory state back to disk. So a "clear the cache, then continue working" sequence inside
one process can end with the stale file restored.
We hit the original bug in a deployment: a
Config/Registrar.phpadded to the disk after thecache had been warmed stayed invisible, and every route protected by the filter aliases it
declares answered HTTP 500. Our workaround is to delete the cache file from a separate
process (a plain
rm -fin the deploy script) rather than from PHP, which is a workaroundfor the second detail above rather than for the missing TTL.
This replaces #10537, opened by the same account and closed automatically by
mergeable[bot]because it did not follow this template. It is the same defect, refiled inthe required form: the account cannot reopen the original issue, so this is a new one rather
than a duplicate report.