Queued Export
Export dengan jumlah record di atas Exporter::queueAfter() diberikan ke PandaPanel\Jobs\RunPanelExport alih-alih ditulis di dalam request. Request langsung selesai, kemudian file yang sudah jadi dikirim melalui notification yang membawa download link.
Gunakan halaman ini ketika menentukan threshold queue, ketika queued export tidak pernah tiba, atau ketika ingin menghasilkan export dari console command daripada dari tombol.
Contoh minimal yang berfungsi
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Resources\Orders\Exports;
use PandaPanel\Actions\Exports\ExportColumn;
use PandaPanel\Actions\Exports\Exporter;
final class OrderExporter extends Exporter
{
/**
* @return list<ExportColumn>
*/
public static function columns(): array
{
return [
ExportColumn::make('reference'),
ExportColumn::make('total'),
];
}
/**
* Anything over a thousand orders goes to a worker.
*/
public static function queueAfter(): int
{
return 1000;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
php artisan queue:workTidak ada bagian lain yang berubah. Action, dialog, dan ExportRun tetap sama — hanya proses yang menjalankannya yang berbeda.
Threshold
ExportAction menghitung record sebelum file mulai ditulis: count() pada constrained query untuk table export, atau count($keys) untuk bulk export.
if ($exporter::queueAfter() >= 0 && $count > $exporter::queueAfter()) {
RunPanelExport::dispatch(/* … */);
return;
}2
3
4
5
queueAfter() | Behavior |
|---|---|
0 | selalu queued |
2000 | default — queued jika lebih dari 2000 record |
| angka negatif apa pun | tidak pernah queued, berapa pun jumlah record |
Threshold berupa angka, bukan flag, karena kedua ekstrem sama-sama buruk: export kecil di background job memberikan pengalaman lebih lambat daripada waktu tunggu yang dihemat, sedangkan export besar dalam request berisiko timeout. Posisi threshold bergantung pada kolom dan database Anda; export enam scalar column jauh lebih murah per record dibanding export yang berjalan melalui dua relation.
Job
use PandaPanel\Actions\Enums\SpreadsheetFormat;
use PandaPanel\Jobs\RunPanelExport;
new RunPanelExport(
string $exporter, // class-string<Exporter>
string $resource, // class-string<PandaPanel\Resources\Resource>
array $columns, // list<string>, the chosen column names
SpreadsheetFormat $format,
int|string $owner, // the key of the user the file belongs to
array $tableState, // the query string the list was showing
?array $keys, // an explicit selection, or null for the whole list
string $panelId,
);2
3
4
5
6
7
8
9
10
11
12
13
Seluruh payload job berupa scalar atau plain array. Ini bukan sekadar gaya penulisan: Eloquent builder menyimpan closure dan tidak dapat diserialize, sehingga job yang menerima "query" akan gagal ketika masuk queue. Job membawa deskripsi query lalu membangunnya kembali di worker.
handle() melakukan empat hal:
- me-resolve panel berdasarkan id dan menjadikannya current panel — resource scope, table, dan URL dibaca melalui current panel; tanpa ini job berjalan di luar konteks panel;
- membangun ulang query:
whereKey($keys)untuk explicit selection, atau resource query yang dibatasiPandaPanel\Tables\TableQuerymenggunakanRequest::create('/', 'GET', $tableState); - menulis file dengan
ExportRun::write()— writer yang sama dengan jalur inline; - mencari owner menggunakan
Auth::getProvider()->retrieveById()lalu mengirim notification.
Membangun ulang query melalui TableQuery yang sama dengan list menjaga isi file tetap jujur: row yang diexport adalah row yang sesuai dengan screen, termasuk filter dan search, bukan seluruh data yang secara umum dapat dilihat resource.
Retry
public int $tries = 3;
public function backoff(): array
{
return [10, 60];
}2
3
4
5
6
Export hanya membaca row dan menulis file, sehingga kegagalan di tengah proses belum mengubah sesuatu yang terlihat pengguna — file setengah jadi akan digantikan oleh attempt berikutnya. Karena itu kegagalan seperti koneksi database terputus, disk sementara tidak tersedia, atau worker restart di tengah proses layak dicoba kembali. Backoff digunakan agar tiga retry tidak langsung menghantam outage yang sama dalam hitungan detik.
Import memiliki karakteristik sebaliknya dan dikonfigurasi dengan cara sebaliknya — lihat Queued import.
Yang diterima pengguna
Saat berhasil, notification persistent sekaligus broadcast dikirim:
Notification::make('export-ready')
->title($exporter::completedMessage($result['records']))
->success()
->icon('download')
->persistent()
->actions([
NotificationAction::make('download')->label('Download')->url(/* panel.{id}.export-file */),
])
->send($user);2
3
4
5
6
7
8
9
Persistent karena file adalah hasil utama. Jika hanya toast yang muncul ketika pengguna sedang berada di tab lain, export selesai tanpa ada cara yang mudah untuk menemukannya. Berbeda dengan jalur inline, notification ini dibroadcast karena tidak ada response HTTP yang dapat membawanya.
Setelah attempt terakhir gagal:
Notification::make('export-failed')
->title('Export failed')
->body($exception?->getMessage() ?? 'The file could not be written.')
->danger()
->icon('triangle-alert')
->persistent()
->send($user);2
3
4
5
6
7
Export yang gagal diam-diam lebih buruk daripada export yang gagal secara jelas: pengguna sudah meminta file, diberi tahu bahwa file sedang disiapkan, lalu menunggu notification yang tidak pernah datang.
Jika owner sudah tidak ada — misalnya account dihapus antara request dan waktu worker menjalankan job — kedua jalur berhenti tanpa mengirim apa pun. Ini merupakan race condition normal, bukan error kedua yang perlu dilempar dari failure handler.
Lihat Notification import dan export.
Mendispatch job sendiri
Constructor bersifat public, sehingga console command atau scheduled task dapat mengantrikan export tanpa screen panel:
use App\Panels\Admin\Resources\Orders\Exports\OrderExporter;
use App\Panels\Admin\Resources\Orders\OrderResource;
use PandaPanel\Actions\Enums\SpreadsheetFormat;
use PandaPanel\Jobs\RunPanelExport;
RunPanelExport::dispatch(
OrderExporter::class,
OrderResource::class,
['reference', 'total'],
SpreadsheetFormat::Xlsx,
$user->getKey(),
['filters' => ['status' => 'shipped']], // as the list's query string would spell it
null, // or a list of keys for an explicit selection
'admin', // the panel id
)->onQueue('reports');2
3
4
5
6
7
8
9
10
11
12
13
14
15
RunPanelExport menggunakan trait Laravel Queueable dan tidak menetapkan connection atau queue sendiri, sehingga secara default mengikuti konfigurasi queue aplikasi. Dispatch manual juga merupakan cara menempatkan panel export di dedicated queue — action bawaan selalu mendispatch ke default queue.
Panel id harus dikenal registry; id yang tidak valid melempar PanelRegistrationException::unknownPanel() di dalam job.
Testing
use Illuminate\Support\Facades\Queue;
use PandaPanel\Jobs\RunPanelExport;
it('queues an export above the threshold', function (): void {
Queue::fake();
// … trigger the action …
Queue::assertPushed(RunPanelExport::class);
});2
3
4
5
6
7
8
9
10
Untuk mengassert notification, jalankan job lalu gunakan helper package:
fakePanelNotifications();
$job = new RunPanelExport(
UserExporter::class,
UserResource::class,
['name'],
SpreadsheetFormat::Csv,
$user->getKey(),
[],
null,
'admin',
);
$job->failed(new RuntimeException('the disk went away'));
assertPanelNotificationSentTo($user, 'Export failed');2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gotchas
- Queued export membutuhkan worker. Dengan
QUEUE_CONNECTION=sync, job berjalan inline sehingga threshold kehilangan fungsinya tetapi file tetap dihasilkan. Dengan queue connection nyata tanpa worker aktif, file tidak pernah tiba dan request awal tidak error. - Success flash default tetap mengatakan export sudah siap. Action endpoint melakukan redirect
back()->with('success', $action->getSuccessMessage()), sedangkan queued path tidak membuat toast eksplisit. Akibatnya defaultYour export is ready.muncul ketika job baru saja didispatch. Override dengan->successMessage('Preparing your export. You will be notified when it is ready.')untuk exporter yang dapat queue. - Table state adalah snapshot konfigurasi query, bukan snapshot data. Job menjalankan ulang query ketika worker mengambilnya, sehingga record yang berubah setelah tombol ditekan diexport dalam keadaan terbaru saat job berjalan.
- Route URL dibangun di dalam job. Notification link menggunakan
route($panel->routeName('export-file'), …, absolute: false), sehingga worker denganAPP_URLyang salah masih menghasilkan relative path yang dapat digunakan. - Retry dapat menulis ulang nama file yang sama jika
Exporter::fileName()menggunakan timestamp hingga detik dan beberapa attempt terjadi pada detik yang sama. Ini aman karena content seharusnya identik; tetapifileName()tanpa timestamp berarti export sebelumnya memang akan digantikan. - Job mengikat panel, bukan tenant.
Tenancy::bind()dipanggil middlewareResolveTenant, sedangkan worker tidak menjalankan middleware tersebut. Karena ituResource::query()pada tenant-scoped resource akan mencapaiTenancy::require()tanpa tenant dan melemparPanelRegistrationException::noCurrentTenant(). Pada tenant-scoped panel, gunakanqueueAfter()negatif agar export berjalan dalam request, atau dispatch job sendiri yang membungkus proses menggunakanPandaPanel\Tenancy\Tenancy::for($tenant, …).
Lihat juga
- Class exporter —
queueAfter(),chunkSize(),completedMessage() - ExportAction
- Queued import
- Notification import dan export
- Storage dan cleanup
- Queue notification
- Queue di production
- Testing notification