diff --git a/src/ChunkReader.php b/src/ChunkReader.php index 3e55cc92..94719beb 100644 --- a/src/ChunkReader.php +++ b/src/ChunkReader.php @@ -9,6 +9,8 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Jobs\SyncJob; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; +use Maatwebsite\Excel\Concerns\ShouldBatch; use Maatwebsite\Excel\Concerns\ShouldQueueWithoutChain; use Maatwebsite\Excel\Concerns\WithChunkReading; use Maatwebsite\Excel\Concerns\WithEvents; @@ -37,7 +39,7 @@ public function __construct(Container $container) * @param WithChunkReading $import * @param Reader $reader * @param TemporaryFile $temporaryFile - * @return PendingDispatch|Collection|null + * @return PendingDispatch|\Illuminate\Bus\PendingBatch|Collection|null */ public function read(WithChunkReading $import, Reader $reader, TemporaryFile $temporaryFile) { @@ -94,6 +96,14 @@ public function read(WithChunkReading $import, Reader $reader, TemporaryFile $te $jobs->push($afterImportJob); + // Check if the import class is batchable + if ($import instanceof ShouldBatch) { + return Bus::batch([ + $jobs->toArray(), + ]); + } + + // Check if the import class is queueable if ($import instanceof ShouldQueue) { return new PendingDispatch( (new QueueImport($import))->chain($jobs->toArray()) @@ -133,7 +143,8 @@ protected function dispatchNow($command, $handler = null) { $uses = class_uses_recursive($command); - if (in_array(InteractsWithQueue::class, $uses) && + if ( + in_array(InteractsWithQueue::class, $uses) && in_array(Queueable::class, $uses) && !$command->job ) { $command->setJob(new SyncJob($this->container, json_encode([]), 'sync', 'sync')); diff --git a/src/Concerns/BatchableTrait.php b/src/Concerns/BatchableTrait.php new file mode 100644 index 00000000..fe6fa5a3 --- /dev/null +++ b/src/Concerns/BatchableTrait.php @@ -0,0 +1,25 @@ +batch(); + + return $batch !== null && method_exists($batch, 'cancelled') && $batch->cancelled(); + } + } +} else { + trait BatchableTrait + { + public function batchCancelled() + { + return false; + } + } +} diff --git a/src/Concerns/ShouldBatch.php b/src/Concerns/ShouldBatch.php new file mode 100644 index 00000000..613ddbbc --- /dev/null +++ b/src/Concerns/ShouldBatch.php @@ -0,0 +1,7 @@ +reader->read($import, $filePath, $readerType, $disk); - if ($response instanceof PendingDispatch) { + if ($response instanceof PendingDispatch || $response instanceof \Illuminate\Bus\PendingBatch) { return $response; } diff --git a/src/Fakes/ExcelFake.php b/src/Fakes/ExcelFake.php index d8cb0aad..a4ebae31 100644 --- a/src/Fakes/ExcelFake.php +++ b/src/Fakes/ExcelFake.php @@ -6,8 +6,10 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Traits\Macroable; +use Maatwebsite\Excel\Concerns\ShouldBatch; use Maatwebsite\Excel\Exporter; use Maatwebsite\Excel\Importer; use Maatwebsite\Excel\Reader; @@ -102,6 +104,11 @@ public function handle() Queue::push($this->job); + // Check if the export class is batchable + if ($export instanceof ShouldBatch) { + return Bus::batch([$this->job]); + } + return new PendingDispatch($this->job); } @@ -122,7 +129,7 @@ public function raw($export, string $writerType) * @param string|UploadedFile $file * @param string|null $disk * @param string|null $readerType - * @return Reader|PendingDispatch + * @return Reader|PendingDispatch|\Illuminate\Bus\PendingBatch */ public function import($import, $file, string $disk = null, string $readerType = null) { @@ -174,7 +181,7 @@ public function toCollection($import, $file, string $disk = null, string $reader * @param string|UploadedFile $file * @param string|null $disk * @param string $readerType - * @return PendingDispatch + * @return PendingDispatch|\Illuminate\Bus\PendingBatch */ public function queueImport(ShouldQueue $import, $file, string $disk = null, string $readerType = null) { @@ -197,6 +204,11 @@ public function handle() Queue::push($this->job); + // Check if the import class is batchable + if ($import instanceof ShouldBatch) { + return Bus::batch([$this->job]); + } + return new PendingDispatch($this->job); } diff --git a/src/Jobs/AfterImportJob.php b/src/Jobs/AfterImportJob.php index 15be0e19..761e4558 100644 --- a/src/Jobs/AfterImportJob.php +++ b/src/Jobs/AfterImportJob.php @@ -4,8 +4,10 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Collection; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Events\ImportFailed; use Maatwebsite\Excel\HasEventBus; @@ -14,7 +16,7 @@ class AfterImportJob implements ShouldQueue { - use HasEventBus, InteractsWithQueue, Queueable; + use BatchableTrait, HasEventBus, InteractsWithQueue, Queueable, Dispatchable; /** * @var WithEvents @@ -57,6 +59,11 @@ public function setDependencies(Collection $jobs) public function handle() { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + foreach ($this->dependencyIds as $id) { if (!ReadChunk::isComplete($id)) { // Until there is no jobs left to run we put this job back into the queue every minute diff --git a/src/Jobs/AppendDataToSheet.php b/src/Jobs/AppendDataToSheet.php index c1c29431..d8d53a80 100644 --- a/src/Jobs/AppendDataToSheet.php +++ b/src/Jobs/AppendDataToSheet.php @@ -6,13 +6,14 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Files\TemporaryFile; use Maatwebsite\Excel\Jobs\Middleware\LocalizeJob; use Maatwebsite\Excel\Writer; class AppendDataToSheet implements ShouldQueue { - use Queueable, Dispatchable, ProxyFailures, InteractsWithQueue; + use BatchableTrait, Queueable, Dispatchable, ProxyFailures, InteractsWithQueue; /** * @var array @@ -73,6 +74,11 @@ public function middleware() */ public function handle(Writer $writer) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + (new LocalizeJob($this->sheetExport))->handle($this, function () use ($writer) { $writer = $writer->reopen($this->temporaryFile, $this->writerType); diff --git a/src/Jobs/AppendQueryToSheet.php b/src/Jobs/AppendQueryToSheet.php index 7ac4f0d0..60776516 100644 --- a/src/Jobs/AppendQueryToSheet.php +++ b/src/Jobs/AppendQueryToSheet.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Events\AfterChunk; @@ -16,7 +17,7 @@ class AppendQueryToSheet implements ShouldQueue { - use Queueable, Dispatchable, ProxyFailures, InteractsWithQueue, HasEventBus; + use BatchableTrait, Queueable, Dispatchable, ProxyFailures, InteractsWithQueue, HasEventBus; /** * @var TemporaryFile @@ -90,6 +91,11 @@ public function middleware() */ public function handle(Writer $writer) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + (new LocalizeJob($this->sheetExport))->handle($this, function () use ($writer) { if ($this->sheetExport instanceof WithEvents) { $this->registerListeners($this->sheetExport->registerEvents()); diff --git a/src/Jobs/AppendViewToSheet.php b/src/Jobs/AppendViewToSheet.php index a7d2c09e..4b4a6f4e 100644 --- a/src/Jobs/AppendViewToSheet.php +++ b/src/Jobs/AppendViewToSheet.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\FromView; use Maatwebsite\Excel\Files\TemporaryFile; use Maatwebsite\Excel\Jobs\Middleware\LocalizeJob; @@ -13,7 +14,7 @@ class AppendViewToSheet implements ShouldQueue { - use Queueable, Dispatchable, InteractsWithQueue; + use BatchableTrait, Queueable, Dispatchable, InteractsWithQueue; /** * @var TemporaryFile @@ -68,6 +69,11 @@ public function middleware() */ public function handle(Writer $writer) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + (new LocalizeJob($this->sheetExport))->handle($this, function () use ($writer) { $writer = $writer->reopen($this->temporaryFile, $this->writerType); diff --git a/src/Jobs/CloseSheet.php b/src/Jobs/CloseSheet.php index 7225505a..98f2eb70 100644 --- a/src/Jobs/CloseSheet.php +++ b/src/Jobs/CloseSheet.php @@ -4,13 +4,16 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\WithEvents; use Maatwebsite\Excel\Files\TemporaryFile; use Maatwebsite\Excel\Writer; class CloseSheet implements ShouldQueue { - use Queueable, ProxyFailures; + use BatchableTrait, Queueable, Dispatchable, ProxyFailures, InteractsWithQueue; /** * @var object @@ -54,6 +57,11 @@ public function __construct($sheetExport, TemporaryFile $temporaryFile, string $ */ public function handle(Writer $writer) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + $writer = $writer->reopen( $this->temporaryFile, $this->writerType diff --git a/src/Jobs/QueueExport.php b/src/Jobs/QueueExport.php index 768b7ae9..ea97729a 100644 --- a/src/Jobs/QueueExport.php +++ b/src/Jobs/QueueExport.php @@ -4,6 +4,8 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\WithMultipleSheets; use Maatwebsite\Excel\Exceptions\NoSheetsFoundException; use Maatwebsite\Excel\Files\TemporaryFile; @@ -13,7 +15,7 @@ class QueueExport implements ShouldQueue { - use ExtendedQueueable, Dispatchable; + use BatchableTrait, ExtendedQueueable, Dispatchable, InteractsWithQueue; /** * @var object @@ -59,6 +61,11 @@ public function middleware() */ public function handle(Writer $writer) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + (new LocalizeJob($this->export))->handle($this, function () use ($writer) { $writer->open($this->export); diff --git a/src/Jobs/ReadChunk.php b/src/Jobs/ReadChunk.php index ef73373d..4981d8f4 100644 --- a/src/Jobs/ReadChunk.php +++ b/src/Jobs/ReadChunk.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Cache; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Concerns\WithChunkReading; use Maatwebsite\Excel\Concerns\WithCustomValueBinder; use Maatwebsite\Excel\Concerns\WithEvents; @@ -24,7 +25,7 @@ class ReadChunk implements ShouldQueue { - use Queueable, HasEventBus, InteractsWithQueue; + use BatchableTrait, Queueable, HasEventBus, InteractsWithQueue; /** * @var int @@ -165,6 +166,11 @@ public function retryUntil() */ public function handle(TransactionHandler $transaction) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + if (method_exists($this->import, 'setChunkOffset')) { $this->import->setChunkOffset($this->startRow); } diff --git a/src/Jobs/StoreQueuedExport.php b/src/Jobs/StoreQueuedExport.php index 5469a927..b39e055f 100644 --- a/src/Jobs/StoreQueuedExport.php +++ b/src/Jobs/StoreQueuedExport.php @@ -4,12 +4,15 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Maatwebsite\Excel\Concerns\BatchableTrait; use Maatwebsite\Excel\Files\Filesystem; use Maatwebsite\Excel\Files\TemporaryFile; class StoreQueuedExport implements ShouldQueue { - use Queueable; + use BatchableTrait, Queueable, Dispatchable, InteractsWithQueue; /** * @var string @@ -25,6 +28,7 @@ class StoreQueuedExport implements ShouldQueue * @var TemporaryFile */ private $temporaryFile; + /** * @var array|string */ @@ -49,6 +53,11 @@ public function __construct(TemporaryFile $temporaryFile, string $filePath, stri */ public function handle(Filesystem $filesystem) { + // Determine if the batch has been cancelled... + if ($this->batchCancelled()) { + return; + } + $filesystem->disk($this->disk, $this->diskOptions)->copy( $this->temporaryFile, $this->filePath diff --git a/src/QueuedWriter.php b/src/QueuedWriter.php index a8c6a669..cedef5ea 100644 --- a/src/QueuedWriter.php +++ b/src/QueuedWriter.php @@ -4,10 +4,12 @@ use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\LazyCollection; use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Concerns\FromQuery; use Maatwebsite\Excel\Concerns\FromView; +use Maatwebsite\Excel\Concerns\ShouldBatch; use Maatwebsite\Excel\Concerns\WithCustomChunkSize; use Maatwebsite\Excel\Concerns\WithCustomQuerySize; use Maatwebsite\Excel\Concerns\WithMultipleSheets; @@ -56,7 +58,7 @@ public function __construct(Writer $writer, TemporaryFileFactory $temporaryFileF * @param string $disk * @param string|null $writerType * @param array|string $diskOptions - * @return \Illuminate\Foundation\Bus\PendingDispatch + * @return PendingDispatch|Illuminate\Bus\PendingBatch */ public function store($export, string $filePath, string $disk = null, string $writerType = null, $diskOptions = []) { @@ -65,6 +67,8 @@ public function store($export, string $filePath, string $disk = null, string $wr $jobs = $this->buildExportJobs($export, $temporaryFile, $writerType); + $queueExportJob = new QueueExport($export, $temporaryFile, $writerType); + $jobs->push(new StoreQueuedExport( $temporaryFile, $filePath, @@ -72,8 +76,16 @@ public function store($export, string $filePath, string $disk = null, string $wr $diskOptions )); + // Check if the export class is batchable + if ($export instanceof ShouldBatch) { + return Bus::batch([ + $jobs->prepend($queueExportJob) + ->toArray(), + ]); + } + return new PendingDispatch( - (new QueueExport($export, $temporaryFile, $writerType))->chain($jobs->toArray()) + $queueExportJob->chain($jobs->toArray()) ); } diff --git a/tests/Data/Stubs/ShouldBatchExport.php b/tests/Data/Stubs/ShouldBatchExport.php new file mode 100644 index 00000000..fe1ab66d --- /dev/null +++ b/tests/Data/Stubs/ShouldBatchExport.php @@ -0,0 +1,24 @@ + $row[0], + ]); + } + + /** + * @return int + */ + public function batchSize(): int + { + return 100; + } + + /** + * @return int + */ + public function chunkSize(): int + { + return 100; + } +} diff --git a/tests/QueuedExportTest.php b/tests/QueuedExportTest.php index ab2f5ec4..8d255462 100644 --- a/tests/QueuedExportTest.php +++ b/tests/QueuedExportTest.php @@ -2,6 +2,7 @@ namespace Maatwebsite\Excel\Tests; +use Illuminate\Bus\PendingBatch; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Queue; @@ -15,6 +16,7 @@ use Maatwebsite\Excel\Tests\Data\Stubs\QueuedExportWithFailedEvents; use Maatwebsite\Excel\Tests\Data\Stubs\QueuedExportWithFailedHook; use Maatwebsite\Excel\Tests\Data\Stubs\QueuedExportWithLocalePreferences; +use Maatwebsite\Excel\Tests\Data\Stubs\ShouldBatchExport; use Maatwebsite\Excel\Tests\Data\Stubs\ShouldQueueExport; use Throwable; @@ -29,6 +31,21 @@ public function test_can_queue_an_export() ]); } + public function test_can_batch_an_export() + { + if (!class_exists(\Illuminate\Bus\Batchable::class)) { + $this->markTestSkipped('Batch jobs are not supported in this version of Laravel.'); + } + + $export = new ShouldBatchExport(); + + $batch = $export->queue('batch-export.xlsx', 'test')->name('batch-export-name'); + + $this->assertInstanceOf(PendingBatch::class, $batch); + $this->assertEquals('batch-export-name', $batch->name); + $this->assertCount(1, $batch->jobs); + } + public function test_can_queue_an_export_and_store_on_different_disk() { $export = new QueuedExport(); diff --git a/tests/QueuedImportTest.php b/tests/QueuedImportTest.php index 39271af8..65c4a566 100644 --- a/tests/QueuedImportTest.php +++ b/tests/QueuedImportTest.php @@ -2,6 +2,7 @@ namespace Maatwebsite\Excel\Tests; +use Illuminate\Bus\PendingBatch; use Illuminate\Foundation\Bus\PendingDispatch; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobProcessed; @@ -19,6 +20,7 @@ use Maatwebsite\Excel\Tests\Data\Stubs\QueuedImportWithFailure; use Maatwebsite\Excel\Tests\Data\Stubs\QueuedImportWithMiddleware; use Maatwebsite\Excel\Tests\Data\Stubs\QueuedImportWithRetryUntil; +use Maatwebsite\Excel\Tests\Data\Stubs\ShouldBatchImport; use Throwable; class QueuedImportTest extends TestCase @@ -58,6 +60,21 @@ public function test_can_queue_an_import() $this->assertInstanceOf(PendingDispatch::class, $chain); } + public function test_can_batch_an_import() + { + if (!class_exists(\Illuminate\Bus\Batchable::class)) { + $this->markTestSkipped('Batch jobs are not supported in this version of Laravel.'); + } + + $import = new ShouldBatchImport(); + + $batch = $import->queue('import-batches.xlsx')->name('batch-import-name'); + + $this->assertInstanceOf(PendingBatch::class, $batch); + $this->assertEquals('batch-import-name', $batch->name); + $this->assertCount(1, $batch->jobs); + } + public function test_can_queue_an_import_with_batch_cache_and_file_store() { config()->set('queue.default', 'sync');