Building an Automated QR Code System in Laravel
A quick look at how we architected automatic QR code generation using Laravel's queue system, scheduled commands, model observers, and action-based architecture.

The Problem
Every published project in our system needs a QR code for quick access and sharing.
Rather than generating these QR Codes on-demand which would block the request, we wanted an automated system that:
- Generates QR codes asynchronously
- Automatically creates missing QR codes
- Handles failures gracefully
- Provides clear user feedback
The Architecture
We built this using five key Laravel components:
- Action – Business logic for QR generation
- Job – Queued async processing
- Observer – Triggers QR generation on model changes
- Command – Scheduled missing QR detection
- React Component – User-facing display
1. The Action: Business Logic Layer
Following the action pattern, we isolated the QR generation logic:
class CreateQrCode extends Action
{
protected function handle(array $data, ?Model $model = null): mixed
{
$model->clearMediaCollection('qr-codes');
$qr = ($this->makeQr)([
'data' => route('resource.show', $model),
'ecc' => QrEcc::H,
'type' => QrType::WEBP,
]);
$model
->addMediaFromString($qr['data'])
->usingFileName("qr-{$model->id}.webp")
->toMediaCollection('qr');
return $model;
}
}
Why an action? Actions are reusable, testable, and keep business logic out of controllers, jobs, and commands.
2. The Job: Async Processing
The job delegates to the action:
class CreateQrCodeJob implements ShouldQueue
{
public function __construct(public Model $model) {}
public function handle(CreateQrCode $action): void
{
$action([], $this->model);
}
}
Why queue it? Image generation is CPU-intensive. Queuing keeps web requests fast and lets us control throughput.
3. The Observer: Automatic QR Generation on Updates
We use a model observer to automatically trigger QR generation whenever a project is created or updated in a way that affects its public URL.
class ProjectObserver
{
public function saved(Project $project): void
{
CreateQrCodeJob::dispatch($project);
}
}
The observer is registered once in a service provider:
Project::observe(ProjectObserver::class);
Why an observer?
- Keeps QR generation fully automatic
- Ensures updates (e.g. slug or visibility changes) always regenerate the QR
- Avoids coupling QR logic to controllers or forms
- Works seamlessly with queued jobs
4. The Command: Automated Recovery
We also set up a scheduled command that runs hourly to catch missing QR codes:
class MakeQrCodes extends Command
{
protected $signature = 'app:make-qr-codes {--all}';
public function handle(): int
{
$records = $this->option('all')
? Model::all()
: Model::whereDoesntHave('media', fn($q) =>
$q->where('collection_name', 'qr')
)->get();
if ($records->isEmpty()) {
info('No missing QR codes.');
return Command::SUCCESS;
}
progress(
label: 'Dispatching jobs',
steps: $records,
callback: fn($r) => CreateQrCodeJob::dispatch($r),
);
return Command::SUCCESS;
}
}
Scheduled:
Schedule::command('app:make-qr-codes')->hourly();
Why schedule it? It acts as a safety net for edge cases, failed jobs, or legacy records.
5. The Frontend: User Feedback
The component shows the QR code, or a “QR code is being generated” state:
export default function QrPanel({ record }) {
return record.qr ? (
<img src={record.qr} className="image-pixelated" />
) : (
<div>
<StatusIcon intent="warning" />
<p>QR code is being generated.</p>
</div>
);
}
Why show a pending state? Users get immediate feedback instead of empty UI, and the QR appears automatically once processed.
Spatie Media Library Integration
We manage QR codes as media attachments using Spatie Media Library.
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
class Model extends Model implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('qr')->singleFile();
}
}
Benefits:
- Database tracking in the
mediatable - Automatic accessors via
$model->qr - Storage flexibility (S3, local, etc.)
singleFile()ensures clean replacements
The Flow
- Project created or updated → Observer dispatches job
- Queue worker → Executes job
- Action → Generates and stores QR
- User views page → Sees QR or pending state
- Hourly command → Backfills any missing QR codes
Summary
This setup combines observers, queues, actions, and scheduled commands to create a reliable, hands-off QR generation system.
QR codes stay in sync with project changes, users get clear feedback, and background processing keeps the app fast — all while remaining easy to extend and maintain.