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 <noreply@anthropic.com>main
parent
dd89ee395c
commit
d3b777d8e8
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\QrCode\Actions;
|
||||||
|
|
||||||
|
use App\Domains\QrCode\Models\QrCode;
|
||||||
|
use App\Domains\QrCode\Services\QrRenderer;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class GenerateQrCode
|
||||||
|
{
|
||||||
|
public function __construct(private readonly QrRenderer $renderer) {}
|
||||||
|
|
||||||
|
public function handle(
|
||||||
|
int $workspaceId,
|
||||||
|
string $type,
|
||||||
|
array $payload,
|
||||||
|
array $style,
|
||||||
|
User $creator,
|
||||||
|
?int $linkId = null,
|
||||||
|
): QrCode {
|
||||||
|
$data = $this->renderer->buildPayload($type, $payload);
|
||||||
|
|
||||||
|
return QrCode::create([
|
||||||
|
'workspace_id' => $workspaceId,
|
||||||
|
'link_id' => $linkId,
|
||||||
|
'type' => $type,
|
||||||
|
'payload' => $payload,
|
||||||
|
'style' => $style ?: null,
|
||||||
|
'created_by' => $creator->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\QrCode\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class QrCode extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = ['id'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'payload' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\QrCode\Services;
|
||||||
|
|
||||||
|
use chillerlan\QRCode\QRCode;
|
||||||
|
use chillerlan\QRCode\QROptions;
|
||||||
|
use chillerlan\QRCode\Output\QRMarkupSVG;
|
||||||
|
use chillerlan\QRCode\Output\QRGdImagePNG;
|
||||||
|
|
||||||
|
class QrRenderer
|
||||||
|
{
|
||||||
|
public function toSvg(string $data, array $style): string
|
||||||
|
{
|
||||||
|
$options = new QROptions([
|
||||||
|
'outputInterface' => 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\QrCode\Services\QrRenderer;
|
||||||
|
|
||||||
|
it('generates valid SVG for URL type', function () {
|
||||||
|
$renderer = new QrRenderer;
|
||||||
|
$svg = $renderer->toSvg('https://example.com', []);
|
||||||
|
|
||||||
|
expect($svg)->toContain('<svg')
|
||||||
|
->toContain('</svg>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates png as base64', function () {
|
||||||
|
$renderer = new QrRenderer;
|
||||||
|
$png = $renderer->toPngBase64('https://example.com', []);
|
||||||
|
|
||||||
|
expect($png)->toStartWith('data:image/png;base64,');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue