---
title: "Reading files — Imports for Laravel | Roundly"
description: "The native streaming CSV and XLSX readers — format detection by content, encodings, delimiters, XLSX support, limits, rejections and Imports::read()."
url: https://roundly-consulting.com/open-source/docs/imports-for-laravel/reading-files
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Imports for Laravel](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)

# Reading files

Both readers are part of the package and stream: only the current row is in memory, and XLSX shared strings are spooled to a php://temp stream (memory up to xlsx.shared\_strings\_memory, then disk) — a workbook with 200,000 strings reads in well under 16 MB. There is no third-party spreadsheet library to install or keep updated.

## Format by content

The extension and the client MIME type are ignored. An empty file is rejected (empty), and one larger than limits.file\_size is rejected before reading (too\_large):

| Content | Result |
| --- | --- |
| a ZIP with a SpreadsheetML workbook | XLSX (.xlsm only with xlsx.allow\_macro\_enabled) |
| OpenDocument, binary Excel, another ZIP | open\_document · xlsb · unsupported\_format |
| an OLE compound file (.xls, password-protected workbooks) | legacy\_or\_encrypted |
| UTF-16 / UTF-32 byte order marks, a NUL byte in the first 64 KB | utf16 · utf32 · binary |
| anything else | CSV |

## CSV

- UTF-8 with or without a BOM. The whole file is checked in 64 KB blocks; the first invalid byte rejects it as not\_utf8 with its line and a guess (Windows-1250, Windows-1252, ISO-8859) and tells the user to save as “CSV UTF-8”. Pure ASCII passes.
- Excel’s sep=; first line sets the delimiter. Otherwise it is sniffed among csv.delimiters (comma and semicolon) from the most consistent parse, or set with upload(…, delimiter: ';') or useDelimiter().
- RFC 4180 quoting (delimiters, quotes and newlines inside quotes), no escape character; LF, CRLF and CR-only files.
- Row numbers are the ones a spreadsheet would show; blank records count but are not imported; missing trailing cells are empty; a non-empty value beyond the header’s last column is a row error.
- A leading ' before = + - @, a tab or a CR is stripped (csv.unescape\_formula\_prefix) — what the package’s own CSV writer adds.

## XLSX

| Area | Supported |
| --- | --- |
| Parts | Located through the package relationships; transitional and strict OOXML; absolute and case-different part names. |
| Sheets | The first visible worksheet by default; sheet: 'Name' (exact, then case-insensitive — hidden sheets too) or sheet: 2 (1-based among visible sheets); chart sheets are skipped. |
| Cells | Shared strings (rich text concatenated, phonetic runs skipped, \_xHHHH\_ decoded), inline strings, formula results (cached values — formulas are never evaluated), booleans, errors (a cell error at validation), ISO dates, numbers. |
| Dates | Number formats decide; the 1900 system (Lotus’ phantom 29 Feb 1900 refused) and the 1904 system; time-only formats read as H:i:s; wall-clock time in the session timezone — a DateTime field then converts it to app.timezone. |
| Sparse rows | Missing r attributes read sequentially; formatted blank rows are skipped; data below more than xlsx.max\_blank\_rows empty rows rejects the file instead of being dropped. |

Container and XML safety: the ZIP is opened read-only with a local-vs-central header consistency check; the entry count is capped (xlsx.max\_entries); encrypted entries are refused; no part may declare more than xlsx.max\_part\_size; the bytes really inflated are counted while parsing, per part and across the read (xlsx.max\_inflated), so a size that lies is stopped mid-stream; entries are parsed straight from the ZIP, never extracted; a DOCTYPE in any parsed part rejects the file before an entity could be declared, and an external entity is never resolved. The reader restores the process’s previous libxml error mode when the file closes, so queue workers and Octane keep their own.

## Sheets, delimiters and title rows

```php
// A title row above the headers, a named sheet, a forced delimiter
$session = Imports::for($team)->by($user)->upload(
    'assets',
    $request->file('file'),
    sheet: 'Assets',        // a name (exact, then case-insensitive) or a 1-based position among visible sheets
    delimiter: ';',
    headerRow: 2,
    locale: 'sk',           // labels, messages and the decimal separator of this session
);

// Change it later — the preview is dropped (preview again)
$session->useSheet('Assets');   // XLSX: another sheet
$session->useDelimiter(';');    // CSV: override the sniffed delimiter
$session->useHeaderRow(2);      // a title row above the headers
```

## Limits

limits.file\_size (KB, default 10,240), limits.rows (data rows, default 10,000, ceiling 100,000), limits.columns (default 100 — non-empty cells only) and limits.cell\_length. An importer may override each with limits() — withCellLength() for the cell length.

## Rejections

A whole upload is rejected and nothing is stored. ImportFileRejectedException (HTTP 422) carries reason (a RejectionReason) and context; its message is translated in English and Slovak and says what to do:

```php
use RoundlyConsulting\Imports\Exceptions\ImportFileRejectedException;

try {
    $session = Imports::for($team)->by($user)->upload('assets', $request->file('file'));
} catch (ImportFileRejectedException $e) {
    $e->reason;          // RejectionReason::NotUtf8
    $e->context;         // the values in the message (line, encoding guess)
    $e->getMessage();    // translated, and says how to fix the file
}
```

| RejectionReason | When |
| --- | --- |
| `empty` | The file has no bytes. |
| `too_large` | Over limits.file\_size — checked before reading. |
| `macro_enabled` | An .xlsm workbook while xlsx.allow\_macro\_enabled is off. |
| `open_document · xlsb · unsupported_format` | An .ods, a binary workbook, another ZIP. |
| `legacy_or_encrypted` | An OLE file: .xls or a password-protected workbook. |
| `utf16 · utf32 · binary · not_utf8` | The encoding checks; not\_utf8 names the line and a guess (Windows-1250, Windows-1252, ISO-8859) and asks for “CSV UTF-8”. |
| `no_header` | The header row is empty. |
| `too_many_columns · too_many_rows` | Over the importer’s limits. |
| `malformed_spreadsheet` | A damaged or invalid workbook, an inconsistent ZIP, a DOCTYPE in a parsed part. |
| `data_after_blank_rows` | XLSX: a row with data below more than xlsx.max\_blank\_rows empty rows — rows are never dropped silently. |
| `zip_too_large` | A workbook part inflating past xlsx.max\_part\_size / xlsx.max\_inflated (also file batches). |
| `format_not_accepted` | The importer’s formats() excludes the detected format. |
| `too_many_files · archive_too_large · zip_ratio · zip_unsafe_path · zip_symlink · zip_encrypted · zip_inconsistent · zip_entry_too_large` | File batches — see File batches. |
| `unreadable` | The upload could not be read. |

A sheet that does not exist throws SheetNotFoundException (422) — available lists the sheet names.

## The standalone reader

The pipeline’s own reader, usable on its own for any CSV or XLSX your app needs to read — no session, nothing stored, the same safety checks:

```php
use RoundlyConsulting\Imports\DataTransferObjects\ImportSource;
use RoundlyConsulting\Imports\Facades\Imports;

$file = Imports::read($request->file('file'), sheet: 'Assets', delimiter: null, headerRow: 1);
// or Imports::read('/path/to/file.csv'), Imports::read(ImportSource::disk('s3', 'exports/assets.xlsx'))

try {
    $file->format();            // SourceFormat::Csv | SourceFormat::Xlsx
    $file->sheets();            // list<SheetInfo> (XLSX), [] for CSV
    $file->sheet();             // the sheet read (XLSX) or null
    $file->delimiter();         // CSV: the delimiter used; delimiterSource(): Sniffed | Hint | Override
    $file->encoding();          // CSV: `utf-8` or `utf-8-bom`; null for XLSX
    $file->headerRow();         // 1-based
    $file->headers();           // list<string|null>, null for a blank header cell
    $file->displayHeaders();    // list<string>, a blank header is `Column C`
    foreach ($file->rows() as $rowNumber => $cells) {   // keyed by the row number a user sees
        // list<Cell|null> — null for an empty cell
    }
    $file->count();             // data rows, in one full pass (every limit enforced)
} finally {
    $file->close();             // also closed on destruct
}
```

rows() rejects the file (too\_many\_rows) at the first row over the limit. A disk source is copied to a temp directory imports:sweep clears. SpreadsheetFile::open() reads a local path directly with ReadOptions:

```php
use RoundlyConsulting\Imports\Reading\{Cell, ReadOptions, SpreadsheetFile};

$file = SpreadsheetFile::open('/path/to/file.xlsx', ReadOptions::fromConfig(sheet: 'Assets', headerRow: 2));

foreach ($file->rows() as $rowNumber => $cells) {
    $cell = $cells[0];      // ?Cell
    $cell?->type;           // CellType: text, number, boolean, date, error
    $cell?->value;          // string, numeric string, bool, CarbonImmutable or an error code (#N/A)
    $cell?->display();
}

// Accepted everywhere a file is
ImportSource::upload($uploadedFile); ImportSource::path('/tmp/a.csv'); ImportSource::disk('s3', 'exports/a.xlsx');
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/imports-for-laravel.md)
