چرا این پکیج؟
بیشتر پکیجهای اکسل، ایمپورت را یک عملیات واحد میبینند: فایل را بخوان، سطرها را تحویل بده، تمام. این کار تا زمانی جواب میدهد که یک سطر وسط یک فایل ۵۰٬۰۰۰ سطری رد نشود، یا یک chunk job نمیرد و ندانید کدام سطرها commit شدهاند، یا یک بازبین، audit trail هر سطر ردشده را از شما نخواهد.
Purser ایمپورت را یک pipeline چندمرحلهای میبیند. هر مرحله یک job صفمحور مستقل و قابل retry است. هر خطای اعتبارسنجی در برابر سطری که باعث آن شده ثبت میشود. pipeline با یا بدون سطرهای معتبر به پایان میرسد.
پیشنیازها
- PHP نسخهٔ 8.1 یا بالاتر
- Laravel نسخههای 10، 11 یا 12
- یکی از دو بستهٔ خواننده:
maatwebsite/excel— driver پیشفرض، مبتنی بر PhpSpreadsheetopenspout/openspout— streaming driver، مصرف حافظهٔ ثابت
- یک queue connection دیتابیسی برای production
نصب
composer require akbarjimi/purser
php artisan vendor:publish --tag=purser
php artisan vendor:publish --tag=purser-sheets
php artisan migrate
دستور publish اول، فایل config/purser.php را نصب میکند.
دستور دوم، config/purser-sheets.php را نصب میکند؛ جایی که
ستونهای spreadsheet را به فیلدهای دامنهٔ خود map میکنید و قواعد
اعتبارسنجی هر sheet را تعریف میکنید.
پنج جدول ساخته میشود: excel_files، excel_sheets،
excel_rows، excel_row_chunks و excel_row_errors.
شروع سریع
۱. یک handler بنویسید
handler یک stream از سطرهای معتبر دریافت میکند و هر کاری که اپلیکیشن شما نیاز دارد انجام میدهد.
<?php
declare(strict_types=1);
namespace App\Imports;
use Akbarjimi\Purser\Contracts\ImportHandler;
use Akbarjimi\Purser\DTOs\ValidatedRow;
use App\Models\User;
final class UserImportHandler implements ImportHandler
{
public function handle(int $fileId, iterable $rows): void
{
foreach ($rows as $row) {
assert($row instanceof ValidatedRow);
User::updateOrCreate(
['email' => $row->data['email']],
[
'name' => $row->data['name'],
'age' => $row->data['age'],
],
);
}
}
}
۲. sheet را در config/purser-sheets.php پیکربندی کنید
return [
'Users' => [
'mapping' => [
'name' => 'A',
'email' => 'B',
'age' => 'C',
],
'validation' => [
'name' => 'required|string|max:255',
'email' => 'required|email',
'age' => 'required|integer|min:18',
],
],
];
۳. ایمپورت را dispatch کنید
use Akbarjimi\Purser\Services\ImportManager;
use App\Imports\UserImportHandler;
app(ImportManager::class)
->import('uploads/users.xlsx', disk: 's3')
->withHandler(UserImportHandler::class)
->dispatch();
pipeline بهصورت asynchronous اجرا میشود. دستور excel:status {fileId}
پیشرفت را نشان میدهد، excel:retry {fileId} chunkهای failed را دوباره
dispatch میکند، و سطرهای ردشده از طریق ErrorReportService در دسترساند.
نکته: handler فقط پس از پردازش همهٔ chunkها فراخوانی
میشود. سطرهایی که اعتبارسنجی را رد نکردهاند، در excel_row_errors
ذخیره میشوند و هرگز به handler نمیرسند.
معماری pipeline
هر ایمپورت از شش مرحله عبور میکند. هر مرحله یک job صفمحور یا event listener است. هر مرحله خروجیاش را پیش از signal دادن به مرحلهٔ بعد persist میکند. هیچ چیز بین مراحل در حافظه نگه داشته نمیشود.
ساخت رکورد ExcelFile و fire رویداد ExcelFileRegistered.
کشف sheetها با SheetDiscoveryService و persist آنها در excel_sheets.
استخراج سطرها با batchای از ExtractSheetRowsJob، یکی برای هر sheet.
ساخت ExcelRowChunk با اندازهٔ chunk_size برای هر sheet.
اجرای ProcessChunkJobها با allowFailures(true)؛ هر chunk مستقل پردازش میشود.
handler شما با یک LazyCollection از ValidatedRowها فراخوانی میشود.
هر مرحله میتواند شکست بخورد و بدون اجرای دوبارهٔ مراحل قبلی retry شود. هر انتقال وضعیت توسط یک enum state machine اعتبارسنجی میشود. هیچ حالت مشترک قابلتغییری بین مراحل وجود ندارد.
رویدادها و state machine
رویدادهای عمومی
| رویداد | معنا |
|---|---|
ExcelFileRegistered | رکورد فایل ساخته شد، pipeline شروع شد. |
FileSheetsScanCompleted | sheetها کشف و persist شدند. |
AllRowsExtracted | همهٔ سطرهای هر sheet در excel_rows هستند. |
FileProcessingCompleted | همهٔ chunkها پردازش شدند و handler فراخوانی شد. |
هر چهار رویداد پس از commit دیتابیس dispatch میشوند.
State machine فایل
PENDING -> READING | FAILED
READING -> ROWS_EXTRACTING | ROWS_EXTRACTED | FAILED
ROWS_EXTRACTED -> PROCESSING | FAILED
PROCESSING -> COMPLETED | FAILED
FAILED -> PROCESSING (retry)
COMPLETED -> (terminal)
هر موجودیت (file، sheet، row، chunk) enum مخصوص خود را با
canTransitionTo() دارد. انتقالهای غیرمجاز از طریق
HasStatusTransitions یک RuntimeException پرتاب
میکنند. انتقالهای idempotent (از یک state به خودش) no-op هستند.
Idempotency در هر مرحله
HandleExcelFileRegisteredاگر sheetها موجود باشند، discovery را skip میکند.SheetRowBufferبا کلید(excel_sheet_id, content_hash, hash_algo)upsert میکند.ProcessChunkJobاگر chunk قبلاًCOMPLETEDباشد بلافاصله return میکند.ChunkerServiceدر یک transaction با سه retry و کلید یکتای(excel_sheet_id, from_row_id, to_row_id)اجرا میشود.
Reader drivers
موتور خواندن پشت قرارداد ExcelReaderDriver قرار دارد. دو پیادهسازی
همراه پکیج ارائه میشود که معاملههای مهندسی متفاوتی دارند.
PhpSpreadsheetDriver
پیشفرضاز maatwebsite/excel استفاده میکند. کل فایل را در حافظه
بارگذاری میکند. برای فایلهای تا چند ده هزار سطر مناسب است.
- حافظه
- ~۱KB per cell
- فرمتها
- xls، xlsx، ods، csv
- totalRows
- دقیق
OpenSpoutDriver
Streamingاز openspout/openspout استفاده میکند. خوانندهٔ streaming
با حافظهٔ ثابت بدون توجه به اندازهٔ فایل.
- حافظه
- ثابت
- فرمتها
- xlsx، csv
- totalRows
- ۰ (مستند شده)
انتخاب driver
# از طریق .env
EXCEL_IMPORTER_DRIVER=openspout
# یا در config/purser.php
'driver' => 'openspout',
اگر بستهٔ متناظر نصب نباشد، driver در نخستین استفاده
MissingDriverDependencyException پرتاب میکند و دستور
composer لازم را نام میبرد.
واگرایی totalRows
PhpSpreadsheetDriver اعداد دقیق سطر و ستون برمیگرداند؛
OpenSpoutDriver همیشه 0 برمیگرداند چون
بدون مصرف کل sheet نمیتواند پیششمارش کند. اگر قبل از extraction
شمارش دقیق نیاز دارید، از driver نخست استفاده کنید.
اعتبارسنجی و transformation
دو مرحلهٔ اختیاری بین reader و handler اجرا میشوند. ترتیب ثابت است:
raw row (column letters)
-> mapping
-> transformer
-> validation
-> ValidatedRow
Mapping
mapping یک آرایهٔ config است، نه یک کلاس. کلیدها نام فیلدهایی هستند که handler شما خواهند دید، و مقادیر حروف ستونها هستند.
Validation
قواعد استاندارد Laravel که مستقیم به Validator::make()
پاس داده میشوند. اعتبارسنجی روی خروجی transformer اجرا میشود، نه روی
سطر خام.
اگر sheet هیچ قاعدهای نداشته باشد و strict_validation روی
false باشد، همهٔ سطرها قبول میشوند. در production مقدار را
true بگذارید تا misconfiguration را زودتر بگیرید.
Transformer
یک کلاس که TransformerInterface را پیادهسازی میکند. از
طریق container resolve میشود، پس constructor injection کار میکند.
<?php
namespace App\Transformers;
use Akbarjimi\Purser\Contracts\TransformerInterface;
use Akbarjimi\Purser\Models\ExcelSheet;
final class UserTransformer implements TransformerInterface
{
public function transform(array $mappedRow, ExcelSheet $sheet): array
{
return [
'name' => trim((string) $mappedRow['name']),
'email' => strtolower(trim((string) $mappedRow['email'])),
'age' => (int) $mappedRow['age'],
];
}
}
اگر transformer exception پرتاب کند، سطر با وضعیت
FAILED و error_type: system ثبت میشود و
chunk به کار خود ادامه میدهد.
بازیابی خطا
pipeline طوری طراحی شده که یک سطر خراب نتواند ایمپورت را متوقف کند. این ارزش اصلی پکیج است.
دو کلاس خطا
| نوع | دامنه | رفتار |
|---|---|---|
| Validation failure | per row، مورد انتظار | سطر FAILED_VALIDATION میشود؛ یک ExcelRowError
برای هر نقض قاعده نوشته میشود؛ chunk ادامه مییابد. |
| System failure | per chunk، غیرمنتظره | chunk با پیام exception در ستون error علامتگذاری
میشود؛ job دوباره throw میکند تا Laravel retry کند. |
خواندن خطاها
use Akbarjimi\Purser\Services\ErrorReportService;
$service = app(ErrorReportService::class);
$page = $service->paginate($fileId, perPage: 50);
$json = $service->toJson($fileId);
$path = $service->toSpreadsheet($fileId, disk: 'local');
workflow بازیابی
دستور excel:retry فقط روی فایلهایی با وضعیت FAILED
کار میکند. سه پیششرط دارد:
- فایل موجود و soft-delete نشده باشد.
- وضعیت فایل
FAILEDباشد. - حداقل یک chunk وضعیت
FAILEDداشته باشد.
در این صورت، فایل به PROCESSING میرود، chunkهای failed به
PENDING بازنشانی میشوند، و یک Bus::batch جدید
dispatch میشود. chunkهای completed دستنخورده باقی میمانند.
مهم: اگر شکست ناشی از دادهای باشد که تغییر نخواهد
کرد، retry دوباره شکست میخورد. ستون error روی
excel_row_chunks علت را نام میبرد.
پیکربندی
دو فایل به config/ منتشر میشوند:
purser.php برای تنظیمات سراسری pipeline و
purser-sheets.php برای mapping و validation هر sheet.
کلیدهای اصلی در purser.php
| کلید | پیشفرض | کاربرد |
|---|---|---|
driver | maatwebsite | maatwebsite یا openspout |
chunk_size | 1000 | تعداد سطر در هر chunk |
insert_batch_size | 100 | تعداد سطر در هر batch دیتابیسی |
hash_algo | sha256 | الگوریتم hash برای dedup سطرها |
max_sheets | 50 | رد فایلهایی که از این تعداد sheet بیشتر دارند |
strict_validation | false | throw در صورت نبود قاعده برای یک sheet |
default_disk | local | disk پیشفرض برای فایلهای آپلودشده |
queue | default | نام queue connection برای jobها |
متغیرهای محیطی
EXCEL_IMPORTER_DRIVER=openspout
EXCEL_IMPORTER_CHUNK_SIZE=500
EXCEL_IMPORTER_INSERT_BATCH_SIZE=200
EXCEL_IMPORTER_DISK=s3
EXCEL_IMPORTER_QUEUE=imports
EXCEL_IMPORTER_HASH_ALGO=sha256
EXCEL_IMPORTER_STRICT_VALIDATION=true
EXCEL_IMPORTER_LOG_ENABLED=true
دستورات کنسول
سه دستور Artisan همراه پکیج ارائه میشود.
excel:status {fileId}
وضعیت کامل یک ایمپورت را چاپ میکند: متادیتای فایل، وضعیت هر sheet، شمارش chunkها بر اساس وضعیت، شمارش سطرها بر اساس وضعیت، و تعداد کل خطاها.
File #42
+----------------+---------------------+
| Field | Value |
+----------------+---------------------+
| Name | users.xlsx |
| Status | completed |
| Rows Extracted | 2026-09-21 10:14:05 |
| Completed | 2026-09-21 10:14:19 |
+----------------+---------------------+
Errors: 152
excel:retry {fileId}
chunkهای failed را دوباره dispatch میکند. پیششرطها در بخش بازیابی خطا توضیح داده شدهاند.
excel:benchmark
یک fixture مصنوعی .xlsx میسازد، آن را از کل pipeline عبور
میدهد، و زمان هر فاز و مصرف حافظه را چاپ میکند. برای مقایسهٔ driverها
یا تنظیم chunk_size استفاده میشود.
php artisan excel:benchmark --rows=50000 --driver=openspout
تست و مشارکت
اجرای تستها
composer test
# یا مستقیم:
vendor/bin/pest
# با coverage:
vendor/bin/pest --coverage --min=80 --ci
Code style
Laravel Pint با preset laravel. پیش از هر commit اجرا کنید:
vendor/bin/pint
قالب commit
<type>(<scope>): <imperative, lowercase, ≤ 72 chars>
# نمونهها:
fix(processor): persist row errors outside chunk transaction
feat(drivers): add csv reader driver
docs: clarify error report structure
امنیت
آسیبپذیریها را بهجای issue عمومی، به
security@akbarjimi.com گزارش دهید. جزئیات کامل در
SECURITY.md
آمده است.
لایسنس
این پروژه تحت لایسنس MIT منتشر شده است. Copyright © 2026 Mohammad Akbari.
برای مطالعهٔ متن کامل لایسنس به فایل LICENSE.md در مخزن مراجعه کنید.