PHPackages                             hasanhawary/export-builder - 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. [PDF &amp; Document Generation](/categories/documents)
4. /
5. hasanhawary/export-builder

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

hasanhawary/export-builder
==========================

A modular Laravel export builder around maatwebsite/excel (CSV/XLS/XLSX,PDF)

2.4.3(4d ago)2540MITPHPPHP &gt;=8.1 &lt;8.6

Since Sep 21Pushed 3w agoCompare

[ Source](https://github.com/hasanhawary/export-builder)[ Packagist](https://packagist.org/packages/hasanhawary/export-builder)[ Docs](https://github.com/hasanhawary/export-builder)[ RSS](/packages/hasanhawary-export-builder/feed)WikiDiscussions main Synced 2d ago

READMEChangelogDependencies (14)Versions (21)Used By (0)

Export Builder
==============

[](#export-builder)

[![Latest Stable Version](https://camo.githubusercontent.com/bc6fd1cd84846a4ebffcdcd92576da2e61163263660bf09d5948d8f50f8e98a5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f686173616e6861776172792f6578706f72742d6275696c6465722e737667)](https://packagist.org/packages/hasanhawary/export-builder)[![Total Downloads](https://camo.githubusercontent.com/1af148ad26c943fbdf56c5e701e5d6af7a8ad4c09fc7e49b13ed4ccd5546ff00/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646d2f686173616e6861776172792f6578706f72742d6275696c6465722e737667)](https://packagist.org/packages/hasanhawary/export-builder)[![PHP Version](https://camo.githubusercontent.com/cb89b4b720744af4b7130b8314ae8aec618a975c1fd74bf000f848e25646e800/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f686173616e6861776172792f6578706f72742d6275696c6465722e737667)](https://packagist.org/packages/hasanhawary/export-builder)[![License](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

A modular Laravel package for building reusable, memory-safe Excel and PDF exports.
Define one export class, get XLSX / XLS / CSV / PDF output, direct download or queued job — with relation support, advanced filters, permission gating, and full translation support.

---

Table of Contents
-----------------

[](#table-of-contents)

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Export Formats](#export-formats)
- [Column Types](#column-types)
- [Relation Exports](#relation-exports)
- [Custom Relations (Easy Mode)](#custom-relations-easy-mode)
- [Morph Relation Exports](#morph-relation-exports)
- [Advanced Filters](#advanced-filters)
- [Column Selection Filter](#column-selection-filter)
- [PDF Output](#pdf-output)
- [Queued Exports](#queued-exports)
- [Routes](#routes)
- [Permissions](#permissions)
- [Translations](#translations)
- [Overriding Controllers and Services](#overriding-controllers-and-services)
- [Performance](#performance)
- [Testing](#testing)

---

Features
--------

[](#features)

- **XLSX, XLS, CSV, PDF** — one export class drives all formats
- **Memory-safe streaming** — `lazyById()` cursor, configurable chunk size, peak memory stays flat regardless of dataset size
- **Direct download or queued job** — same export class, two delivery modes
- **Relation support** — belongs-to/has-one, has-many concat, has-many list, count with alias, nested dot-notation, polymorphic
- **Advanced filters** — `whereIn`, `whereHas`, morph constraints, enum resolvers
- **Column selection filter** — client can request a subset of columns at runtime
- **Automatic type formatting** — text, int, float, money, date, datetime, bool, array, classPath, Enum
- **Full translation support** — English and Arabic built-in; headings and bool values auto-translated
- **Permission gating** — per-page permission config, custom resolver override, scoped list visibility
- **Safe package routes** — never claims `/export`; host routes always win on conflict
- **Publishable** — config, views, migrations, and lang files are all publishable
- **Contracts / interfaces** — `BaseExportContract` for type-hinting custom implementations
- **121 tests, 257 assertions**

---

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

[](#requirements)

DependencyVersionPHP8.1 – 8.5Laravel10, 11, 12, 13`maatwebsite/excel`^3.1 or ^4.0`carlos-meneses/laravel-mpdf`^2.0---

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

[](#installation)

```
composer require hasanhawary/export-builder
```

Laravel auto-discovers the service provider. No manual registration needed.

**Optional publishes:**

```
# Config
php artisan vendor:publish --tag=export-builder-config

# PDF Blade view (customise the template)
php artisan vendor:publish --tag=export-builder-views

# Language files (en + ar)
php artisan vendor:publish --tag=export-builder-lang
# Publishes to: lang/en/export.php and lang/ar/export.php

# Migration for queued export history
php artisan vendor:publish --tag=export-builder-migrations
php artisan migrate
```

Published lang files land in `lang/vendor/export/{en,ar}/export.php`.

---

Quick Start
-----------

[](#quick-start)

Create an export class in the configured namespace (default: `App\Tools\Export`):

```
namespace App\Tools\Export;

use App\Models\User;
use HasanHawary\ExportBuilder\BaseExport;

class UserExport extends BaseExport
{
    public function __construct(array $filter)
    {
        parent::__construct([
            'model'   => User::class,
            'columns' => [
                'id'         => 'int',
                'name'       => 'text',
                'email'      => 'text',
                'is_active'  => 'bool',
                'created_at' => 'datetime',
            ],
        ], $filter);
    }
}
```

`page=user` resolves to `App\Tools\Export\UserExport`.

**Direct download in a controller:**

```
use HasanHawary\ExportBuilder\ExportBuilder;

public function export(Request $request)
{
    return (new ExportBuilder($request->validated()))->response();
}
```

**Via package routes:**

```
GET  api/export-direct?page=user&format=xlsx
GET  api/export-direct?page=user&format=pdf&start=2026-01-01&end=2026-06-30
POST api/export          { "page": "user", "format": "xlsx" }   ← queued
GET  api/export-log                                              ← history

```

---

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

[](#configuration)

Publish and edit `config/export.php`:

```
return [
    // Namespace where export classes live
    'namespace'  => 'App\\Tools\\Export',

    // Translation file for column headings and bool values
    // 'export' → package built-in (en + ar) — recommended
    // 'my_file' → use your own lang/en/my_file.php
    'trans_file' => 'export',

    // Rows per chunk for lazyById() streaming (memory-safe)
    'chunk_size' => 500,

    'pdf' => [
        'settings'          => [],      // static: logo_url, company_name, etc.
        'settings_resolver' => null,    // callable / invokable / [Class, 'method']
    ],

    'module' => [
        'enabled' => true,

        'routes' => [
            'enabled'      => true,
            'middleware'   => ['api'],
            'prefix'       => 'api',
            'export_path'  => 'export',
            'direct_path'  => 'export-direct',
            'log_path'     => 'export-log',
            'name_prefix'  => 'export-builder.export.',
        ],

        'controllers' => [
            'direct' => \HasanHawary\ExportBuilder\Http\Controllers\ExportController::class,
            'jobs'   => \HasanHawary\ExportBuilder\Http\Controllers\ExportJobController::class,
        ],

        'services' => [
            'export'      => \HasanHawary\ExportBuilder\Services\ExportService::class,
            'export_file' => \HasanHawary\ExportBuilder\Services\ExportFileService::class,
            'permissions' => \HasanHawary\ExportBuilder\Services\ExportPermissionResolver::class,
        ],

        'storage' => [
            'disk' => 'local',
            'path' => 'exports',
        ],

        'permissions' => [
            'enabled'   => false,
            'abilities' => [
                'export'   => 'export',
                'queue'    => 'create-export-file',
                'view_all' => 'view-all-export-file',
                'view_own' => 'view-own-export-file',
                'delete'   => 'delete-export-file',
            ],
            'pages' => [
                // Per-page ability overrides:
                // 'user' => ['export' => 'export-user', 'queue' => 'queue-user'],
            ],
        ],
    ],
];
```

---

Export Formats
--------------

[](#export-formats)

FormatParameterNotesXLSX`format=xlsx`DefaultXLS`format=xls`CSV`format=csv`PDF`format=pdf`Uses Blade view---

Column Types
------------

[](#column-types)

TypeOutput`text`Raw string value`int`Cast to integer`float`Cast to float (non-numeric returned as-is)`money``number_format($v, 2, '.', '')``date``YYYY-MM-DD``datetime``YYYY-MM-DD HH:MM:SS``bool` / `boolean`Translated Yes / No`array``implode(' , ', array_filter($v))``classPath`Class basename, translated if key exists`MyEnum::class`Calls `MyEnum::resolve($value)`---

Relation Exports
----------------

[](#relation-exports)

```
parent::__construct([
    'model'   => User::class,
    'columns' => ['id' => 'int', 'name' => 'text'],

    'relations' => [
        // One-to-one / BelongsTo
        'one' => [
            'role_id' => ['role' => ['name' => 'text']],
        ],

        'many' => [
            // Concat all related values into one cell
            'concat' => [
                'tags' => ['label' => 'text'],
            ],

            // Multi-line block per related item
            'list' => [
                'addresses' => ['city' => 'text', 'country' => 'text'],
            ],

            // Count with optional alias
            'count' => [
                'orders as orders_total',
            ],
        ],

        // Nested — automatically resolved as dot-notation with()
        // e.g. 'department_id' => ['department' => ['company' => ['name' => 'text']]]
    ],
], $filter);
```

**Custom eager loads and selects:**

```
'customWith'   => ['settings', 'profile'],       // extra with() paths
'customSelect' => ['id', 'name', 'email'],        // restrict SELECT columns
'additionalQuery' => [
    'posts_count' => fn ($q) => $q->withCount('posts'),
],
```

---

Custom Relations (Easy Mode)
----------------------------

[](#custom-relations-easy-mode)

`customRelations()` is the simplest way to export related data without touching the `relations` config array. Override it in your export class and define columns with closures or attribute names — no schema-level config needed.

```
class UserExport extends BaseExport
{
    public function __construct(array $filter)
    {
        parent::__construct([
            'model'   => User::class,
            'columns' => ['id' => 'int', 'name' => 'text', 'email' => 'text'],
            // No 'relations' key needed — customRelations() handles it below
        ], $filter);
    }

    public function customRelations(): array
    {
        return [
            // Simple attribute access
            'role' => ['name'],

            // Callable — full control over the value
            'profile' => [
                'full_address' => fn ($profile) => "{$profile->city}, {$profile->country}",
                'avatar_url'   => fn ($profile) => $profile->avatar ?? 'N/A',
            ],

            // Collection relation — one column per item with an index prefix
            'permissions' => ['name'],
        ];
    }
}
```

**How it works:**

Config keyValue typeOutput column name`'role' => ['name']`Attribute string`role_name``'profile' => ['full_address' => fn]`Callable`profile_full_address`Collection relationAny attribute`permissions_0_name`, `permissions_1_name`, …**Rules:**

- The array key is the Eloquent relation name (e.g. `role` → `$model->role`)
- When the relation is a `Collection`, each item gets an index prefix (`relation_0_key`, `relation_1_key`)
- When the relation is a single model, the key is `relation_column`
- `strip_tags()` is applied to all values automatically
- Columns ending in `_id` are automatically removed from the heading row
- Works with the `related` column filter — clients can request `related[]=role_name`

**Compared to `relations` config:**

`relations` config`customRelations()`SetupDeclare in constructor arrayOverride one methodNested relationsYes (dot-notation)NoCallablesNoYesCollection indexingNoYesColumn filter supportYesYesBest forStandard BelongsTo / HasManyCustom display logic, computed values---

Morph Relation Exports
----------------------

[](#morph-relation-exports)

```
'relations' => [
    'morph' => [
        'sourceable_id' => [
            'relation' => 'sourceable',   // Eloquent morphTo method
            'column'   => 'name',         // Column to display
            'type'     => 'text',         // convertValue type (default: text)
            'fallback' => null,           // Value when relation is null
        ],
    ],
],
```

---

Advanced Filters
----------------

[](#advanced-filters)

Request body:

```
{
  "page": "user",
  "format": "xlsx",
  "advanced": [
    { "key": "status",  "value": ["active", "pending"] },
    { "key": "role_id", "value": 3 }
  ]
}
```

Only keys matching actual table columns or configured relation keys are accepted — all others are silently ignored to prevent SQL injection.

**Relation filter config:**

```
'filterRelations' => [
    'many' => [
        'role_id' => ['relation' => 'role', 'column' => 'id'],

        // With morph constraint:
        'source_id' => [
            'relation'    => 'sourceable',
            'morph'       => 'sourceable',
            'morph_types' => [Campaign::class, Sponsor::class],
            'column'      => 'id',
        ],
    ],
],
```

**Enum resolver:**

```
protected array $resolvers = [
    'status' => ['enum' => StatusEnum::class, 'method' => 'fromLabel'],
];
```

---

Column Selection Filter
-----------------------

[](#column-selection-filter)

Clients can request a subset of columns at runtime:

```
GET api/export-direct?page=user&format=xlsx&columns[]=id&columns[]=name&related[]=roles

```

Or via JSON:

```
{
  "page": "user",
  "format": "xlsx",
  "columns": ["id", "name", "email"],
  "related": ["roles", "orders_total"]
}
```

`columns` filters base columns. `related` filters relation columns (concat, list, count).

---

PDF Output
----------

[](#pdf-output)

The default Blade view is `export::pdf.export` (the view name, not a translation key).

Publish to customise:

```
php artisan vendor:publish --tag=export-builder-views
```

Override per export class:

```
public function pdfView(): string
{
    return 'exports.my-custom-template';
}

public function pdfData(): array
{
    return [
        'title'   => 'Users Report',
        'columns' => array_map(fn ($h) => ['label' => $h, 'width' => 'auto'], $this->headings()),
        'rows'    => $this->buildQuery()->get()->map(fn ($r) => array_values($this->map($r)))->toArray(),
    ];
}
```

**PDF settings resolver** — resolve dynamic settings (e.g. company logo from DB):

```
// config/export.php
'pdf' => [
    'settings_resolver' => [App\Services\BrandSettings::class, 'forExport'],
    // Also supports: invokable class string, Closure
],
```

The resolver must return an array. Keys available in the Blade view as `$settings['logo_url']`, `$settings['company_name']`, etc.

---

Queued Exports
--------------

[](#queued-exports)

```
POST api/export
{ "page": "user", "format": "xlsx" }

```

Response (202 Accepted):

```
{
  "data": {
    "id": 1,
    "exportable_type": "user",
    "format": "xlsx",
    "status": "pending",
    "file_url": null
  },
  "message": "Export started successfully."
}
```

Poll for completion:

```
GET api/export-log           ← list all (paginated, ?per_page=15&status=completed)
GET api/export-log/{id}      ← show one
GET api/export-log/{id}/download  ← stream the file
DELETE api/export-log/{id}   ← soft-delete record and remove stored file

```

Files are stored on the configured disk:

```
'storage' => ['disk' => 'local', 'path' => 'exports'],
```

---

Routes
------

[](#routes)

All package routes default to the `api` prefix. They never claim a URI already owned by the host app.

MethodURIRoute nameDescription`GET``api/export-direct``export-builder.export.direct`Direct file download`GET``api/export``export-builder.export.download`Direct file download (alias)`POST``api/export``export-builder.export.store`Create queued export`GET``api/export-log``export-builder.export.logs.index`List export history`GET``api/export-log/{id}``export-builder.export.logs.show`Show one export record`GET``api/export-log/{id}/download``export-builder.export.logs.download`Download exported file`DELETE``api/export-log/{id}``export-builder.export.logs.destroy`Delete record and file**Disable all package routes:**

```
'module' => ['enabled' => false],
```

**Move to a different prefix:**

```
'routes' => [
    'prefix'      => 'internal/reports',
    'name_prefix' => 'reports.',
],
```

---

Permissions
-----------

[](#permissions)

Disabled by default. Enable and configure per-page abilities:

```
'permissions' => [
    'enabled' => true,
    'abilities' => [
        'export'   => 'export',
        'queue'    => 'create-export-file',
        'view_all' => 'view-all-export-file',
        'view_own' => 'view-own-export-file',
        'delete'   => 'delete-export-file',
    ],
    'pages' => [
        'user' => [
            'export' => 'export-user',
            'queue'  => 'create-user-export',
        ],
    ],
],
```

**Custom resolver** — replace the entire permission logic:

```
'services' => [
    'permissions' => App\Services\MyExportPermissionResolver::class,
],
```

Your class must extend `ExportPermissionResolver` or implement the same public API (`canExport`, `canCreateQueued`, `canList`, `canView`, `canDelete`, `scopeForUser`).

---

Translations
------------

[](#translations)

The package ships with English and Arabic translations for column headings and boolean values.

**Published to:** `lang/{en,ar}/export.php`

KeyEnglishArabic`id`IDالمعرف`name`Nameالاسم`email`Emailالبريد الإلكتروني`is_active`Activeنشط`created_at`Created Atتاريخ الإنشاء`yes`Yesنعم`no`Noلا**Use your own translation file** — point to any file in your project's `lang/` directory:

```
// config/export.php
'trans_file' => 'my_exports',  // looks up lang/en/my_exports.php keys
```

**Add custom column translations** — add keys to your published `lang/vendor/export/en/export.php`:

```
return [
    // ...existing keys...
    'order_number' => 'Order #',
    'total_amount' => 'Total',
];
```

---

Overriding Controllers and Services
-----------------------------------

[](#overriding-controllers-and-services)

Every package class can be replaced from config:

```
'module' => [
    'controllers' => [
        'direct' => App\Http\Controllers\CustomExportController::class,
        'jobs'   => App\Http\Controllers\CustomExportJobController::class,
    ],
    'services' => [
        'export'      => App\Services\CustomExportService::class,
        'export_file' => App\Services\CustomExportFileService::class,
        'permissions' => App\Services\CustomExportPermissionResolver::class,
    ],
],
```

---

Performance
-----------

[](#performance)

FeatureDetail**Lazy streaming**`lazyById()` cursor — peak memory stays flat at any dataset size**Configurable chunk size**`export.chunk_size` (default 500)**FK detection cache**Computed once per export instance**Schema column cache**Static cache per table, one `SHOW COLUMNS` per request**Query cache**`buildQuery()` result cached per instance**PDF settings cache**Resolver called once per `ExportBuilder` instance---

Testing
-------

[](#testing)

```
composer test
```

The test suite covers:

- Excel generation with spreadsheet readback (all column types)
- PDF generation with Blade view
- Relation exports: belongs-to, has-many concat, count alias, nested, morph
- Column filter — heading/map consistency, PDF filter safety
- Route registration, host conflict protection, disable/enable
- Permission deny/allow for direct, queued, list, download, delete
- Custom controller and service overrides
- `ExportFileService` full lifecycle and delete edge cases
- `AdvancedFilter` security allowlist, relation filters, enum resolver, error recovery
- `HelperTrait::convertValue` — all 10+ type branches and edge cases
- `ExportBuilder::buildFileName` — naming consistency between direct and queued paths
- Storage config SSOT via `storageDisk()` / `storagePath()`
- Architecture regression guards (SSOT fixes)
- Edge cases: morph, nested relations, `customWith`, `customSelect`, PDF resolver variants

---

License
-------

[](#license)

MIT © [Hasan Hawary](https://github.com/hasanhawary)

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance97

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 66.7% 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 ~15 days

Recently: every ~9 days

Total

19

Last Release

4d ago

Major Versions

v1.4.0 → v2.02026-04-20

PHP version history (3 changes)v1.0.0PHP &gt;=8.0

v2.2.0PHP ^8.2

2.4.0PHP &gt;=8.1 &lt;8.6

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/33836947?v=4)[Hassan Elhawary](/maintainers/hasanhawary)[@hasanhawary](https://github.com/hasanhawary)

---

Top Contributors

[![hassanmuhammd](https://avatars.githubusercontent.com/u/126090873?v=4)](https://github.com/hassanmuhammd "hassanmuhammd (2 commits)")[![hasanhawary](https://avatars.githubusercontent.com/u/33836947?v=4)](https://github.com/hasanhawary "hasanhawary (1 commits)")

---

Tags

laravelpdfexportexcelxlsxlsxcsv

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/hasanhawary-export-builder/health.svg)

```
[![Health](https://phpackages.com/badges/hasanhawary-export-builder/health.svg)](https://phpackages.com/packages/hasanhawary-export-builder)
```

###  Alternatives

[rap2hpoutre/fast-excel

Fast Excel import/export for Laravel

2.3k27.0M52](/packages/rap2hpoutre-fast-excel)[psalm/plugin-laravel

Psalm plugin for Laravel

3355.3M346](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58171.6k14](/packages/api-platform-laravel)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1235.9k20](/packages/fleetbase-core-api)

PHPackages © 2026

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