Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# Path-based git attributes
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html

# Ignore all test and documentation with "export-ignore".
# Keep development files out of the Composer package. docs, llms.txt and
# resources/boost ship with it, for AI assistants and Laravel Boost.
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/.github export-ignore
/.editorconfig export-ignore
/phpunit.xml.dist export-ignore
/.scrutinizer.yml export-ignore
/pint.json export-ignore
/tests export-ignore
/.editorconfig export-ignore
/AGENTS.md export-ignore
/CLAUDE.md export-ignore
/node_modules export-ignore
/package.json export-ignore
/pnpm-lock.yaml export-ignore
/tsconfig.json export-ignore
/vitest.config.ts export-ignore
20 changes: 0 additions & 20 deletions .github/coverage.svg

This file was deleted.

81 changes: 0 additions & 81 deletions .github/workflows/run-tests.yml

This file was deleted.

138 changes: 138 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: tests

on:
pull_request:
push:
branches:
- main
- next-gen

# The unreleased javaabu/* dependencies come from VCS repositories; the job's own
# token keeps Composer within GitHub's API limits
env:
COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ github.token }}"}}'

jobs:
pint:
runs-on: ubuntu-latest

name: Pint

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
coverage: none

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Check code style
run: vendor/bin/pint --test

js:
runs-on: ubuntu-latest

name: React pages (typecheck, tests)

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 22
package-manager-cache: false

# The default store is inside a node_modules folder, which breaks the declaration
# build of @javaabu/forms (installed from git and built on install)
- name: Install dependencies
run: pnpm install --frozen-lockfile --store-dir "$RUNNER_TEMP/pnpm-store"

- name: Typecheck
run: pnpm typecheck

- name: Test
run: pnpm test

benchmark:
runs-on: ubuntu-latest

name: Benchmark (1M rows)

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.4
extensions: curl, mbstring, zip, pdo, sqlite, pdo_sqlite
coverage: none

- name: Install dependencies
run: composer update --prefer-dist --no-interaction --no-progress

- name: Export a million rows with flat memory
run: vendor/bin/phpunit --group benchmark

tests:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php: [ 8.4, 8.5 ]
laravel: [ 12.*, 13.* ]
stability: [ prefer-lowest, prefer-stable ]
include:
- laravel: 12.*
testbench: 10.*
- laravel: 13.*
testbench: 11.*

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}

env:
COVERAGE: ${{ matrix.php == '8.4' && matrix.laravel == '13.*' && matrix.stability == 'prefer-stable' }}

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv
coverage: ${{ env.COVERAGE == 'true' && 'pcov' || 'none' }}

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress

- name: Execute tests
if: env.COVERAGE != 'true'
run: vendor/bin/phpunit

- name: Execute tests with coverage
if: env.COVERAGE == 'true'
run: vendor/bin/phpunit --coverage-clover build/coverage.xml

- name: Upload coverage to Codecov
if: env.COVERAGE == 'true'
uses: codecov/codecov-action@v5
with:
files: build/coverage.xml
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ composer.lock
vendor
coverage
.phpunit.result.cache
.php-cs-fixer.cache
.phpunit.cache
.idea
clover.xml
node_modules
35 changes: 0 additions & 35 deletions .php_cs.dist.php

This file was deleted.

46 changes: 46 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# exports: notes for AI coding agents

`javaabu/exports` writes Eloquent models (or any rows) to CSV, XLSX and ODS with OpenSpout, streaming so memory stays flat. It adds queued exports tracked by an `Export` model, notifications, broadcasting, routes with an overridable controller and JSON API, Blade and Inertia React pages, and a policy with permissions from `javaabu/permissions`. Apps subclass `ModelExport`, so treat its public and protected methods, `Column`, `Exportable`, `WritesFiles`, `Exports`, `Route::exports()` and the JSON shape of `ExportResource` as public contracts.

## Layout

| Path | What |
| --- | --- |
| `src/ModelExport.php` | Base class: default columns, `columns()`, headings, formatting, chunked reading, queue serialization |
| `src/IterableExport.php`, `src/Contracts`, `src/Concerns/WritesFiles.php` | Non-model exports, the `Exportable` contract, `download()`/`store()`/`writeTo()`/`queue()` |
| `src/Columns/Column.php` | Column definitions |
| `src/Writers/SpreadsheetWriter.php` | OpenSpout writer: cells, formula escaping, widths, RTL |
| `src/Models/Export.php`, `src/Enums` | The Export model, `Formats`, `ExportStatuses` |
| `src/Jobs/ProcessExport.php`, `src/Events`, `src/Notifications`, `src/Commands` | The queued job, events (broadcastable), notifications, `exports:prune` |
| `src/Support` | `SerializableQuery`, `Broadcasting`, `TemporaryFile` |
| `src/Routing`, `src/Http`, `src/Policies`, `src/Views` | `Route::exports()`, controller, request, resource, policy, `ViewRenderer` |
| `config/exports.php`, `database/migrations` | Options; the `exports` table |
| `resources/views` | Blade pages (javaabu/forms components) |
| `resources/js` | Inertia React pages (TypeScript), shipped through javaabu/manifest |
| `lang/en.json`, `lang/dv.json` | Translations; English strings are the keys |
| `docs`, `llms.txt` | Docusaurus pages for docs.javaabu.com and the index for AI assistants |
| `resources/boost` | Laravel Boost guideline and skill, shipped in the package |
| `tests` | Testbench tests; two guards (`web` → `User`, `web_admin` → `Admin`); `tests/Benchmark` is the 1M-row benchmark |

## Rules

- Follow Javaabu codestyle ([boost-javaabu-guidelines](https://github.com/Javaabu/boost-javaabu-guidelines)): snake_case variables and properties, camelCase methods, early returns, `#[Test]` and `it_…` test names. Pint enforces formatting.
- Memory must stay flat: never collect rows into arrays, never `get()` a whole query, never buffer a file in memory. `composer benchmark` and `StreamingTest` guard this.
- Strings are written as text cells; never let user data become a formula.
- Nothing may assume the `web` guard or `App\Models\User`. Type-hint users as `Model`; exports belong to users through the `user` morph. Test on both guards.
- Reverb and Inertia are optional: feature-detect them, keep them in `suggest`, and test both ways.
- npm packages only the React pages or `@javaabu/exports` imports use go in the manifest `npm(..., for: NpmScopes::PAGES)` call. Only packages that a registered Blade script (`exports::progress`) imports go in the assets scope. `NpmRequirementsTest` checks that a Blade-only app isn't asked for React.
- v1 subclasses must keep working: don't rename or retype `ModelExport` hooks without an entry in `docs/upgrading.md`.
- Every user-facing string goes through `__()` with an English key, and gets a line in `lang/en.json` and `lang/dv.json` (`TranslationsTest` checks). Use Thaana in tests of user-facing behaviour. List uncertain translations in `docs/advanced-usage/translations.md`. Views use logical CSS only (`ms-`, `pe-`, `start-`), never `left`/`right`.
- Every change needs tests, docs (linked from `llms.txt`), the Boost skill if it changes how the package is used, and a line in `CHANGELOG.md`.
- Work on the `next-gen` branch; don't commit to `main`.

## Commands

```bash
composer install
composer lint # vendor/bin/pint --test
composer format # vendor/bin/pint
composer test # vendor/bin/phpunit
composer benchmark # 1M rows; EXPORTS_BENCHMARK_ROWS to change
```
21 changes: 18 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
# Changelog

All notable changes will be documented in this file
All notable changes will be documented in this file.

## 1.0.0 - 202X-XX-XX
## 2.0.0 - Unreleased

- initial release
- Requires PHP 8.4 and Laravel 12 or 13.
- Replaced maatwebsite/excel with OpenSpout: CSV, XLSX and ODS are streamed with flat memory. See `docs/upgrading.md`.
- Exports read with `lazyById()` (or `lazy()` for ordered queries), eager loading per chunk. A million rows export with a few MB of memory.
- Smarter defaults: hidden attributes, passwords and tokens are left out, foreign keys are replaced by their relation, headings come from validation attribute names, the model or a readable headline (translated), enums use their label, booleans are "Yes"/"No", dates are in the app timezone.
- `columns()` with dot-notation relations and `Column` definitions (label, format, computed value).
- `IterableExport` for rows that aren't models.
- CSV values that would run as formulas are escaped; XLSX strings are never written as formulas. XLSX columns are sized to fit, and sheets are right-to-left in Dhivehi.
- `queue()` writes exports on the queue and returns an `Export` record (status, progress, file, user morph). `ProcessExport` streams the file to a configurable disk, saves progress, stops when cancelled, and notifies the user (mail by default) with a signed, expiring download link.
- `exports:prune` deletes expired exports and their files.
- Export events: `ExportStarted`, `ExportProgressed`, `ExportCompleted`, `ExportFailed`, `ExportCancelled`.
- `Route::exports()` registers the exports routes (index, store, show, download, cancel, destroy, signed download) with the app's prefix, name, guard and middleware. `ExportsController` answers JSON or renders pages, and can be replaced from config, the macro or the container.
- `ExportPolicy` and the permissions `view_exports`, `view_others_exports`, `delete_exports`, `delete_others_exports`, registered with javaabu/permissions. Exports belong to any user model through a morph; nothing assumes the `web` guard.
- `Exports::register('users', UsersExport::class)` lets the API start an export by name.
- Live progress: export events broadcast on private channels (`exports.{uuid}`, `exports.owner.{type}.{id}`) when laravel/reverb is installed and broadcasting is on (`exports.broadcasting.enabled`: auto, true or false). The API tells pages whether to listen or to poll, and how often.
- Exports pages in Blade (javaabu/forms v2 components) and Inertia React (TypeScript, @javaabu/forms and @javaabu/onu, shipped through javaabu/manifest), chosen with `exports.views`, with live progress, in English and Dhivehi, right-to-left ready.
- The React pages' npm packages are declared in manifest's `pages` scope, so Blade-only apps' `manifest:check` doesn't ask for React.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
Loading
Loading