Skip to content

Bug: FileVarExportHandler silently drops the TTL passed by FileLocatorCached, so the locator cache never expires #10539

Description

@communicationbox

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

  1. Enable the locator cache: php spark optimize, which sets $locatorCacheEnabled = true
    in app/Config/Optimize.php.
  2. Run any command or request, so that the cache file is written to
    writable/cache/FactoriesCache_FileLocatorCache.
  3. Open that file. It contains <?php return [ ... ]; and nothing else: no timestamp, no
    TTL, no field that could ever make it expire.
  4. Add a new file that the locator is meant to discover — for example a
    Config/Registrar.php in a namespace registered in Config\Autoload.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions