Back

Job Batching

Let me see the code 👀
routes/web.php
1<?php
2 
3use Livewire\Volt\Volt;
4 
5Volt::route('/job-batching', 'job-batching');
resources/views/livewire/job-batching.blade.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\Attributes\Title;
11use Livewire\Volt\Component;
12use Livewire\WithPagination;
13 
14new
15#[Title('Job Batching - Larva Interactions')]
16class extends Component
17{
18 use WithPagination;
19 
20 public $perPage = 10;
21 public $search = '';
22 public $batchId;
23 public $batchFinished, $batchCancelled = false;
24 public $batchProgress = 0;
25 public $startBatch = false;
26 private $table = 'websites';
27 
28 public function updated($property)
29 {
30 if ($property === 'search') {
31 $this->resetPage();
32 }
33 }
34 
35 #[Computed]
36 public function websites()
37 {
38 $website = DB::table($this->table);
39 
40 if (!empty($this->search)) {
41 $search = trim(strtolower($this->search));
42 $website = $website->where('Domain', 'like', '%'.$search.'%');
43 }
44 
45 $website = $website->orderBy('GlobalRank')->cursorPaginate($this->perPage);
46 
47 return $website;
48 }
49 
50 public function start()
51 {
52 $this->startBatch = true;
53 
54 DB::table($this->table)->truncate();
55 
56 $websites = LazyCollection::make(function () {
57 $handle = fopen(base_path('csvfile/majestic_million.csv'), 'r');
58 
59 $header = fgetcsv($handle);
60 while (($line = fgetcsv($handle)) !== false) {
61 yield array_combine($header, $line);
62 }
63 });
64 
65 $batch = Bus::batch([])->dispatch();
66 $websites->chunk(1000)->each(function ($chunk) use ($batch) {
67 $batch->add(new ProcessInsertRecord('websites', $chunk->toArray()));
68 });
69 
70 $this->batchId = $batch->id;
71 }
72 
73 #[Computed]
74 public function batch()
75 {
76 if (!$this->batchId) {
77 return null;
78 }
79 
80 return Bus::findBatch($this->batchId);
81 }
82 
83 public function updateBatchProgress()
84 {
85 $this->batchProgress = $this->batch->progress();
86 $this->batchFinished = $this->batch->finished();
87 $this->batchCancelled = $this->batch->cancelled();
88 
89 if ($this->batchFinished) {
90 $this->startBatch = false;
91 }
92 }
93 
94 public function with(): array
95 {
96 return [
97 'md_content' => markdown_convert(resource_path('docs/job-batching.md'))
98 ];
99 }
100}
101?>
102 
103<div x-data="{ batchFinished: $wire.entangle('batchFinished').live,
104 batchCancelled: $wire.entangle('batchCancelled').live,
105 batchProgress: $wire.entangle('batchProgress').live,
106 startBatch: $wire.entangle('startBatch').live,
107 }" x-effect="console.log('batch finished: ', batchFinished, 'batch cancelled: ', batchCancelled, 'batch progress: ', batchProgress, 'start batch: ', startBatch)"
108 x-init="$watch('batchFinished', (value) => {
109 if (value) {
110 startBatch = false;
111 }
112 });">
113 
114 <a href="/" class="underline text-blue-500">Back</a>
115 
116 <h1 class="text-2xl">Job Batching</h1>
117 {!! $md_content !!}
118 <p>This example shows how to implements Job Batching with Livewire and Alpine using <a class="underline text-blue-500" href="https://blog.majestic.com/development/majestic-million-csv-daily/">Majestic Million data.</a></p>
119 <p>The CSV file stored in <code class="bg-gray-100 p-1 inline-block rounded">csvfile</code> directory.</p>
120 
121 <div class="my-4">
122 <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>
123 
124 <template x-if="startBatch && !batchFinished">
125 <div class="relative pt-1" wire:key="batch-start" wire:poll="updateBatchProgress">
126 <div class="overflow-hidden h-4 flex rounded bg-green-100">
127 <div :style="{width: batchProgress + '%'}" class="bg-green-500 transition-all"></div>
128 </div>
129 <div class="flex justify-end" x-text="batchProgress + '%'"></div>
130 </div>
131 </template>
132 
133 <template x-if="batchFinished">
134 <div class="relative pt-1" wire:key="batch-end">
135 <div class="overflow-hidden h-4 flex rounded bg-green-100">
136 <div :style="{width: '100%'}" class="bg-green-500 transition-all"></div>
137 </div>
138 <div class="flex justify-end" x-text="'100%'"></div>
139 </div>
140 </template>
141 
142 <template x-if="batchCancelled && !batchFinished">
143 <div class="mt-4 flex justify-end">
144 <p class="text-red-600">Failed!</p>
145 </div>
146 </template>
147 </div>
148 
149 <div class="flex items-center gap-4 my-4">
150 <div class="flex-1">
151 <label>
152 <select wire:model.change="perPage">
153 <option>10</option>
154 <option>25</option>
155 <option>50</option>
156 <option>100</option>
157 </select>
158 entries per page
159 </label>
160 <input type="text" wire:model.change="search">
161 </div>
162 
163 <button wire:click="$refresh" class="rounded p-2 bg-blue-500 text-white">Refresh</button>
164 </div>
165 
166 <table class="w-full table-auto border-collapse border border-slate-400">
167 <thead>
168 <tr>
169 <th class="p-2 border border-slate-300">GlobalRank</th>
170 <th class="p-2 border border-slate-300">Domain</th>
171 <th class="p-2 border border-slate-300">TLD</th>
172 <th class="p-2 border border-slate-300">RefSubNets</th>
173 <th class="p-2 border border-slate-300">RefIPs</th>
174 <th class="p-2 border border-slate-300">PrevGlobalRank</th>
175 <th class="border border-slate-300">PrevRefSubNets</th>
176 <th class="border border-slate-300">PrevRefIPs</th>
177 </tr>
178 </thead>
179 <tbody>
180 @forelse ($this->websites as $site)
181 <tr @class(['bg-gray-100'=> ($loop->index % 2 === 0)])>
182 <td class="p-2 border border-slate-300">{{ $site->GlobalRank }}</td>
183 <td class="p-2 border border-slate-300">{{ $site->Domain }}</td>
184 <td class="p-2 border border-slate-300">{{ $site->TLD }}</td>
185 <td class="p-2 border border-slate-300">{{ $site->RefSubNets }}</td>
186 <td class="p-2 border border-slate-300">{{ $site->RefIPs }}</td>
187 <td class="p-2 border border-slate-300">{{ $site->PrevGlobalRank }}</td>
188 <td class="p-2 border border-slate-300">{{ $site->PrevRefSubNets }}</td>
189 <td class="p-2 border border-slate-300">{{ $site->PrevRefIPs }}</td>
190 </tr>
191 @empty
192 <tr>
193 <td class="p-2 text-center bg-gray-100" colspan="12">No data.</td>
194 </tr>
195 @endforelse
196 </tbody>
197 <tfoot>
198 <tr>
199 <td colspan="12">{{ $this->websites->links() }}</td>
200 </tr>
201 </tfoot>
202 </table>
203</div>

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

The CSV file stored in csvfile directory.

GlobalRank Domain TLD RefSubNets RefIPs PrevGlobalRank PrevRefSubNets PrevRefIPs
No data.