Building an Automated QR Code System in Laravel

A quick look at how automatic QR code generation was architected using Laravel's queue system, scheduled commands, model observers, and action-based architecture.

The Problem

Every published project needs a QR code for quick access and sharing.

Rather than generating QR Codes on-demand — which would block the request — the goal was an automated system that:

  • Generates QR codes asynchronously
  • Automatically creates missing QR codes
  • Handles failures gracefully
  • Provides clear user feedback

The Architecture

This was built using five key Laravel components:

  1. Action – Business logic for QR generation
  2. Job – Queued async processing
  3. Observer – Triggers QR generation on model changes
  4. Command – Scheduled missing QR detection
  5. React Component – User-facing display

1. The Action: Business Logic Layer

Following the action pattern, the QR generation logic is isolated:

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 throughput be controlled.

3. The Observer: Automatic QR Generation on Updates

A model observer automatically triggers 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

A scheduled command 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

QR codes are managed 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 media table
  • Automatic accessors via $model->qr
  • Storage flexibility (S3, local, etc.)
  • singleFile() ensures clean replacements

The Flow

  1. Project created or updated → Observer dispatches job
  2. Queue worker → Executes job
  3. Action → Generates and stores QR
  4. User views page → Sees QR or pending state
  5. 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.