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
}
| ImporterRow | Returns |
|---|---|
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:
| Class | Is |
|---|---|
ImporterReader | The interface a reader implements — an iterator over the rows. |
CSVReader / XLSXReader | The two that ship; the extension picks between them. |
InvalidReader | The one used when neither matches — it simply reads nothing, which is what makes isValid() false instead of throwing. |
ExporterWriter | The interface a writer implements. |
CSVWriter / XLSXWriter | The 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.