PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Exception

Unserialize not permitted

Error message

Unserialize not permitted

What it means

PhpSpreadsheet's Shared\XMLWriter can buffer its output into a temporary file whose handle cannot survive PHP serialization. To prevent a resurrected writer with a dangling/missing temp file, __unserialize() deliberately throws. It is a hard guard: there is no supported way to unserialize an XMLWriter instance (the method is planned to disappear once PHP 8.6 makes unserialization of internal classes impossible).

Source

Thrown at src/PhpSpreadsheet/Shared/XMLWriter.php:80

        if ($this->tempFileName != '') {
            @unlink($this->tempFileName);
        }
    }

    /**
     * Unserialization is not allowed. This needs to be enforced
     * in the class before Php8.6, but, with that release,
     * this method is no longer needed and will not be executed.
     *
     * @see https://github.com/php/php-src/pull/21694
     *
     * @param mixed[] $data
     */
    public function __unserialize(array $data): void
    {
        $this->tempFileName = '';

        throw new SpreadsheetException('Unserialize not permitted');
    }

    /**
     * Get written data.
     */
    public function getData(): string
    {
        if ($this->tempFileName == '') {
            return $this->outputMemory(true);
        }
        $this->flush();

        return file_get_contents($this->tempFileName) ?: '';
    }

    /**
     * Wrapper method for writeRaw.
     *

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Do not serialize/unserialize PhpSpreadsheet objects; store the finished artifact instead (the written XLSX/CSV file path, or the XML string from XMLWriter::getData()).
  2. Remove Spreadsheet/Writer/XMLWriter instances from the data you pass to serialize()/cache/queue payloads before they get persisted.
  3. If you only need the writer's output, cache getData() (the XML string) and rebuild a fresh XMLWriter when needed.
  4. If a third-party serializer hits it, wrap the payload in a serializable DTO that excludes the writer (e.g. implement __sleep/__serialize on your wrapper to strip it).

Example fix

// before
$blob = unserialize($redis->get('writer'));
// throws: Unserialize not permitted

// after
$xml = $redis->get('writer-output'); // cache the string from ->getData()
$writer = new \PhpOffice\PhpSpreadsheet\Shared\XMLWriter();
$writer->writeRaw($xml);
Defensive patterns

Strategy: validation

Validate before calling

// Never feed PhpSpreadsheet objects to unserialize(); inspect payloads before unserializing
function payloadIsSafeToUnserialize(string $blob): bool
{
    return !str_contains($blob, 'PhpOffice\\PhpSpreadsheet\\Shared\\XMLWriter');
}

Type guard

function isSafeForSerialization(mixed $value): bool
{
    return !($value instanceof \PhpOffice\PhpSpreadsheet\Shared\XMLWriter)
        && !($value instanceof \PhpOffice\PhpSpreadsheet\Spreadsheet);
}

Try / catch

try {
    $obj = unserialize($blob);
} catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
    // rebuild from source data instead of the cached object
    $obj = buildFreshFromSource();
}

Prevention

When it happens

Trigger: Calling unserialize() on a byte string that contains a serialized PhpOffice\PhpSpreadsheet\Shared\XMLWriter (directly or nested inside another object); caching serialized PhpSpreadsheet objects (Worksheet/Writer/Reader internals hold XMLWriter instances) in Redis, sessions, queues, or message payloads; generic deep-clone helpers implemented via serialize()/unserialize().

Common situations: Queues (Laravel, Symfony Messenger) that serialize job payloads carrying a Spreadsheet or a Writer; session or cache drivers that serialize whole objects; upgrading from a workflow that stored half-built export objects. The error surfaces at unserialize time, far from where the object was stored.

Related errors


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/e2ae35d63d72a4b7. Report an issue: GitHub.