cbytedigital/laravel-bi-data-export
A Laravel package for exporting business intelligence data. It provides a simple way to move application data into BI and reporting tools, helping teams prepare datasets for analysis without writing custom export logic.
This is a 1.0.0 release with a deliberately narrow surface: it exports data to CSV. No import, no other formats, no documented queue or scheduling integration. Treat the first release as a single-purpose utility rather than a data pipeline, and confirm the actual API from the package README and src/ before wiring it in — at 1.0.0 the public class names are the contract you'll be pinned to.
composer require the package, then check whether it ships a config file (php artisan vendor:publish --tag=...) or is convention-only. Many small export packages are zero-config.Route::middleware('auth')->get('/reports/orders.csv', function () {
$orders = Order::query()->where('status', 'paid')->with('customer');
return app(OrderCsvExport::class)->download($orders);
});
If the package has no download helper, you're composing it yourself with the framework's streamDownload / response()->stream() — wrap whatever the package gives you (header row + row serialization) in that.
Extract the export from the controller. The controller should resolve filters, authorize, and hand off. Put column selection, ordering, and row shaping in a dedicated class so the same export is reusable from a route, a command, and a test.
final class OrderCsvExport
{
public function columns(): array
{
return ['id', 'reference', 'customer', 'total', 'paid_at'];
}
public function rows(Builder $query): iterable
{
foreach ($query->cursor() as $order) {
yield [
$order->id,
$order->reference,
$order->customer?->name,
number_format($order->total / 100, 2, '.', ''),
$order->paid_at?->toIso8601String(),
];
}
}
}
Feed it an iterable, not a collection. cursor() (or lazy()) keeps the generator lazy. If the 1.0.0 API only accepts Collection, cap the query with a date range or limit and accept that large exports aren't supported yet — don't assume chunking exists because it isn't in the release notes.
Reuse across delivery channels. One export class, three entry points: HTTP download for interactive use, php artisan orders:export --since=... writing to Storage::disk('exports') for scheduled runs, and a Mailable attaching the file. This is where a tiny package earns its place — the value is in the shared column definition, not the file write.
Schedule the big ones. Anything a user might trigger repeatedly belongs in a queued job or scheduler task that writes to a disk and notifies via mail or a signed temporary URL. Keeps request timeouts out of the picture and gives you a durable artifact.
Test the output, not the package. Assert status, Content-Type: text/csv, Content-Disposition filename, and the parsed rows via a small CSV reader — header count matching a known row's column count catches off-by-one mistakes in column definitions.
$response = $this->actingAs($admin)->get('/reports/orders.csv');
$response->assertOk();
$this->assertStringContainsString('reference,total', $response->streamedContent());
Delimiter and locale. Comma-separated with a dot decimal separator is the safe default. If your audience is on a European Excel locale, they'll see decimal columns collapse into one. Decide explicitly: either emit ; with a config flag, or document "data import, not Excel double-click."
CSV injection. Any cell beginning with =, +, -, @, tab, or CR is a formula risk when opened in Excel. Customer names and notes are user-controlled. Prefix with ' or strip before writing — the package almost certainly does not do this for you at 1.0.0.
UTF-8 BOM for Excel. Without \xEF\xBB\xBF at the start of the stream, Excel mangles accented characters. Add it in your own wrapper; it's a one-liner and a recurring support ticket otherwise.
Output buffering breaks downloads. A stray newline after ?> or a dd() earlier in the request yields the classic "headers already sent" corruption — the CSV downloads but is full of HTML. If output is garbled, ob_get_level() and any middleware echoing are the first place to look.
N+1 inside the loop. with() the relations your columns touch before iterating, or cursor() will fire a query per row and turn a 2-second export into a 200-second one.
Memory and time limits. Streaming is the difference between working and not. Check memory_get_peak_usage on a 50k-row export in a test; if it climbs linearly, you're buffering and need to cap row counts or wait for a later release.
Pin the version and wrap the call. 1.0.0's API is not yet battle-tested. Isolate every package call behind your export class or a thin adapter so a 1.1 or 2.0 rename is a one-file change. Watch for deprecations in point releases rather than assuming semver safety on the config keys.
Extension points to check before building your own. Look for a formatter/serializer/column-definition hook in the source. If one exists, use it — reimplementing row shaping outside the package means you own the header/row alignment problem forever.
How can I help you explore Laravel packages today?