Back

Job Batching

Let me see the code 👀
routes/web.php
1<?php
2 
3use App\Livewire\JobBatching;
4 
5Route::get('/job-batching', JobBatching::class);
app/Livewire/JobBatching.php
1<?php
2 
3namespace App\Livewire;
4 
5use App\Jobs\ProcessInsertRecord;
6use Illuminate\Support\Facades\Bus;
7use Illuminate\Support\Facades\DB;
8use Illuminate\Support\LazyCollection;
9use Livewire\Attributes\Computed;
10use Livewire\Component;
11use Livewire\WithPagination;
12 
13class JobBatching extends Component
14{
15 use WithPagination;
16 
17 public $perPage = 10;
18 public $search = '';
19 public $batchId;
20 public $batchFinished, $batchCancelled = false;
21 public $batchProgress = 0;
22 public $startBatch = false;
23 private $table = 'websites';
24 
25 public function updated($property)
26 {
27 if ($property === 'search') {
28 $this->resetPage();
29 }
30 }
31 
32 #[Computed]
33 public function websites()
34 {
35 $website = DB::table($this->table);
36 
37 if (!empty($this->search)) {
38 $search = trim(strtolower($this->search));
39 $website = $website->where('Domain', 'like', '%'.$search.'%');
40 }
41 
42 $website = $website->orderBy('GlobalRank')->cursorPaginate($this->perPage);
43 
44 return $website;
45 }
46 
47 public function start()
48 {
49 $this->startBatch = true;
50 
51 DB::table($this->table)->truncate();
52 
53 $websites = LazyCollection::make(function () {
54 $handle = fopen(base_path('csvfile/majestic_million.csv'), 'r');
55 
56 $header = fgetcsv($handle);
57 while (($line = fgetcsv($handle)) !== false) {
58 yield array_combine($header, $line);
59 }
60 });
61 
62 $batch = Bus::batch([])->dispatch();
63 $websites->chunk(1000)->each(function ($chunk) use ($batch) {
64 $batch->add(new ProcessInsertRecord('websites', $chunk->toArray()));
65 });
66 
67 $this->batchId = $batch->id;
68 }
69 
70 #[Computed]
71 public function batch()
72 {
73 if (!$this->batchId) {
74 return null;
75 }
76 
77 return Bus::findBatch($this->batchId);
78 }
79 
80 public function updateBatchProgress()
81 {
82 $this->batchProgress = $this->batch->progress();
83 $this->batchFinished = $this->batch->finished();
84 $this->batchCancelled = $this->batch->cancelled();
85 
86 if ($this->batchFinished) {
87 $this->startBatch = false;
88 }
89 }
90 
91 public function render()
92 {
93 return view('livewire.job-batching')
94 ->title('Job Batching');
95 }
96}
resources/views/livewire/job-batching.blade.php
1<div x-data="{ batchFinished: $wire.entangle('batchFinished').live,
2 batchCancelled: $wire.entangle('batchCancelled').live,
3 batchProgress: $wire.entangle('batchProgress').live,
4 startBatch: $wire.entangle('startBatch').live,
5 }" x-effect="console.log('batch finished: ', batchFinished, 'batch cancelled: ', batchCancelled, 'batch progress: ', batchProgress, 'start batch: ', startBatch)"
6 x-init="$watch('batchFinished', (value) => {
7 if (value) {
8 startBatch = false;
9 }
10 });">
11 
12 <a href="/" class="underline text-blue-500">Back</a>
13 
14 <h1 class="text-2xl">Job Batching</h1>
15 
16 <p>This example shows how to implements Job Batching with Livewire and AlpineJS using <a class="underline text-blue-500" href="https://blog.majestic.com/development/majestic-million-csv-daily/">Majestic Million data.</a></p>
17 <p>The CSV file stored in <code class="bg-gray-100 p-1 inline-block rounded">csvfile</code> directory.</p>
18 
19 <div class="my-4">
20 <button type="button" @click="startBatch = true; batchFinished = false; batchCancelled = false; batchProgress = 0; $wire.start();" :disabled="startBatch" :class="startBatch ? 'disabled:bg-slate-100' : ''" class="bg-blue-300 p-2 rounded">Import</button>
21 
22 <template x-if="startBatch && !batchFinished">
23 <div class="relative pt-1" wire:key="batch-start" wire:poll="updateBatchProgress">
24 <div class="overflow-hidden h-4 flex rounded bg-green-100">
25 <div :style="{width: batchProgress + '%'}" class="bg-green-500 transition-all"></div>
26 </div>
27 <div class="flex justify-end" x-text="batchProgress + '%'"></div>
28 </div>
29 </template>
30 
31 <template x-if="batchFinished">
32 <div class="relative pt-1" wire:key="batch-end">
33 <div class="overflow-hidden h-4 flex rounded bg-green-100">
34 <div :style="{width: '100%'}" class="bg-green-500 transition-all"></div>
35 </div>
36 <div class="flex justify-end" x-text="'100%'"></div>
37 </div>
38 </template>
39 
40 <template x-if="batchCancelled && !batchFinished">
41 <div class="mt-4 flex justify-end">
42 <p class="text-red-600">Failed!</p>
43 </div>
44 </template>
45 </div>
46 
47 <div class="flex items-center gap-4 my-4">
48 <div class="flex-1">
49 <label>
50 <select wire:model.change="perPage">
51 <option>10</option>
52 <option>25</option>
53 <option>50</option>
54 <option>100</option>
55 </select>
56 entries per page
57 </label>
58 <input type="text" wire:model.change="search">
59 </div>
60 
61 <button wire:click="$refresh" class="rounded p-2 bg-blue-500 text-white">Refresh</button>
62 </div>
63 
64 <table class="w-full table-auto border-collapse border border-slate-400">
65 <thead>
66 <tr>
67 <th class="p-2 border border-slate-300">GlobalRank</th>
68 <th class="p-2 border border-slate-300">Domain</th>
69 <th class="p-2 border border-slate-300">TLD</th>
70 <th class="p-2 border border-slate-300">RefSubNets</th>
71 <th class="p-2 border border-slate-300">RefIPs</th>
72 <th class="p-2 border border-slate-300">PrevGlobalRank</th>
73 <th class="border border-slate-300">PrevRefSubNets</th>
74 <th class="border border-slate-300">PrevRefIPs</th>
75 </tr>
76 </thead>
77 <tbody>
78 @forelse ($this->websites as $site)
79 <tr @class(['bg-gray-100'=> ($loop->index % 2 === 0)])>
80 <td class="p-2 border border-slate-300">{{ $site->GlobalRank }}</td>
81 <td class="p-2 border border-slate-300">{{ $site->Domain }}</td>
82 <td class="p-2 border border-slate-300">{{ $site->TLD }}</td>
83 <td class="p-2 border border-slate-300">{{ $site->RefSubNets }}</td>
84 <td class="p-2 border border-slate-300">{{ $site->RefIPs }}</td>
85 <td class="p-2 border border-slate-300">{{ $site->PrevGlobalRank }}</td>
86 <td class="p-2 border border-slate-300">{{ $site->PrevRefSubNets }}</td>
87 <td class="p-2 border border-slate-300">{{ $site->PrevRefIPs }}</td>
88 </tr>
89 @empty
90 <tr>
91 <td class="p-2 text-center bg-gray-100" colspan="12">No data.</td>
92 </tr>
93 @endforelse
94 </tbody>
95 <tfoot>
96 <tr>
97 <td colspan="12">{{ $this->websites->links() }}</td>
98 </tr>
99 </tfoot>
100 </table>
101</div>

This example shows how to implements Job Batching with Livewire and AlpineJS using Majestic Million data.

The CSV file stored in csvfile directory.

GlobalRank Domain TLD RefSubNets RefIPs PrevGlobalRank PrevRefSubNets PrevRefIPs
No data.