From d3b777d8e848542df66eeaf12bd9c65add985a07 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 01:13:41 +0200 Subject: [PATCH] feat(qr): QR code renderer (SVG + PNG), model, generate action Implements QrRenderer using chillerlan/php-qrcode v6 API (outputInterface instead of deprecated outputType constants), QrCode Eloquent model with ULID auto-generation and soft deletes, and GenerateQrCode action. Co-Authored-By: Claude Sonnet 4.6 --- app/Domains/QrCode/Actions/GenerateQrCode.php | 32 ++++++++++++ app/Domains/QrCode/Models/QrCode.php | 50 +++++++++++++++++++ app/Domains/QrCode/Services/QrRenderer.php | 48 ++++++++++++++++++ tests/Unit/QrCode/QrRendererTest.php | 18 +++++++ 4 files changed, 148 insertions(+) create mode 100644 app/Domains/QrCode/Actions/GenerateQrCode.php create mode 100644 app/Domains/QrCode/Models/QrCode.php create mode 100644 app/Domains/QrCode/Services/QrRenderer.php create mode 100644 tests/Unit/QrCode/QrRendererTest.php diff --git a/app/Domains/QrCode/Actions/GenerateQrCode.php b/app/Domains/QrCode/Actions/GenerateQrCode.php new file mode 100644 index 0000000..81476ce --- /dev/null +++ b/app/Domains/QrCode/Actions/GenerateQrCode.php @@ -0,0 +1,32 @@ +renderer->buildPayload($type, $payload); + + return QrCode::create([ + 'workspace_id' => $workspaceId, + 'link_id' => $linkId, + 'type' => $type, + 'payload' => $payload, + 'style' => $style ?: null, + 'created_by' => $creator->id, + ]); + } +} diff --git a/app/Domains/QrCode/Models/QrCode.php b/app/Domains/QrCode/Models/QrCode.php new file mode 100644 index 0000000..5a58578 --- /dev/null +++ b/app/Domains/QrCode/Models/QrCode.php @@ -0,0 +1,50 @@ + 'array', + 'style' => 'array', + 'ulid' => 'string', + ]; + + protected static function booted(): void + { + static::creating(function (self $model) { + if (empty($model->ulid)) { + $model->ulid = (string) Str::ulid(); + } + }); + } + + protected static function newFactory(): \Database\Factories\QrCodeFactory + { + return \Database\Factories\QrCodeFactory::new(); + } + + public function workspace(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(\App\Domains\Workspace\Models\Workspace::class); + } + + public function link(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(\App\Domains\Link\Models\Link::class); + } + + public function creator(): \Illuminate\Database\Eloquent\Relations\BelongsTo + { + return $this->belongsTo(\App\Models\User::class, 'created_by'); + } +} diff --git a/app/Domains/QrCode/Services/QrRenderer.php b/app/Domains/QrCode/Services/QrRenderer.php new file mode 100644 index 0000000..e50f79f --- /dev/null +++ b/app/Domains/QrCode/Services/QrRenderer.php @@ -0,0 +1,48 @@ + QRMarkupSVG::class, + 'outputBase64' => false, + ]); + + return (new QRCode($options))->render($data); + } + + public function toPngBase64(string $data, array $style): string + { + $options = new QROptions([ + 'outputInterface' => QRGdImagePNG::class, + 'outputBase64' => true, + 'scale' => 10, + ]); + + return (new QRCode($options))->render($data); + } + + public function buildPayload(string $type, array $data): string + { + return match ($type) { + 'url' => $data['url'], + 'text' => $data['text'], + 'wifi' => "WIFI:T:{$data['encryption']};S:{$data['ssid']};P:{$data['password']};;", + 'vcard' => $this->buildVcard($data), + default => $data['url'] ?? '', + }; + } + + private function buildVcard(array $d): string + { + return "BEGIN:VCARD\nVERSION:3.0\nFN:{$d['name']}\nEMAIL:{$d['email']}\nTEL:{$d['phone']}\nEND:VCARD"; + } +} diff --git a/tests/Unit/QrCode/QrRendererTest.php b/tests/Unit/QrCode/QrRendererTest.php new file mode 100644 index 0000000..b2fa3ee --- /dev/null +++ b/tests/Unit/QrCode/QrRendererTest.php @@ -0,0 +1,18 @@ +toSvg('https://example.com', []); + + expect($svg)->toContain('toContain(''); +}); + +it('generates png as base64', function () { + $renderer = new QrRenderer; + $png = $renderer->toPngBase64('https://example.com', []); + + expect($png)->toStartWith('data:image/png;base64,'); +});