Search by

akika / laravel-exporter

akika.digital

Asynchronous, queue-based data exports for Laravel applications.

Package info

github.com/akikadigital/laravel-exporter

pkg:composer/akika/laravel-exporter

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.2 2026-09-20 08:38 UTC

This package is auto-updated.

Last update: 2026-09-20 09:53:01 UTC


README

Asynchronous, queue-based data exports for Laravel applications.

Laravel Exporter is designed for exporting large datasets without keeping an HTTP request open while the export is generated.

Instead of generating a large CSV directly inside a controller or Livewire request, the package creates an export record, dispatches the work to Laravel's queue, processes the dataset in chunks, stores the resulting file, and tracks the export lifecycle.

Features

  • Asynchronous queue-based exports
  • Large dataset processing using chunking
  • CSV exports
  • Export progress tracking
  • Export lifecycle management
  • User/owner-associated exports
  • Export cancellation
  • Expiring exports
  • Automatic pruning
  • Duplicate active-export prevention
  • Export lifecycle events
  • Laravel filesystem-backed storage
  • Signed download URLs
  • Configurable queue connection and queue
  • Configurable job attempts and timeout
  • Configurable export storage and expiration
  • Framework-agnostic core with no Livewire dependency

Requirements

  • PHP 8.2+
  • Laravel 11, 12 or 13
  • A configured Laravel queue

Installation

Install the package using Composer:

composer require akika/laravel-exporter

Laravel will automatically discover the package service provider.

Publish the Configuration

Publish the package configuration:

php artisan vendor:publish --tag=exporter-config

The configuration file will be published to:

config/exporter.php

The package supports the following environment variables:

EXPORTER_DISK=local
EXPORTER_PATH=exports

EXPORTER_QUEUE_CONNECTION=
EXPORTER_QUEUE=exports
EXPORTER_QUEUE_TRIES=3
EXPORTER_QUEUE_TIMEOUT=3600

EXPORTER_CHUNK_SIZE=1000
EXPORTER_EXPIRES_AFTER_DAYS=7

EXPORTER_PROGRESS_EVENT_INTERVAL=5

EXPORTER_DOWNLOAD_URL_EXPIRES_AFTER=15
EXPORTER_ROUTE_PREFIX=exports

EXPORTER_PRUNE_DELETE_RECORDS=true

All values are optional. The package will use the defaults defined in config/exporter.php when an environment variable is not provided.

Publish the Migrations

php artisan vendor:publish --tag=exporter-migrations

Then run:

php artisan migrate

The package uses its database tables to track export state, progress, metadata, expiration, and active export locks.

Configuration

The package configuration is located at:

config/exporter.php

You can configure:

  • Storage disk and export path
  • Queue connection and queue name
  • Queue attempts and timeout
  • Database chunk size
  • Export lifetime
  • Progress event interval
  • Signed download URL lifetime
  • Download route prefix
  • Route middleware
  • Expired export pruning behavior

Storage

By default, generated exports are stored using Laravel's local filesystem disk:

EXPORTER_DISK=local
EXPORTER_PATH=exports

Any compatible disk configured in config/filesystems.php may be used.

Queue

Exports are processed asynchronously.

You may configure a dedicated queue:

EXPORTER_QUEUE_CONNECTION=redis
EXPORTER_QUEUE=exports
EXPORTER_QUEUE_TRIES=3
EXPORTER_QUEUE_TIMEOUT=3600

If EXPORTER_QUEUE_CONNECTION is not configured, Laravel's default queue connection is used.

Chunk Size

Large datasets are processed in chunks to avoid loading the complete result set into memory:

EXPORTER_CHUNK_SIZE=1000

Larger values may improve throughput while consuming more memory. Smaller values reduce memory usage but may result in additional database queries.

Export Lifetime

Completed, failed, and cancelled exports receive an expiration date:

EXPORTER_EXPIRES_AFTER_DAYS=7

Expired exports can subsequently be removed using the exporter:prune command.

Creating an Export

Create an export class that implements the Exportable contract.

<?php

namespace App\Exports;

use Akika\LaravelExporter\Concerns\InteractsWithExportOptions;
use Akika\LaravelExporter\Contracts\Exportable;
use App\Models\Purchase;
use Illuminate\Database\Eloquent\Builder;

class PurchasesExport implements Exportable
{
    use InteractsWithExportOptions;

    public function query(): Builder
    {
        return Purchase::query()
            ->when(
                $this->option('store_id'),
                fn (Builder $query, $storeId) =>
                    $query->where('store_id', $storeId)
            )
            ->orderBy('id');
    }

    public function headings(): array
    {
        return [
            'ID',
            'Reference',
            'Amount',
            'Created At',
        ];
    }

    public function map(mixed $purchase): array
    {
        return [
            $purchase->id,
            $purchase->reference,
            $purchase->amount,
            $purchase->created_at?->toDateTimeString(),
        ];
    }

    public function validateOptions(): void
    {
        //
    }
}

Every exporter implements four core methods:

query()
headings()
map()
validateOptions()

query() returns the query supplying the export records.

headings() defines the output columns.

map() transforms each record into an export row.

validateOptions() validates options supplied to the export. When an exporter does not require option validation, the method may remain empty.

Validating Export Options

Export-specific options can be validated before the export is processed.

For example:

public function validateOptions(): void
{
    validator(
        $this->options(),
        [
            'store_id' => [
                'nullable',
                'integer',
            ],
            'from' => [
                'nullable',
                'date',
            ],
            'to' => [
                'nullable',
                'date',
                'after_or_equal:from',
            ],
        ]
    )->validate();
}

This allows invalid export requests to fail before unnecessary queue work is performed.

Queueing an Export

Use the Exporter facade to create and queue an export:

use Akika\LaravelExporter\Facades\Exporter;
use App\Exports\PurchasesExport;

$export = Exporter::make(PurchasesExport::class)
    ->name('Purchases')
    ->for(auth()->user())
    ->with([
        'store_id' => 12,
    ])
    ->queue();

The call returns an Export model while the actual file is generated asynchronously by the queue.

The application does not need to keep the HTTP request open while the dataset is exported.

Export Options

Use with() to pass application-specific options to an exporter:

$export = Exporter::make(PurchasesExport::class)
    ->with([
        'store_id' => 12,
        'from' => '2026-09-01',
        'to' => '2026-09-30',
    ])
    ->queue();

Inside the exporter, individual options can be accessed using:

$storeId = $this->option('store_id');

A default value may also be provided:

$storeId = $this->option(
    'store_id',
    null
);

Associating an Export With an Owner

An export may be associated with an Eloquent model, such as the authenticated user:

$export = Exporter::make(PurchasesExport::class)
    ->for(auth()->user())
    ->queue();

The owner is stored using a polymorphic relationship.

This allows applications to retrieve exports belonging to a particular user or other model without the package being coupled to a specific authentication model.

Unique Exports

Use unique() to prevent identical active export requests from being queued multiple times:

$export = Exporter::make(PurchasesExport::class)
    ->for(auth()->user())
    ->with([
        'store_id' => 12,
    ])
    ->unique()
    ->queue();

If an identical export is already pending or processing, the existing export is returned instead of creating another export and queue job.

Once that export reaches a terminal state, another identical export may be created.

The fingerprint used for duplicate detection considers the export definition, owner, format, and canonicalized options.

unique() provides request idempotency. It is not export-result caching.

Checking Progress

Refresh the export model to retrieve its latest state:

$export->refresh();

$export->status;
$export->processed_rows;
$export->total_rows;
$export->progress;
$export->remaining_rows;

progress is calculated from the processed and total row counts rather than being stored as a separate database value.

For example:

if ($export->isProcessing()) {
    echo "{$export->progress}%";
}

Status Helpers

The Export model provides convenient lifecycle helpers:

$export->isPending();
$export->isProcessing();
$export->isCompleted();
$export->isFailed();
$export->isCancelled();
$export->isActive();
$export->isFinished();

These helpers avoid requiring applications to directly compare enum values throughout their code.

Querying Exports

Exports can be queried using the provided model scopes.

For example, retrieve exports belonging to the authenticated user:

use Akika\LaravelExporter\Models\Export;

$exports = Export::query()
    ->forOwner(auth()->user())
    ->latest()
    ->paginate();

Available status scopes include:

Export::query()->pending();
Export::query()->processing();
Export::query()->active();
Export::query()->completed();
Export::query()->failed();
Export::query()->cancelled();
Export::query()->finished();
Export::query()->expired();

Scopes may be combined with normal Eloquent queries:

$exports = Export::query()
    ->forOwner(auth()->user())
    ->completed()
    ->latest()
    ->paginate();

Cancelling an Export

An export that has not reached a terminal state may be cancelled:

$export->cancel();

A cancelled export enters the CANCELLED state and receives an expiration date.

Queue workers processing exports should respect the cancellation state while processing chunks.

Downloads

Once an export has completed, a signed download URL can be generated:

$url = $export->downloadUrl();

downloadUrl() returns null when the export is not currently downloadable.

For code that requires a valid download URL, use:

$url = $export->requireDownloadUrl();

requireDownloadUrl() throws an ExportNotDownloadableException when the export cannot be downloaded.

The default signed URL lifetime is configured using:

EXPORTER_DOWNLOAD_URL_EXPIRES_AFTER=15

The default download route is protected by the configured route middleware.

Generate download URLs when they are needed rather than storing them permanently, since signed URLs are intentionally short-lived.

Export Lifecycle

A successful export normally follows this lifecycle:

PENDING
   │
   ▼
PROCESSING
   │
   ▼
COMPLETED

An export may instead reach:

FAILED

or:

CANCELLED

The active states are:

PENDING
PROCESSING

The terminal states are:

COMPLETED
FAILED
CANCELLED

Terminal exports receive an expiration date and may later be pruned.

Events

Laravel Exporter dispatches Laravel events throughout the export lifecycle.

Available lifecycle events include:

ExportStarted
ExportProgressUpdated
ExportCompleted
ExportFailed
ExportCancelled

Applications may listen for these events to implement:

  • Browser notifications
  • Broadcasting
  • Email notifications
  • Database notifications
  • Audit logging
  • Application-specific workflows
  • UI progress updates

For example:

use Akika\LaravelExporter\Events\ExportCompleted;
use Illuminate\Support\Facades\Event;

Event::listen(
    ExportCompleted::class,
    function (ExportCompleted $event) {
        // Handle completed export.
    }
);

The package deliberately stops at Laravel lifecycle events.

The core package does not require Livewire, Laravel Echo, Reverb, WebSockets, or any particular frontend framework.

Lifecycle events are intentionally framework-agnostic. Applications may choose how those events are consumed, including polling, database notifications, broadcasting, email, or custom application workflows.

Laravel Exporter does not configure broadcasting or frontend notifications on behalf of the consuming application.

Progress Events

Progress events are emitted according to the configured percentage interval:

EXPORTER_PROGRESS_EVENT_INTERVAL=5

With the default value, progress events are emitted approximately when the export advances through:

5%
10%
15%
20%
...
100%

This prevents an event from being emitted for every exported row when processing large datasets.

Applications are not required to consume progress events. Export progress can also be retrieved directly from the Export model, making approaches such as Livewire or AJAX polling possible without additional broadcasting infrastructure.

Pruning Expired Exports

Expired exports can be cleaned up using:

php artisan exporter:prune

The command removes expired export files.

When configured to do so, it also removes the corresponding database records:

EXPORTER_PRUNE_DELETE_RECORDS=true

The command additionally performs defensive cleanup of stale export locks.

Scheduling Pruning

You should run the prune command periodically using Laravel's scheduler.

For example:

use Illuminate\Support\Facades\Schedule;

Schedule::command('exporter:prune')
    ->daily();

Ensure Laravel's scheduler itself is configured to run in your production environment.

Queue Worker

Exports require a running Laravel queue worker.

For example:

php artisan queue:work

If exports use the dedicated exports queue:

php artisan queue:work --queue=exports

Production applications should manage queue workers using an appropriate process supervisor or Laravel Horizon.

The package provides configuration for queue attempts and job timeout:

EXPORTER_QUEUE_TRIES=3
EXPORTER_QUEUE_TIMEOUT=3600

Large exports may require an appropriate timeout based on dataset size, database performance, and storage performance.

Duplicate Request Protection

Calling unique() prevents duplicate active export requests.

For example, repeated requests such as:

PurchasesExport
Owner: User #15
Format: CSV
store_id: 12
from: 2026-09-01
to: 2026-09-30

produce the same canonical fingerprint.

While an identical export is pending or processing, Laravel Exporter returns the existing export instead of creating another one.

When the export reaches a terminal state, its active lock is released.

This is particularly useful for preventing accidental duplicate exports caused by repeated button clicks or duplicate HTTP requests.

How It Works

User requests export
        │
        ▼
Exporter::make(...)
        │
        ├── validate exporter
        ├── validate configuration
        ├── validate options
        └── optionally calculate fingerprint
        │
        ▼
Export record
    PENDING
        │
        ▼
ProcessExport job
        │
        ▼
   PROCESSING
        │
        ├── rebuild exporter
        ├── validate options
        ├── execute query
        ├── process records in chunks
        ├── map rows
        ├── write CSV
        ├── update progress
        └── dispatch lifecycle events
        │
        ▼
    COMPLETED
        │
        ▼
Signed download URL
        │
        ▼
    Expiration
        │
        ▼
 exporter:prune

If processing fails:

PROCESSING
    │
    ▼
  FAILED

If the export is cancelled:

PENDING / PROCESSING
        │
        ▼
    CANCELLED

Application Integration

Laravel Exporter is responsible for generating and tracking exports. User-interface behavior belongs to the consuming application.

For example, an application may periodically query active exports:

use Akika\LaravelExporter\Models\Export;

$exports = Export::query()
    ->forOwner(auth()->user())
    ->active()
    ->get();

A Livewire application may use conditional polling while exports are active. Applications requiring real-time push notifications may listen to the package lifecycle events and implement Laravel broadcasting separately.

This separation keeps the package usable by traditional Laravel applications, Livewire applications, API backends, Vue or React frontends, and other Laravel-based architectures without requiring frontend-specific dependencies.

Security

Generated exports may contain sensitive application data.

Applications are responsible for ensuring that users are authorized to request, view, and download exports.

Laravel Exporter uses signed download URLs, but signed URLs should not be treated as a replacement for application authorization where sensitive data is involved.

The default download route uses:

[
    'web',
    'auth',
]

Applications using a different authentication mechanism may publish the configuration and change the route middleware.

Avoid using a publicly accessible filesystem disk for sensitive exports unless public access is intentional.

For sensitive exports:

  • Use an appropriate private filesystem disk.
  • Keep authentication and authorization checks in place.
  • Use short-lived signed download URLs.
  • Configure an appropriate export expiration period.
  • Schedule exporter:prune regularly.
  • Avoid exposing another user's export records or download URLs.

Error Handling

Laravel Exporter provides package-specific exceptions for errors that applications may need to handle.

These include exceptions such as:

ExporterException
InvalidExporterException
ExportConfigurationException
UnsupportedExportFormatException
ExportNotDownloadableException

For example:

use Akika\LaravelExporter\Exceptions\ExporterException;

try {
    $export = Exporter::make(PurchasesExport::class)
        ->queue();
} catch (ExporterException $exception) {
    // Handle exporter-specific errors.
}

Validation exceptions raised by validateOptions() retain Laravel's normal validation behavior.

Testing

Run the package test suite using:

composer test

Or run PHPUnit directly:

vendor/bin/phpunit

The test suite covers core functionality including:

  • Export creation
  • CSV generation
  • Export options
  • Export lifecycle
  • Progress calculation
  • Lifecycle events
  • Cancellation
  • Fingerprinting
  • Duplicate export prevention
  • Lock release
  • Owner scopes
  • Status scopes
  • Expiration
  • Pruning
  • Configuration validation
  • Invalid exporter handling

Development

Clone the repository and install its dependencies:

composer install

Validate the Composer configuration:

composer validate --strict

Regenerate Composer's autoloader when necessary:

composer dump-autoload

Run the complete test suite:

composer test

Changes should not be committed unless the existing test suite remains green.

License

Laravel Exporter is open-source software licensed under the MIT license.