symfony/symfony · error · Symfony\Component\Cache\Exception\InvalidArgumentException

Cache key "%s" has non-serializable "%s" value.

Error message

Cache key "%s" has non-serializable "%s" value.

What it means

PhpFilesAdapter::doSave() (PhpFilesAdapter.php:219-224) serializes object/array values via VarExporter into PHP files that OPcache compiles. If VarExporter::export throws (resource inside, closure, unsupported internal object), InvalidArgumentException is raised, naming the offending key and value type.

Source

Thrown at src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php:223

        return $now < $expiresAt;
    }

    protected function doSave(array $values, int $lifetime): array|bool
    {
        $ok = true;
        $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
        $allowCompile = self::isSupported();

        foreach ($values as $key => $value) {
            unset($this->values[$key]);
            $isStaticValue = true;
            if (null === $value) {
                $value = "'N;'";
            } elseif (\is_object($value) || \is_array($value)) {
                try {
                    $value = VarExporter::export($value, $isStaticValue);
                } catch (\Exception $e) {
                    throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
                }
            } elseif (\is_string($value)) {
                // Wrap "N;" in a closure to not confuse it with an encoded `null`
                if ('N;' === $value) {
                    $isStaticValue = false;
                }
                $value = var_export($value, true);
            } elseif (!\is_scalar($value)) {
                throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
            } else {
                $value = var_export($value, true);
            }

            $encodedKey = rawurlencode($key);

            if ($isStaticValue) {
                $value = "return [{$expiry}, {$value}];";
            } elseif ($this->appendOnly) {

View on GitHub (pinned to 698e28026c)

Solutions

  1. Convert non-exportable fields to scalars/arrays before saving.
  2. Make objects implement `__serialize`/`__unserialize` and keep them free of resources/closures.
  3. Switch to an adapter that uses PHP's native serialize() (e.g. FilesystemAdapter) if you must store resource-bearing objects — though resources still won't survive.

Example fix

// before
$item->set($pdoStatement); // PDOStatement
$pool->save($item);

// after
$item->set($pdoStatement->fetchAll(\PDO::FETCH_ASSOC));
$pool->save($item);
Defensive patterns

Strategy: validation

Validate before calling

foreach ($values as $k => $v) {
    if (is_object($v) || is_array($v)) {
        try { \Symfony\Component\VarExporter\VarExporter::export($v); }
        catch (\Throwable $e) { throw new \RuntimeException(sprintf('%s not exportable', $k), 0, $e); }
    }
}
$pool->save($item);

Type guard

function isExportable(mixed $value): bool
{
    if (is_resource($value) || $value instanceof \Closure) { return false; }
    try { \Symfony\Component\VarExporter\VarExporter::export($value); return true; }
    catch (\Throwable) { return false; }
}

Try / catch

try {
    $pool->save($item);
} catch (\Symfony\Component\Cache\Exception\InvalidArgumentException $e) {
    // convert value to a safe representation and retry, or log and skip
}

Prevention

When it happens

Trigger: Calling `$pool->save($item)` (or saveDeferred/commit) where the item's value is an object/array containing a resource, closure, or VarExporter-incompatible internal class.

Common situations: Caching entities that hold open file handles, GD images, PDO statements, or SplFileInfo; caching DTOs with closure-based computed properties.

Related errors


AI-assisted analysis of symfony/symfony@698e28026c (2026-08-06). Data as JSON: /api/errors/6ebd7bd227b203c8. Report an issue: GitHub.