Framework
Features

Import & Export

Read and write tabular data as CSV or XLSX through one API — an Importer that iterates typed rows, and an Exporter that streams a download.

CSV & XLSX

CSV works out of the box. XLSX support comes from OpenSpout, an optional dependency:

composer require openspout/openspout

Both sides pick the format automatically — the Importer from the file extension, the Exporter from whether OpenSpout is installed (falling back to CSV).

Importing

Open a file with new Importer($path) — it detects CSV vs XLSX from the extension. Check isValid() first: it is false when the file cannot be read or its extension has no supported reader:

use Framework\ImpExp\Importer;

$importer = new Importer("/tmp/users.csv");
if (!$importer->isValid()) {
    return;   // unsupported or unreadable file
}

Next, tell the importer which column holds which field. For a fixed layout, setColumnNames() takes the keys in order (column 1, 2, 3…):

$importer->setColumnNames("name", "email", "age");   // name = col 1, email = 2, age = 3

When the columns can arrive in any order, build a column map instead. getHeader() returns the file's header row as a list of Select options (one per column) — show it to the user, or match the labels yourself, to decide which column is which. Then pass the resulting key => column map to setColumns():

$header = $importer->getHeader();   // the file columns, as Select options

$importer->setColumns([
    "name"  => 1,   // your "name" field is in column 1
    "email" => 2,
    "age"   => 4,   // columns need not be contiguous or in order
]);

Use getData() to pull a small sample (three rows by default) for a preview before committing, then iterate the whole file. Each row is an ImporterRow with typed accessors addressed by your field keys:

$preview = $importer->getData(amount: 5);   // an ImporterData sample

foreach ($importer as $row) {
    $name  = $row->getString("name");
    $email = $row->getString("email");
    $age   = $row->getInt("age");
    // …store the row
}
ImporterRowReturns
getString(key)A string cell.
getInt(key) / getFloat(key)A numeric cell.
getList(key)A delimited cell as a list.
toArray()The whole row as an array.

Exporting

Build an Exporter with the total row count, a title and a file name (pass useCSV: true to force CSV). Columns and rows are keyed maps: addHeaders() takes a key => label map, and each writeLine() takes a key => value map with the same keys, so the columns stay aligned. download() streams the file to the browser:

use Framework\ImpExp\Exporter;

$exporter = new Exporter(
    total:    count($users),
    title:    "Users",
    fileName: "users",
    useCSV:   false,   // XLSX when OpenSpout is available
);

// Columns: field key => column label
$exporter->addHeaders([
    "name"  => "Name",
    "email" => "Email",
    "age"   => "Age",
]);
$exporter->writeHeader();

foreach ($users as $user) {
    // Each row keyed by the same field keys
    $exporter->writeLine([
        "name"  => $user->name,
        "email" => $user->email,
        "age"   => $user->age,
    ]);
}

$exporter->download();

Add one column conditionally with addHeader($key, $label, $condition) — a column left out of the headers is simply ignored on every line, so the map keeps the output aligned.

Chunked exports

For a large data set, don't hold every row in memory. Pass the total up front, then loop: startChunk() advances a schema request to the next page, you fetch and write just that page, and isComplete() ends the loop:

use Framework\ImpExp\Exporter;

$exporter = new Exporter(
    total:    User::getTotal($query),
    title:    "Users",
    fileName: "users",
);
$exporter->addHeaders([ "name" => "Name", "email" => "Email" ]);
$exporter->writeHeader();

while (!$exporter->isComplete()) {
    $exporter->startChunk($query, perPage: 2000);   // advance to the next page
    foreach (User::getEntityList($query) as $user) {
        $exporter->writeLine([
            "name"  => $user->name,
            "email" => $user->email,
        ]);
    }
}

$exporter->download();

Readers & writers

The Importer and Exporter are wrappers — the format work happens in a reader or a writer behind them, chosen for you:

ClassIs
ImporterReaderThe interface a reader implements — an iterator over the rows.
CSVReader / XLSXReaderThe two that ship; the extension picks between them.
InvalidReaderThe one used when neither matches — it simply reads nothing, which is what makes isValid() false instead of throwing.
ExporterWriterThe interface a writer implements.
CSVWriter / XLSXWriterThe two that ship; XLSX is used when OpenSpout is installed.

You only meet them when adding a format: implement the interface, and the wrapper above it — the typed rows, the column map, the chunking — keeps working unchanged.

Reporting progress

An import of any size outlives a single page load, so the client needs to know how far along it is. Progress stores the count on the signed-in credential, which the client can then poll from a second route:

use Framework\Core\Progress;

Progress::start();

foreach ($importer as $index => $row) {
    // …store the row
    Progress::update($index + 1);
}

Progress::end();

increment() is the same as update() without keeping a counter yourself, and get() is what the polling route returns.

The interesting part is what update() does besides counting: it writes a byte to the client and flushes, so PHP notices when the browser has gone. If the user closed the tab or navigated away, the import stops there instead of running to completion against nobody — which for a long import is the difference between a cancelled job and half an hour of wasted work.