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

Could not open resource for writing.

Error message

Could not open resource for writing.

What it means

The Ods writer's createZip() requires $this->fileHandle to be a live stream resource before it can hand it to ZipStream. BaseWriter::openFileHandle() assigns whatever non-string value you pass to save() verbatim, so if you pass the result of a failed fopen() (boolean false), an already-closed resource, or null, is_resource() is false and this exception fires. It does NOT mean the file could not be opened on disk (that case throws 'Could not open file ... for writing.' earlier in openFileHandle()) - it means the value you passed in as the output target is not a usable stream.

Source

Thrown at src/PhpSpreadsheet/Writer/Ods.php:137

        // Close file
        try {
            $zip->finish();
        } catch (OverflowException) {
            throw new WriterException('Could not close resource.');
        }

        $this->maybeCloseFileHandle();
    }

    /**
     * Create zip object.
     */
    private function createZip(): ZipStream
    {
        // Try opening the ZIP file
        if (!is_resource($this->fileHandle)) {
            throw new WriterException('Could not open resource for writing.');
        }

        // Create new ZIP stream
        return ZipStream0::newZipStream($this->fileHandle);
    }

    /**
     * Get Spreadsheet object.
     */
    public function getSpreadsheet(): Spreadsheet
    {
        return $this->spreadSheet;
    }

    /**
     * Set Spreadsheet object.
     *
     * @param Spreadsheet $spreadsheet PhpSpreadsheet object

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Check the fopen() result before passing it: if ($fh === false) fail loudly with your own error.
  2. Simplest: pass a filename string to save() and let the writer open and close the handle itself.
  3. Verify the target directory exists and is writable, and that no open_basedir/safe-path restriction blocks it.
  4. Never fclose() a handle you intend to pass to save(); the writer closes it via maybeCloseFileHandle().

Example fix

// before
$fh = fopen($exportDir . '/out.ods', 'wb'); // returns false if $exportDir is unwritable
$writer->save($fh); // WriterException: Could not open resource for writing.

// after
$fh = fopen($exportDir . '/out.ods', 'wb');
if ($fh === false) {
    throw new RuntimeException("Cannot open output file in $exportDir");
}
$writer->save($fh);
// - or simply -
$writer->save($exportDir . '/out.ods');
Defensive patterns

Strategy: validation

Validate before calling

// Validate before save()
if (is_string($target)) {
    $dir = dirname($target);
    if (!is_dir($dir) || !is_writable($dir)) {
        throw new RuntimeException("Output directory not writable: $dir");
    }
} else {
    if (!is_resource($target) || get_resource_type($target) !== 'stream') {
        throw new RuntimeException('Output target must be a filename or an open stream resource');
    }
}
$writer->save($target);

Type guard

/** @param mixed $fh @phpstan-assert-if-true resource $fh */
function isOpenStream(mixed $fh): bool
{
    return is_resource($fh) && get_resource_type($fh) === 'stream';
}

Prevention

When it happens

Trigger: Calling $writer->save(fopen($path, 'wb')) where fopen returned false (unwritable directory, open_basedir restriction, URL wrapper failure) and the return value was not checked; passing a resource variable that was fclose()'d before save(); passing null or a non-stream value as the $filename argument.

Common situations: Developers switching from save('file.ods') to save($resource) to write into php://output, s3:// streams, or memory without checking fopen's return; permission problems in containerized deployments where /app/var isn't writable; reusing a cached file-handle property that another code path already closed.

Related errors


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