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.

Pagoti QR Code Feature

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:

  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, 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 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.