PHP-SPSS v3 provides a typed, row-oriented model for reading, inspecting, creating, and writing SPSS/PSPP files. The central types are SPSS\Sav\Dataset, VariableDictionary, VariableMetadata, FileMetadata, and FileTechnicalMetadata.
- PHP 8.4.1 or newer
mbstring,bcmath, andzlibextensions- unencrypted SAV (
$FL2) and ZSAV ($FL3) files
Encrypted SAV files are not supported. Decrypt them with SPSS or another compatible tool before using this library.
Both SAV and ZSAV can be read and written. ZSAV uses zlib compression and must be written as a complete dataset; incremental Writer::writeCase() output is not supported for ZSAV.
Reader::readDataset() reads the dictionary, metadata, and cases and returns one immutable typed object:
use SPSS\Sav\Reader;
$dataset = Reader::fromFile('/path/to/input.sav')->readDataset();
echo $dataset->rowCount();
echo $dataset->metadata->label;
foreach ($dataset->variables() as $variable) {
echo $variable->name;
echo $variable->type->value;
}
$age = $dataset->variable('age'); // Lookup by long or short name.
$firstRow = $dataset->row(0);
$allRows = $dataset->rows();Rows use the same column order as Dataset::variables(). To load only the dictionary and metadata, use the explicit metadata path:
$reader = Reader::fromFile('/path/to/input.sav')->readMetaData();
$metadataOnly = $reader->toDataset(includeData: false);The following example creates one numeric variable and one string variable, then writes a SAV file:
use SPSS\Sav\Alignment;
use SPSS\Sav\Dataset;
use SPSS\Sav\FileAttribute;
use SPSS\Sav\FileMetadata;
use SPSS\Sav\FileTechnicalMetadata;
use SPSS\Sav\Measure;
use SPSS\Sav\MissingValues;
use SPSS\Sav\MultipleResponseSet;
use SPSS\Sav\MultipleResponseSetType;
use SPSS\Sav\ValueLabel;
use SPSS\Sav\ValueLabelSet;
use SPSS\Sav\Variable;
use SPSS\Sav\VariableAttribute;
use SPSS\Sav\VariableDictionary;
use SPSS\Sav\VariableFormat;
use SPSS\Sav\VariableMetadata;
use SPSS\Sav\VariableRole;
use SPSS\Sav\VariableSet;
use SPSS\Sav\VariableType;
use SPSS\Sav\Writer;
$numericFormat = new VariableFormat(Variable::FORMAT_TYPE_F, 8, 0);
$stringFormat = new VariableFormat(Variable::FORMAT_TYPE_A, 40, 0);
$status = new VariableMetadata(
name: 'status',
type: VariableType::NUMERIC,
width: 0,
printFormat: $numericFormat,
writeFormat: $numericFormat,
label: 'Interview status',
valueLabels: new ValueLabelSet([
new ValueLabel(1, 'Complete'),
new ValueLabel(2, 'Partial'),
], ['status']),
missingValues: MissingValues::discrete(99),
measure: Measure::NOMINAL,
alignment: Alignment::RIGHT,
role: VariableRole::TARGET,
attributes: [
new VariableAttribute('status', 'source', ['survey']),
],
);
$comment = new VariableMetadata(
name: 'comment',
type: VariableType::STRING,
width: 40,
printFormat: $stringFormat,
writeFormat: $stringFormat,
missingValues: MissingValues::discrete('NA'),
);
$dataset = new Dataset(
dictionary: new VariableDictionary([$status, $comment]),
rows: [
[1, 'Complete response'],
[null, ''],
[99, 'NA'],
],
metadata: new FileMetadata(
label: 'Survey export',
documents: ['Generated by the import pipeline'],
attributes: [new FileAttribute('source', ['crm'])],
variableSets: [new VariableSet('analysis', ['status', 'comment'])],
multipleResponseSets: [
new MultipleResponseSet(
name: '$responses',
type: MultipleResponseSetType::CATEGORY,
variableNames: ['status'],
label: 'Response status',
),
],
),
technicalMetadata: new FileTechnicalMetadata(
sourceFormat: 'sav',
encoding: 'UTF-8',
compression: 1,
),
);
$writer = new Writer($dataset);
$writer->save('/path/to/output.sav');
$writer->close();Writer also accepts an empty constructor followed by writeDataset($dataset):
$writer = new Writer();
$writer->writeDataset($dataset);
$writer->save('/path/to/output.sav');Dataset rows contain int|float|string|null cells, with these rules:
- Numeric variables accept
int,float, ornull. Numericnullis written as SPSS system-missing and is returned asnullwhen read. - String variables accept strings only.
''is a normal empty-string value, not system-missing;nullis invalid for strings. - User-missing values are separate metadata. Define them with
MissingValues::discrete(),MissingValues::range(), orMissingValues::rangeAndValue(). - SPSS dates, times, datetimes, percentages, and currencies remain numeric cells. Their interpretation and display come from each variable's
printFormatandwriteFormat, both represented byVariableFormat.
For example, a date variable is still numeric:
$dateFormat = new VariableFormat(Variable::FORMAT_TYPE_DATE, 11, 0);
$createdAt = new VariableMetadata(
name: 'created_at',
type: VariableType::NUMERIC,
width: 0,
printFormat: $dateFormat,
writeFormat: $dateFormat,
);The library deliberately does not convert these values to DateTimeInterface, formatted strings, or money objects, so the original SPSS numeric value and format stay lossless.
Text-to-number conversion for the three common SPSS date/time formats is available as an explicit opt-in helper:
use SPSS\Sav\Variable;
use SPSS\Utils;
$date = Utils::parseSpssDateTime('31-Jul-2026', Variable::FORMAT_TYPE_DATE);
$duration = Utils::parseSpssDateTime('59:30:15.25', Variable::FORMAT_TYPE_TIME);
$dateTime = Utils::parseSpssDateTime(
'31-Jul-2026 14:05:30.5',
Variable::FORMAT_TYPE_DATETIME,
);The accepted forms are:
- DATE:
dd-Mmm-yyyy, using an English three-letter month. - TIME:
h+:mm[:ss[.fraction]]; this is a duration, so hours may exceed 23. - DATETIME:
dd-Mmm-yyyy HH:mm[:ss[.fraction]], using a 00-23 hour clock.
Parsing is strict and throws InvalidArgumentException for unsupported format codes, invalid calendar dates, or malformed and out-of-range times. Pass the returned number as the cell value; Reader and Writer do not invoke this conversion automatically.
VariableMetadata preserves the variable's long and short names, label, type, storage width, print/write formats, value-label set, user-missing definition, measure, alignment, display columns, role, dictionary index, and VariableAttribute values.
FileMetadata preserves the file label, weight variable, creation time, documents, FileAttribute values, VariableSet definitions, and MultipleResponseSet definitions. FileTechnicalMetadata exposes encoding and low-level provenance such as record type, source version, product name, compression, case count, layout, byte order, and machine representation.
Value-label sets may be shared by listing all member variable names in ValueLabelSet. Set members and the weight variable are referenced by variable name.
FileTechnicalMetadata::sourceFormat selects the default output mode when compression is omitted:
sourceFormat: 'sav'defaults to bytecode-compressed SAV (mode1, record type$FL2).sourceFormat: 'zsav'defaults to zlib-compressed ZSAV (mode2, record type$FL3).
An explicit compression value overrides that default: 0 writes uncompressed SAV, 1 writes bytecode-compressed SAV, and 2 writes ZSAV. The writer derives the matching recordType; normally leave recordType unset. If you set it for low-level interoperability, $FL2 must use mode 0 or 1, while $FL3 must use mode 2.
SPSS fixed-width string fields are measured in bytes after conversion to FileTechnicalMetadata::encoding, not in PHP characters. The writer truncates only at complete target-encoding character boundaries and fills the remaining field with spaces. This also applies to byte-limited variable and value labels, so a multibyte encoding may fit fewer characters than the nominal byte limit.
Provide normal PHP strings in the process's internal encoding; the writer performs the target conversion. Values that must survive exactly should be representable in the selected file encoding.
For ZSAV, use a complete Dataset:
$dataset = new Dataset(
dictionary: $dictionary,
rows: $rows,
technicalMetadata: new FileTechnicalMetadata(
sourceFormat: 'zsav',
sourceVersion: '3.0.0',
),
);
$writer = new Writer($dataset);
$writer->save('/path/to/output.zsav');A semantic sourceVersion such as 3.0.0 is written into the machine-integer version metadata when it has three numeric components. The filename extension alone does not select the output mode.
The legacy low-level API remains available for compatibility in v3: Reader::read() still populates the public header, variable, info, document, and data structures, and Writer still accepts the historical configuration array. Existing integrations can therefore migrate incrementally.
For new code:
- Replace
Reader::fromFile($file)->read()and public-property traversal withReader::fromFile($file)->readDataset(). - Replace column-oriented legacy
Variableobjects and theirdataarrays withVariableMetadataentries in aVariableDictionaryplus row-orientedDataset::rows(). - Replace nested metadata arrays with
FileMetadata,FileTechnicalMetadata,ValueLabelSet,MissingValues,FileAttribute,VariableAttribute,VariableSet, andMultipleResponseSetobjects. - Pass the resulting
Datasettonew Writer($dataset)orWriter::writeDataset($dataset).
Typed objects validate names, widths, row shapes, value kinds, and metadata relationships earlier than the legacy array path. Keep the legacy path only where direct access to binary records is intentionally required.