PHPOffice/PhpSpreadsheet · critical · PhpOffice\PhpSpreadsheet\Writer\Exception

Could not open file "{$filename}" for writing.

Error message

Could not open file "{$filename}" for writing.

What it means

Every writer's save($filename) funnels into BaseWriter::openFileHandle(), which runs fopen($filename, 'wb') (mode 'w' for s3:// URLs) and throws when fopen returns false, meaning the output file could not be created or opened for writing. Root causes are environmental: missing parent directory, insufficient permissions for the PHP process user, the file being locked by another program, a full disk, or an unavailable stream wrapper.

Source

Thrown at src/PhpSpreadsheet/Writer/BaseWriter.php:119

    public function openFileHandle($filename): void
    {
        if (!is_string($filename)) {
            $this->fileHandle = $filename;
            $this->shouldCloseFile = false;

            return;
        }

        $mode = 'wb';
        $scheme = parse_url($filename, PHP_URL_SCHEME);
        if ($scheme === 's3') {
            // @codeCoverageIgnoreStart
            $mode = 'w';
            // @codeCoverageIgnoreEnd
        }
        $fileHandle = $filename ? fopen($filename, $mode) : false;
        if ($fileHandle === false) {
            throw new Exception('Could not open file "' . $filename . '" for writing.');
        }

        $this->fileHandle = $fileHandle;
        $this->shouldCloseFile = true;
    }

    protected function tryClose(): bool
    {
        return fclose($this->fileHandle);
    }

    /**
     * Close file handle only if we opened it ourselves.
     */
    protected function maybeCloseFileHandle(): void
    {
        if ($this->shouldCloseFile) {
            if (!$this->tryClose()) {

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Ensure the parent directory exists and is writable by the PHP user: mkdir(dirname($path), 0775, true), then chown/chmod as needed.
  2. Pre-flight check with is_writable(dirname($path)) (and is_writable($path) for existing files) and fail with a clear message.
  3. Use absolute paths; write to a temp file under sys_get_temp_dir() and move it into place afterwards.
  4. Close the file in other applications and check disk space; verify the stream wrapper is enabled for URL targets.
  5. If unclear, reproduce with a bare fopen($path, 'wb') to confirm the failure is environmental rather than library-specific.

Example fix

// before
$writer->save('/var/www/export/report.xlsx'); // dir missing or not writable

// after
$dir = '/var/www/export';
if (!is_dir($dir)) {
    mkdir($dir, 0775, true);
}
if (!is_writable($dir)) {
    throw new RuntimeException('Export directory not writable: ' . $dir);
}
$writer->save($dir . '/report.xlsx');
Defensive patterns

Strategy: validation

Validate before calling

$path = '/var/www/exports/report.xlsx';
$dir = dirname($path);
if (!is_dir($dir)) {
    mkdir($dir, 0775, true);
}
if (!is_writable($dir)) {
    throw new RuntimeException('Export directory not writable: ' . $dir);
}
$writer->save($path);

Try / catch

use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;

try {
    $writer->save($path);
} catch (WriterException $e) {
    if (str_contains($e->getMessage(), 'Could not open file')) {
        // permissions, lock, or full disk: log $path, notify the user,
        // optionally retry against a temp directory
    } else {
        throw $e;
    }
}

Prevention

When it happens

Trigger: $writer->save('/exports/report.xlsx') when /exports does not exist or is not writable by the web-server user; the target file is currently open in Excel (Windows lock); disk full; an empty filename; an http:// destination with allow_url_fopen disabled.

Common situations: Export directories owned by root while PHP runs as www-data; CLI scripts using relative output paths from a different cwd; Docker volumes mounted read-only; antivirus or spreadsheet applications holding locks on Windows.

Related errors


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