Skip to content

Latest commit

 

History

History
240 lines (184 loc) · 9.9 KB

File metadata and controls

240 lines (184 loc) · 9.9 KB

PHP-SPSS v3 typed Dataset API

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.

Requirements and file support

  • PHP 8.4.1 or newer
  • mbstring, bcmath, and zlib extensions
  • 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.

Reading a Dataset

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);

Creating and writing a Dataset

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');

Cell values and formats

Dataset rows contain int|float|string|null cells, with these rules:

  • Numeric variables accept int, float, or null. Numeric null is written as SPSS system-missing and is returned as null when read.
  • String variables accept strings only. '' is a normal empty-string value, not system-missing; null is invalid for strings.
  • User-missing values are separate metadata. Define them with MissingValues::discrete(), MissingValues::range(), or MissingValues::rangeAndValue().
  • SPSS dates, times, datetimes, percentages, and currencies remain numeric cells. Their interpretation and display come from each variable's printFormat and writeFormat, both represented by VariableFormat.

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.

Metadata represented by the typed model

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.

SAV and ZSAV output

FileTechnicalMetadata::sourceFormat selects the default output mode when compression is omitted:

  • sourceFormat: 'sav' defaults to bytecode-compressed SAV (mode 1, record type $FL2).
  • sourceFormat: 'zsav' defaults to zlib-compressed ZSAV (mode 2, 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.

Migrating from the legacy array API

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:

  1. Replace Reader::fromFile($file)->read() and public-property traversal with Reader::fromFile($file)->readDataset().
  2. Replace column-oriented legacy Variable objects and their data arrays with VariableMetadata entries in a VariableDictionary plus row-oriented Dataset::rows().
  3. Replace nested metadata arrays with FileMetadata, FileTechnicalMetadata, ValueLabelSet, MissingValues, FileAttribute, VariableAttribute, VariableSet, and MultipleResponseSet objects.
  4. Pass the resulting Dataset to new Writer($dataset) or Writer::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.