iter8/builder
A fluent, Laravel-friendly builder for constructing complex queries and reusable data pipelines with a clean, chainable API. Compose filters, sorting, and pagination into readable, testable components while keeping controllers slim and logic organized.
iter8/builder is a Silex service provider, so it only makes sense if you are already inside a Silex 2.x application. It wraps PhpSpreadsheet and Spout behind a single "builder" abstraction so you define columns and rows once and switch the underlying writer via config.
Minimum steps:
^2.8|^3.0, PhpSpreadsheet 1.x wants PHP 7.1+, and modern PhpSpreadsheet/Spout releases will not satisfy its constraints. Pin the whole trio explicitly rather than letting composer update drift.
composer require iter8/builder:^1.0 phpoffice/phpspreadsheet:^1.0 box/spout:^3.0
*ServiceProvider class in src/. Everything you need to know — the container keys it registers (typically a builder / spreadsheet.builder service plus an options array) and the config keys it expects — is in register(). The README is thin; the provider is the documentation.app.php / bootstrap.php:
$app->register(new Iter8\Builder\BuilderServiceProvider(), [
'builder.engine' => 'spout', // or 'phpspreadsheet'
]);
Confirm the exact key names against the provider — this is the single most common source of "class not found" / "options undefined" errors on first run.Fluent define-then-fill, per request. The natural shape of this library is: resolve the builder from the container, declare headers/columns, append rows (arrays or objects), then ask for a StreamedResponse/Response download. Keep that whole sequence inside one controller action for your first export.
Wrap each export in a small definition class. Once you have three exports, inline fluent chains become copy-paste soup. Define one class per report — OrderExport, InvoiceExport — each with columns() and a rows() generator. Controllers then become three lines: build, fill, stream. This also makes the definition unit-testable without touching the container.
Choose the engine per export, not just globally. PhpSpreadsheet buys you formatting, merged cells, formulas, multiple sheets, and real .xlsx output. Spout buys you streaming, constant memory, and speed on six-figure row counts — but no styling. A good default is Spout globally, with a per-request override on the builder for the handful of "pretty" reports. Check whether the package exposes a per-call setter before assuming you must mutate $app['builder.engine'].
Feed rows lazily. The builder's row-append API works best with a generator. If you're on Doctrine, use ->toIterable() (or a manual doctrine.dbal cursor) and yield rows into the builder rather than ->getResult() into an array. This matters far more than which engine you pick.
Return a streamed response, not a string. For anything over a few thousand rows, build the response with StreamedResponse and set Content-Type / Content-Disposition yourself. If the library hands you a writer object, save('php://output') inside the callback is the usual integration.
Test by reading the output back. Generate into a temp file (tempnam), then parse it with PhpSpreadsheet's reader (it reads CSV and XLSX) and assert on cell values. This catches header drift, off-by-one column mapping, and type coercion — the three bugs this kind of code actually produces. Do not mock the builder; you'll be testing your mocks.
spout but call a formatting method that only PhpSpreadsheet supports, you may get an exception, or you may get an unstyled file with no warning. Decided the engine early and keep formatting calls out of shared code paths.memory_limit and surface as a confusing fatal in the middle of a response. Spout streams and stays flat. If you must use PhpSpreadsheet on large data, chunk across multiple sheets rather than raising the limit.ext-zip, ext-xml, ext-gd (for some features), and ext-mbstring; Spout needs ext-zip and ext-xmlreader. A bare Docker image will fail at runtime with a generic exception. Add them to the image, not to the composer suggestions.\xEF\xBB\xBF) for UTF-8 CSVs; adding a BOM to an .xlsx corrupts it. Branch on the output format, and always send an explicit Content-Type with charset=UTF-8.Content-Disposition filenames. Non-ASCII filenames need RFC 5987 encoding (filename*=UTF-8''...) plus a plain ASCII filename fallback, or browsers mangle them. This package won't do it for you.=, +, -, or @ will be interpreted by Excel when a user opens the file. Prefix user-supplied strings with a single quote or a tab. This is a real vulnerability, not a cosmetic issue, and it is your responsibility — the builder passes values through.DateTime and the path is Spout (or an unformatted PhpSpreadsheet cell), you get 43831 instead of 2020-01-01. Pre-format dates to strings in the row mapper unless you specifically need Excel-native date cells.echo, notice, or debug output happens before the streamed response, the file downloads corrupted. ob_end_clean() before the callback helps; also make sure no middleware or profiler is appending to the body.register() override in your own provider. Because the container key is just an entry, you can also decorate it — for example to inject a default filename or audit logging.$app['builder'] (or var_dump(get_class(...))) to confirm you got the adapter you expected and not a lazily-resolved stub. Nine out of ten "it does nothing" reports on this package trace back to a config key whose name doesn't match what the provider reads.How can I help you explore Laravel packages today?