Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Datatables Export Laravel Package

yajra/laravel-datatables-export

Laravel DataTables Export is a plugin for yajra/laravel-datatables that enables server-side exports via queues and OpenSpout, with optional Livewire and DataTables button integrations. Requires PHP 8.3+ and Laravel 13. Install via Composer, run queue:batches-table and migrate, then publish assets fo...

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up in four steps.

composer require yajra/laravel-datatables-export:"^13.0"

php artisan queue:batches-table
php artisan migrate

php artisan vendor:publish --tag=datatables-export

Then add the trait to any existing DataTable service class — this is the only change needed for the server side to accept export requests:

use Yajra\DataTables\WithExportQueue;

class UsersDataTable extends DataTable
{
    use WithExportQueue;
}

Finally, make sure a queue worker is actually running (php artisan queue:work). Nothing about this package works synchronously; the UI is a poller around a queued batch.

Where to look first, in order:

  1. config/datatables-export.php — the only two keys that matter on day one are token_ttl (default 1,440 minutes) and date_formats. Read date_formats before you write any date column, because it drives the auto-detection rules.
  2. public/vendor/datatables/dataTables.queuedExport.js — this is the front-end contract. Skim it to see exactly what payload the queuedExport button sends (current search, ordering, filters) and how polling is scheduled.
  3. The <livewire:export-button> component source if you're on Livewire — the props map 1:1 to the HTML button options.

First use case — the one this package exists for: a table wide enough or large enough that the stock DataTables Buttons export blows the browser tab's memory or stalls a PHP request. You add one button, and the export becomes a batch of chunked rows written by OpenSpout into a file on a queue worker, with a progress bar in the UI. If your export is 300 rows and finishes in 200ms, this package is over-engineering — a plain client-side CSV button is fine. Adopt it when the export is the thing that times out.

Pick one front door per application: the HTML queuedExport button (needs the published script tag after jQuery/DataTables/Buttons) or the Livewire component. Mixing both is possible but means maintaining two option surfaces and two sets of user expectations.

Implementation Patterns

The button-with-options pattern (no Livewire)

public function html(): HtmlBuilder
{
    return $this->builder()
        ->buttons([
            Button::make([
                'extend'         => 'queuedExport',
                'text'           => 'Export Excel',
                'exportType'     => 'xlsx',
                'filename'       => 'users.xlsx',
                'sheetName'      => 'Users',
                'autoDownload'   => true,
                'pollInterval'   => 2000,
                'processingText' => 'Preparing export…',
                'errorText'      => 'Export failed — please retry.',
            ]),
        ]);
}

Supported options: exportType, filename, sheetName, emailTo, autoDownload, pollInterval, processingText, errorText. That's a small enough surface to hardcode per table, which is what you'll do.

The Livewire pattern

<livewire:export-button
    :table-id="$dataTable->getTableId()"
    type="csv"
    button-name="Export CSV"
    filename="my-table.csv"
    sheet-name="Monthly Report"
    auto-download="true" />

This is the better fit when the export button needs to sit inside an existing Livewire dashboard rather than in the DataTables button toolbar, or when you want the button label/filename computed per-request from Blade. Note the sheet-name is irrelevant for csv — a csv export silently ignores it.

Dynamic sheet names / filenames

Two mechanisms, and they don't compose the way you'd hope. Override the method when the name depends on server state:

protected function sheetName(): string
{
    return 'Yearly Report';
}

Pass the prop/blade value when it depends on the page context. Precedence rules changed in 13.3.1 ("reuse data table filename"), so if you both set a filename property on the DataTable and pass a filename prop, verify empirically which wins in your pinned version rather than trusting the docs.

Column formatting is where the real work lives

The package auto-detects types from the underlying value, which is correct maybe 80% of the time. Every deviation is a support ticket, so set exportFormat explicitly on anything that isn't a plain string or float:

// Phone numbers / account codes / zip codes: preserve leading zeroes
Column::make('mobile')->exportFormat('00000000000'),
Column::make('postcode')->exportFormat('@'),

// Numeric formatting
Column::make('total')->exportFormat('0.00'),
Column::make('count')->exportFormat('#,##0'),
Column::make('average')->exportFormat('#,##0.00'),

// Dates
Column::make('report_date')->exportFormat('mm/dd/yyyy'),
Column::make('created_at'),                                   // auto-detected
Column::make('updated_at')->exportFormat(NumberFormat::FORMAT_DATE_DATETIME),

The '@' / FORMAT_TEXT / FORMAT_GENERAL escape hatch is the one you'll reach for most: any column that is conceptually an identifier (invoice numbers, SKUs, employee IDs, phone numbers) should be forced to text, or Excel will mangle it into scientific notation or strip leading zeroes. Users notice this immediately and it looks like the export is broken.

If you need a format the package doesn't recognize as a date, add it to date_formats in the published config rather than fighting it per-column.

Hooking into lifecycle events

Client side, the table element emits four DOM events — use these for toasts and analytics:

document.querySelector('#users-table').addEventListener('datatables-export:success', (event) => {
    console.log(event.detail.export.download_url);
});

datatables-export:start, :progress, :success, :error are all available, and onStart / onProgress / onSuccess / onError callbacks can be passed as button options if you'd rather not use listeners.

Server side, listen for ExportStarted, ExportCompleted, ExportFailed. This is the pattern worth building early: a single ExportCompleted listener that notifies the requesting user is how you turn a fire-and-forget download into something usable — especially since the export may finish long after the user navigated away from the table. Broadcasting is optional and uses your own private channels; the package deliberately doesn't impose an auth scheme.

Access tokens instead of authorization

Status and download URLs carry encrypted access tokens with a TTL (token_ttl, default 1,440 minutes / 24h). This means you can hand a download_url to a user, an email, or a notification without building a signed-route policy. It also means you should think about TTL: a user who starts a 40-minute export, gets interrupted, and comes back the next afternoon will find a dead link. For emailTo-style workflows, consider lowering the TTL rather than raising it.

Housekeeping is not optional

// app/Console/Kernel.php
$schedule->command('datatables:purge-export')->weekly();

Every export writes a file to disk. Without this scheduled command, the exports directory grows forever and nobody notices until the disk fills. Register it the same day you install the package; it's one line and it is the difference between a clean install and an incident.

Route exports to their own queue

Since 13.2.0 the queue is configurable. Do configure it. Exports are bursty, long-running, and memory-heavy; sharing a queue with your transactional jobs means a burst of exports delays password-reset emails. A dedicated exports queue on a dedicated worker is the right default for anything past a prototype.

Gotchas and Tips

The batch table is the classic silent failure. Skip queue:batches-table and migrate, and everything looks wired up — the button appears, the request fires — but progress polling has nothing to read. If polling hangs at 0%, this is the first thing to check.

Version requirements are genuinely restrictive. Laravel 13 and PHP 8.3+ are hard floors. And note the trap in the requirements: OpenSpout 5.x, if installed, requires PHP 8.4+. So on PHP 8.3 you're on OpenSpout 4.x, on PHP 8.4 you can use 5.x — and the changelog shows both being accommodated (13.1.0 added OpenSpout 5, then 13.1.1 patched backward compatibility for 8.3). Lock your composer.json and don't assume an upgrade path is free.

Numeric auto-detection will bite you, repeatedly. The general fix is exportFormat('@') on any identifier-like column. Do a manual pass over every export before shipping — open the file in Excel and LibreOffice, because they disagree about what a "number" is.

Empty values used to become null (fixed 13.1.1) and styles used to be duplicated (fixed 13.2.1). Both are the kind of bug that looks like your data is wrong. If you're on an older patch, pin to ≥13.2.1.

Filename behavior changed in 13.3.1 ("reuse data table filename"). If a global naming convention lives in your base DataTable class and you also pass per-button filename, confirm which one wins after upgrading — this is exactly the sort of thing that breaks quietly in a patch release.

Custom filters only travel if they're in the request payload. The export button serializes the table's current search, ordering, and filters. If you have a custom filter implemented as a separate form field that your DataTable's ajax config attaches manually, the export won't include it, and users will get unfiltered data believing it's filtered. Verify with a filtered export where the filter is obvious (e.g. a single-user filter).

Queue workers pick up code changes only on restart. Standard queue caveat, but here it manifests as "my new exportFormat didn't take effect" — because the worker is still running the old class. php artisan queue:restart after every deploy.

Progress is per-row since 13.3.0 and the protocol was hardened in the same release. On older versions the progress bar interpolates batch completion rather than row completion, which is fine for 3 batches and misleading for 300. If accurate progress matters, you need ≥13.3.0.

Test with Queue::fake(). Asserting the job was dispatched and its payload contains the right filters is far more valuable than asserting a file appeared on disk — the latter couples your test to OpenSpout internals that have already churned across minor versions.

Maturity check before you commit. 36 stars and zero dependents means you are close to the bleeding edge. It's a first-party Yajra package with CI and static analysis, which is reassuring, but expect to read the source rather than the README when something doesn't behave. The good news: the surface area is small, the extension points (WithExportQueue, sheetName(), exportFormat, date_formats config, lifecycle events, configurable queue) cover most reasonable customization, and the changelog is well-kept — which is roughly what you want from a dependency this young.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings