PHPackages                             sinemacula/laravel-resource-exporter - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. sinemacula/laravel-resource-exporter

ActiveLibrary[API Development](/categories/api)

sinemacula/laravel-resource-exporter
====================================

Content-negotiated Laravel API resource exports to CSV, TSV, XLSX, XML, JSON, and NDJSON.

v3.1.0(3w ago)019.2k↓19.2%1Apache-2.0PHPPHP ^8.3CI passing

Since Aug 21Pushed 2w ago1 watchersCompare

[ Source](https://github.com/sinemacula/laravel-resource-exporter)[ Packagist](https://packagist.org/packages/sinemacula/laravel-resource-exporter)[ RSS](/packages/sinemacula-laravel-resource-exporter/feed)WikiDiscussions master Synced 2w ago

READMEChangelog (9)Dependencies (65)Versions (20)Used By (1)

Laravel Resource Exporter
=========================

[](#laravel-resource-exporter)

[![Latest Stable Version](https://camo.githubusercontent.com/7b9a7f08e324411cbb0ee67065102c99408811da2075295645ab372103a2b7ff/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73696e656d6163756c612f6c61726176656c2d7265736f757263652d6578706f727465722e737667)](https://packagist.org/packages/sinemacula/laravel-resource-exporter)[![Build Status](https://github.com/sinemacula/laravel-resource-exporter/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/sinemacula/laravel-resource-exporter/actions/workflows/tests.yml)[![Quality Gates](https://github.com/sinemacula/laravel-resource-exporter/actions/workflows/quality-gates.yml/badge.svg?branch=master)](https://github.com/sinemacula/laravel-resource-exporter/actions/workflows/quality-gates.yml)[![Maintainability](https://camo.githubusercontent.com/e574aee2b27955176b9426a95d95292a833fe2a0f0dead8fca20093f9f35c3a7/68747470733a2f2f716c74792e73682f67682f73696e656d6163756c612f70726f6a656374732f6c61726176656c2d7265736f757263652d6578706f727465722f6d61696e7461696e6162696c6974792e737667)](https://qlty.sh/gh/sinemacula/projects/laravel-resource-exporter)[![Code Coverage](https://camo.githubusercontent.com/715a60b1e368eda316b7875fc83454b48a6f50df5128023bf9ef4635c262410a/68747470733a2f2f716c74792e73682f67682f73696e656d6163756c612f70726f6a656374732f6c61726176656c2d7265736f757263652d6578706f727465722f636f7665726167652e737667)](https://qlty.sh/gh/sinemacula/projects/laravel-resource-exporter)[![Total Downloads](https://camo.githubusercontent.com/6da65f90e81b1a0ddb75d3f2401e253b6e968385038a32b06907bd2a20c113cf/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73696e656d6163756c612f6c61726176656c2d7265736f757263652d6578706f727465722e737667)](https://packagist.org/packages/sinemacula/laravel-resource-exporter)

Return any API resource in the format the client asks for. Add one trait to a `JsonResource`, declare the columns that make sense as a table, and the same endpoint that serves JSON will stream **CSV, TSV, XLSX, XML, JSON, or NDJSON** when the request asks for it - over the `Accept` header or a `?format=` query parameter - with no second controller and no bespoke serialization code.

Rows are pulled lazily from the source, so CSV, TSV, JSON, XML, and NDJSON exports stay flat in memory even for large queries. XLSX is built as a temporary workbook before the response can flush, so large spreadsheets are best queued to disk behind a signed URL. The same engine drives inline downloads, queued exports, and one-off fluent exports.

The package is registered automatically through Laravel's package auto-discovery; publish the config (see below) to register custom formats or tune the negotiation limits.

How It Works
------------

[](#how-it-works)

There are two doors onto one streaming engine:

- **Content negotiation.** A `JsonResource` that uses the `RespondsWithExports` trait keeps serving JSON by default but answers `Accept: text/csv` (or `?format=csv`) by streaming the negotiated format instead - for both a single resource and the collection/page returned by the route. Use `ResourceExport::forQuery()` or a queued export when the export request should switch from paginated JSON to the full query. JSON stays first-class: a browser's default `Accept`always resolves to JSON, `q=0` is honoured, and every response carries `Vary: Accept`.
- **Explicit exports.** The `Exporter` facade builds an export from a resource, a collection, or a query and returns a fluent builder whose verbs - `download()`, `store()`, `toString()`, `toStream()`, `toResponse()` - decide where the bytes go. The dedicated `Exporter::queue($model, $resource)` entry point builds serializable queued tabular exports.

Formats split by **dimensionality**:

- **Hierarchical** formats (JSON, XML, NDJSON) serialize the resource's existing `toArray()` shape, one item at a time. They need no extra configuration and honour the resource's field gating.
- **Tabular** formats (CSV, TSV, XLSX) are single-dimensional and cannot flatten arbitrary nesting, so the resource declares a `TabularSchema` - an explicit list of `Column`s with casts, aggregates, and visibility - that defines exactly what a row looks like. A resource without a tabular schema still negotiates the hierarchical formats and returns `406 Not Acceptable` for a tabular one.

A few rules hold across the surface:

- **Lazy row pipeline.** Rows are pulled from the source lazily (keyset `lazyById` pagination for queries) and passed straight through the engine. Textual and hierarchical writers flush progressively; XLSX finalises a temporary workbook before handing it to the sink.
- **Stateless and Octane-safe.** Nothing caches the request or mutable state between exports; the media-type registry is built once at boot and read-only thereafter.
- **Extensible by configuration.** Register a custom format in `config/exporter.php`; the negotiated and explicit paths resolve the same registry, so they always agree on the available formats.

Installation
------------

[](#installation)

```
composer require sinemacula/laravel-resource-exporter
```

The service provider is auto-discovered. XLSX support is optional - pull it in when you need it:

```
composer require "openspout/openspout:^4.0"
```

Configuration
-------------

[](#configuration)

Publish the package configuration:

```
php artisan vendor:publish --provider="SineMacula\Exporter\ExporterServiceProvider"
```

This creates `config/exporter.php`, where you can control:

KeyDescriptionDefault`default`Default format for explicit export builders (env `EXPORTER_DEFAULT`).`csv``alias`The container / facade accessor alias for the manager (env `EXPORTER_ALIAS`).`exporter``formats`Custom negotiable formats registered with the shared media-type registry.`[]``negotiation`Query-endpoint settings: `default_format`, `max_rows`, `per_page`, `chunk_size`.see file`audit`Optional log `channel` for the `ExportCompleted` audit payload.`null`Usage
-----

[](#usage)

### Content negotiation

[](#content-negotiation)

Add the `RespondsWithExports` trait and implement `ProvidesTabularExport` on your resource. The resource keeps returning JSON; a `TabularSchema` describes the table the tabular formats produce:

```
// app/Http/Resources/UserResource.php
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use SineMacula\Exporter\Contracts\ProvidesTabularExport;
use SineMacula\Exporter\Http\Concerns\RespondsWithExports;
use SineMacula\Exporter\Schema\TabularSchema;

class UserResource extends JsonResource implements ProvidesTabularExport
{
    use RespondsWithExports;

    public function toArray(Request $request): array
    {
        return [
            'id'    => $this->id,
            'name'  => $this->name,
            'email' => $this->email,
        ];
    }

    public function tabular(Request $request): TabularSchema
    {
        return new UserExportSchema($request);
    }
}
```

```
// app/Exports/UserExportSchema.php
use SineMacula\Exporter\Schema\Column;
use SineMacula\Exporter\Schema\TabularSchema;

class UserExportSchema extends TabularSchema
{
    public function columns(): array
    {
        return [
            Column::make('id', 'ID'),
            Column::make('name', 'Name'),
            Column::make('email', 'Email'),
            Column::make('created_at', 'Joined')->date('Y-m-d'),
            Column::make('orders', 'Orders')->count(),
        ];
    }
}
```

The route is unchanged - return the resource or collection exactly as you already do:

```
Route::get('/users', fn () => UserResource::collection(User::query()->paginate()));
```

The response format then follows the request:

```
GET /users                              -> JSON (default)
GET /users    Accept: text/csv          -> streamed CSV download
GET /users?format=xlsx                  -> streamed XLSX download
GET /users    Accept: application/xml   -> streamed XML
```

### Supported formats

[](#supported-formats)

FormatMedia type(s)Shape`json``application/json`Hierarchical (default)`csv``text/csv`Tabular`tsv``text/tab-separated-values`Tabular`xlsx``application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`Tabular`xml``application/xml`, `text/xml`Hierarchical`ndjson``application/x-ndjson`, `application/jsonl`Hierarchical### Columns

[](#columns)

`Column::make($key, $heading)` reads `$key` off each row; chainable modifiers cast, format, aggregate, and gate it:

```
Column::make('created_at', 'Joined')->date('Y-m-d');              // date / dateTime
Column::make('total', 'Total')->number(2);                        // numeric formatting
Column::make('active', 'Active')->boolean('Yes', 'No');           // boolean labels
Column::make('status', 'Status')->enum();                         // backed enum -> value
Column::make('orders', 'Orders')->count();                        // has-many count (withCount)
Column::make('orders', 'Revenue')->sum('total');                  // has-many sum (withSum)
Column::make('orders', 'SKUs')->join(', ');                        // join scalar/Stringable children
Column::make('full_name', 'Name')
    ->resolveUsing(fn ($user) => "{$user->first} {$user->last}"); // computed value
```

Count and sum aggregates are folded into the query (`withCount` / `withSum`); join aggregates eager-load the relation, so none add per-row queries.

### Column visibility

[](#column-visibility)

Tabular exports (CSV / TSV / XLSX) resolve each column straight off the raw model attribute via `data_get`. That read is deliberately fast and constant-memory, but it does **not** route through the resource's `toArray()` / `$hidden` / `when()` gating - so a `$hidden` attribute is still emitted, and field visibility is not inherited from the resource. Gating a field out of an export is the schema author's job, expressed with `->visible()` on the column:

```
use SineMacula\Exporter\Schema\Column;
use Illuminate\Http\Request;

Column::make('salary', 'Salary')
    ->visible(static fn (Request $request): bool => $request->user()?->isAdmin() ?? false);
```

A column whose `->visible()` gate returns `false` is dropped entirely - from the heading row and from every data row - so its value can never leak through any cell or aggregate path. The gate sees only the request (no row) and is evaluated once when the schema is built.

### Explicit exports

[](#explicit-exports)

When you want bytes on demand rather than over HTTP, build an export through the facade:

```
use SineMacula\Exporter\Facades\Exporter;

// Inline download response
return Exporter::collection(UserResource::collection($users))
    ->format('csv')
    ->download('users.csv');

// Store on a disk, get the path back
$path = Exporter::query(User::query()->where('active', true), UserResource::class)
    ->format('xlsx')
    ->store('s3', 'exports/users.xlsx');

// Raw string
$csv = Exporter::export(new UserResource($user))->format('csv')->toString();
```

### Query endpoint (paginated JSON or full export)

[](#query-endpoint-paginated-json-or-full-export)

`ResourceExport::forQuery()` serves a paginated JSON collection for a normal request and streams the full dataset for an export request, from one route - with the safety rails a full-table stream needs:

```
use SineMacula\Exporter\ResourceExport;

Route::get('/users/export', function () {
    return ResourceExport::forQuery(User::query(), UserResource::class)
        ->authorizeUsing(fn ($request) => $request->user()->can('export', User::class))
        ->paginatedJsonOrStreamedExport();
});
```

The full set is capped at `negotiation.max_rows` (default 10000); raise it per call with `->unlimited()` or `->maxRows()`, or queue the export. Authorization is explicit: call `authorizeUsing()` (or `withoutAuthorization()` to opt out deliberately), or the streamed export refuses to run.

### Queued exports

[](#queued-exports)

For large tabular datasets, queue the export to a disk and hand the user a signed URL when it lands:

```
use SineMacula\Exporter\Facades\Exporter;

Exporter::queue(User::class, UserResource::class)
    ->schema(UserExportSchema::class)
    ->format('xlsx')
    ->toDisk('s3', 'exports/users.xlsx')
    ->by($request->user())
    ->authorize('export')
    ->queue();
```

The queued pipeline writes tabular formats (CSV, TSV, XLSX). It streams chunk by chunk into a local staging file and fires `ExportStarting`, `RowsExported`, `ExportCompleted` (carrying a signed temporary URL), and `ExportFailed`. The query is described by a serializable specification - constrain it with the query verbs on the builder rather than passing a live builder. HTTP streaming failures after bytes have started are reported separately through `StreamExportFailed`, with the format, actor and number of rows written.

### Testing

[](#testing)

`Exporter::fake()` swaps the engine for a recorder, so tests assert what would have been exported without producing bytes or dispatching jobs - mirroring `Storage::fake()` and `Bus::fake()`:

```
use SineMacula\Exporter\Facades\Exporter;

$exporter = Exporter::fake();

// ... exercise code that exports ...

$exporter->assertDownloaded('users.csv');
$exporter->assertStored('s3', 'exports/users.xlsx');
$exporter->assertQueued();
$exporter->assertExportedRows(42);
```

### Custom formats

[](#custom-formats)

Register an additional negotiable format through the `formats` block in `config/exporter.php`; the negotiated and explicit paths share one registry, so a custom format is reachable from both.

For a config-cache friendly tabular format, bind an `ExportFormat` in a service provider and list the binding key in config:

```
// app/Providers/AppServiceProvider.php
use App\Exports\Writers\ReportWriter;
use SineMacula\Exporter\Http\ExportFormat;

public function register(): void
{
    $this->app->singleton('exports.formats.report', static fn (): ExportFormat => new ExportFormat(
        'report',
        'report',
        'application/x-report',
        ['application/x-report'],
        true,
        static fn (): ReportWriter => new ReportWriter,
    ));
}
```

```
// config/exporter.php
'formats' => [
    'exports.formats.report',
],
```

Closures and `ExportFormat` instances also work when the config file is not cached.

Requirements
------------

[](#requirements)

- PHP ^8.3
- Laravel ^12.0

Testing
-------

[](#testing-1)

```
composer test                # PHPUnit suite in parallel via Paratest
composer test:coverage       # suite with Clover coverage output
composer test:mutation       # Infection mutation gate (min MSI 90)
composer test:mutation:full  # full mutation suite without thresholds
composer check               # static analysis and lint via qlty
composer format              # format via qlty
composer smells              # duplication / complexity smells via qlty
composer bench               # PHPBench suite for the hot paths
composer bench:ci            # PHPBench with CI artifact dump
composer bench:smoke         # single-rev pass to verify every subject runs
```

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md) for a list of notable changes.

Contributing
------------

[](#contributing)

Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on branching, commits, code quality, and pull requests.

Security
--------

[](#security)

If you discover a security vulnerability, please report it responsibly. See [SECURITY.md](SECURITY.md) for the disclosure policy and contact details.

License
-------

[](#license)

Licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).

###  Health Score

53

—

FairBetter than 96% of packages

Maintenance96

Actively maintained with recent releases

Popularity26

Limited adoption so far

Community16

Small or concentrated contributor base

Maturity64

Established project with proven stability

 Bus Factor1

Top contributor holds 57.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~87 days

Recently: every ~57 days

Total

9

Last Release

27d ago

Major Versions

v1.1.2 → v2.0.02026-02-20

v2.0.1 → v3.0.02026-06-30

### Community

Maintainers

![](https://www.gravatar.com/avatar/6262ea965c244b0c946a2f29a94da05e30846c066a0b59399466216654c78fe6?d=identicon)[sinemacula](/maintainers/sinemacula)

---

Top Contributors

[![sinemacula-ben](https://avatars.githubusercontent.com/u/118753672?v=4)](https://github.com/sinemacula-ben "sinemacula-ben (39 commits)")[![sine-macula-dependencies[bot]](https://avatars.githubusercontent.com/in/4130001?v=4)](https://github.com/sine-macula-dependencies[bot] "sine-macula-dependencies[bot] (25 commits)")[![sine-macula-releases[bot]](https://avatars.githubusercontent.com/in/4070845?v=4)](https://github.com/sine-macula-releases[bot] "sine-macula-releases[bot] (2 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![michaelstivala](https://avatars.githubusercontent.com/u/4493561?v=4)](https://github.com/michaelstivala "michaelstivala (1 commits)")

---

Tags

apilaravelexportresourcesine macula

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/sinemacula-laravel-resource-exporter/health.svg)

```
[![Health](https://phpackages.com/badges/sinemacula-laravel-resource-exporter/health.svg)](https://phpackages.com/packages/sinemacula-laravel-resource-exporter)
```

###  Alternatives

[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[statamic/cms

The Statamic CMS Core Package

4.9k3.8M1.2k](/packages/statamic-cms)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

818355.4k3](/packages/defstudio-telegraph)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5226.7k](/packages/simplestats-io-laravel-client)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
