Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
@@ -45,7 +46,6 @@ enum AiModel: string
|
||||
case OLLAMA_LLAMA3_2_3B = 'llama3.2:3b';
|
||||
case OLLAMA_DEEPSEEK_CODER = 'deepseek-coder:6.7b';
|
||||
|
||||
|
||||
public function getProvider(): AiProvider
|
||||
{
|
||||
return match($this) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
@@ -11,5 +12,6 @@ final class AiQuery
|
||||
public array $messages = [],
|
||||
public float $temperature = 0.7,
|
||||
public ?int $maxTokens = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
@@ -10,5 +11,6 @@ final class AiResponse
|
||||
public string $provider,
|
||||
public string $model,
|
||||
public ?int $tokensUsed = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI\Exception;
|
||||
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
use App\Domain\AI\AiProvider;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
class AiProviderUnavailableException extends FrameworkException
|
||||
{
|
||||
@@ -14,6 +16,15 @@ class AiProviderUnavailableException extends FrameworkException
|
||||
if ($reason) {
|
||||
$message .= ": $reason";
|
||||
}
|
||||
parent::__construct($message);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: ExceptionContext::forOperation('ai_provider_connection', 'AI')
|
||||
->withData([
|
||||
'provider' => $provider->value,
|
||||
'reason' => $reason,
|
||||
'unavailable' => true,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\AI;
|
||||
|
||||
enum Role:string
|
||||
enum Role: string
|
||||
{
|
||||
case SYSTEM = 'system';
|
||||
case USER = 'user';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Common\ValueObject;
|
||||
@@ -15,13 +16,13 @@ final readonly class Email
|
||||
throw new \InvalidArgumentException('E-Mail-Adresse darf nicht leer sein.');
|
||||
}
|
||||
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \InvalidArgumentException('Ungültige E-Mail-Adresse: ' . $value);
|
||||
}
|
||||
|
||||
// Optional: Einfache Domain-Prüfung ohne externen Aufruf
|
||||
$domain = substr(strrchr($value, '@'), 1);
|
||||
if (strlen($domain) < 4 || !str_contains($domain, '.')) {
|
||||
if (strlen($domain) < 4 || ! str_contains($domain, '.')) {
|
||||
throw new \InvalidArgumentException('Ungültige Domain in der E-Mail-Adresse: ' . $domain);
|
||||
}
|
||||
|
||||
|
||||
247
src/Domain/Common/ValueObject/PhoneNumber.php
Normal file
247
src/Domain/Common/ValueObject/PhoneNumber.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Common\ValueObject;
|
||||
|
||||
final readonly class PhoneNumber
|
||||
{
|
||||
/**
|
||||
* @param string $value Phone number in various formats
|
||||
*/
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (empty($value)) {
|
||||
throw new \InvalidArgumentException('Phone number cannot be empty.');
|
||||
}
|
||||
|
||||
$this->validate($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from string value
|
||||
*/
|
||||
public static function from(string $value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse from various formats
|
||||
*/
|
||||
public static function parse(string $value): self
|
||||
{
|
||||
$normalized = self::normalize($value);
|
||||
|
||||
return new self($normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if string is a valid phone number
|
||||
*/
|
||||
public static function isValid(string $value): bool
|
||||
{
|
||||
try {
|
||||
new self($value);
|
||||
|
||||
return true;
|
||||
} catch (\InvalidArgumentException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the normalized phone number value
|
||||
*/
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country code if present
|
||||
*/
|
||||
public function getCountryCode(): ?string
|
||||
{
|
||||
if (str_starts_with($this->value, '+')) {
|
||||
// Extract country code (1-4 digits after +)
|
||||
if (preg_match('/^\+(\d{1,4})/', $this->value, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get national number (without country code)
|
||||
*/
|
||||
public function getNationalNumber(): string
|
||||
{
|
||||
$countryCode = $this->getCountryCode();
|
||||
if ($countryCode !== null) {
|
||||
return substr($this->value, strlen($countryCode) + 1);
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it's a mobile number (basic heuristic)
|
||||
*/
|
||||
public function isMobile(): bool
|
||||
{
|
||||
$countryCode = $this->getCountryCode();
|
||||
$nationalNumber = $this->getNationalNumber();
|
||||
|
||||
// German mobile numbers typically start with 15, 16, 17
|
||||
if ($countryCode === '49') {
|
||||
return preg_match('/^(15|16|17)\d/', str_replace([' ', '-', '.'], '', $nationalNumber)) === 1;
|
||||
}
|
||||
|
||||
// US mobile numbers (basic check for common prefixes)
|
||||
if ($countryCode === '1') {
|
||||
$cleaned = str_replace([' ', '-', '.', '(', ')'], '', $nationalNumber);
|
||||
|
||||
return strlen($cleaned) === 10;
|
||||
}
|
||||
|
||||
// General heuristic: numbers with 10+ digits are likely mobile
|
||||
$digitsOnly = preg_replace('/\D/', '', $nationalNumber);
|
||||
|
||||
return strlen($digitsOnly) >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format as E.164 international format
|
||||
*/
|
||||
public function toE164(): string
|
||||
{
|
||||
if (str_starts_with($this->value, '+')) {
|
||||
return '+' . preg_replace('/\D/', '', $this->value);
|
||||
}
|
||||
|
||||
// If no country code, assume German (+49) for demo
|
||||
$digitsOnly = preg_replace('/\D/', '', $this->value);
|
||||
if (str_starts_with($digitsOnly, '0')) {
|
||||
return '+49' . substr($digitsOnly, 1);
|
||||
}
|
||||
|
||||
return '+49' . $digitsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format as international display format
|
||||
*/
|
||||
public function toInternational(): string
|
||||
{
|
||||
$e164 = $this->toE164();
|
||||
$countryCode = $this->getCountryCode() ?? '49';
|
||||
$nationalNumber = substr($e164, strlen($countryCode) + 1);
|
||||
|
||||
// Format German numbers: +49 123 456789
|
||||
if ($countryCode === '49') {
|
||||
return "+49 " . chunk_split($nationalNumber, 3, ' ');
|
||||
}
|
||||
|
||||
// Format US numbers: +1 (123) 456-7890
|
||||
if ($countryCode === '1' && strlen($nationalNumber) === 10) {
|
||||
return "+1 (" . substr($nationalNumber, 0, 3) . ") " .
|
||||
substr($nationalNumber, 3, 3) . "-" . substr($nationalNumber, 6);
|
||||
}
|
||||
|
||||
// Default formatting
|
||||
return "+{$countryCode} " . chunk_split($nationalNumber, 3, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format as national display format
|
||||
*/
|
||||
public function toNational(): string
|
||||
{
|
||||
$nationalNumber = $this->getNationalNumber();
|
||||
$digitsOnly = preg_replace('/\D/', '', $nationalNumber);
|
||||
|
||||
// German format: 0123 456789
|
||||
if ($this->getCountryCode() === '49') {
|
||||
return '0' . chunk_split($digitsOnly, 3, ' ');
|
||||
}
|
||||
|
||||
// US format: (123) 456-7890
|
||||
if ($this->getCountryCode() === '1' && strlen($digitsOnly) === 10) {
|
||||
return "(" . substr($digitsOnly, 0, 3) . ") " .
|
||||
substr($digitsOnly, 3, 3) . "-" . substr($digitsOnly, 6);
|
||||
}
|
||||
|
||||
// Default: add spaces every 3 digits
|
||||
return chunk_split($digitsOnly, 3, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check equality with another phone number
|
||||
*/
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
// Compare normalized E.164 format for accurate comparison
|
||||
return $this->toE164() === $other->toE164();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if from same country
|
||||
*/
|
||||
public function isSameCountry(self $other): bool
|
||||
{
|
||||
return $this->getCountryCode() === $other->getCountryCode();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate phone number format
|
||||
*/
|
||||
private function validate(string $value): void
|
||||
{
|
||||
// Must contain at least some digits
|
||||
if (! preg_match('/\d/', $value)) {
|
||||
throw new \InvalidArgumentException("Phone number must contain digits: {$value}");
|
||||
}
|
||||
|
||||
// Must be reasonable length (5-20 characters after normalization)
|
||||
$normalized = self::normalize($value);
|
||||
$digitsOnly = preg_replace('/\D/', '', $normalized);
|
||||
$digitCount = strlen($digitsOnly);
|
||||
|
||||
if ($digitCount < 5) {
|
||||
throw new \InvalidArgumentException("Phone number too short: {$value}");
|
||||
}
|
||||
|
||||
if ($digitCount > 20) {
|
||||
throw new \InvalidArgumentException("Phone number too long: {$value}");
|
||||
}
|
||||
|
||||
// Basic format validation
|
||||
if (! preg_match('/^[\+]?[\d\s\-\.\(\)]+$/', $value)) {
|
||||
throw new \InvalidArgumentException("Invalid phone number format: {$value}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize phone number (trim, clean basic formatting)
|
||||
*/
|
||||
private static function normalize(string $value): string
|
||||
{
|
||||
$value = trim($value);
|
||||
|
||||
// Replace common separators with spaces
|
||||
$value = str_replace(['-', '.', '(', ')'], ' ', $value);
|
||||
|
||||
// Clean up multiple spaces
|
||||
$value = preg_replace('/\s+/', ' ', $value);
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Common\ValueObject;
|
||||
@@ -10,14 +11,17 @@ final readonly class RGBColor
|
||||
public int $green,
|
||||
public int $blue
|
||||
) {
|
||||
foreach([$red, $green, $blue,
|
||||
foreach ([$red, $green, $blue,
|
||||
] as $value) {
|
||||
if ($value < 0 || $value > 255) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'RGB-Farbwert (%d, %d, %d) muss zwischen 0 und 255 liegen.',
|
||||
$red, $green, $blue
|
||||
));
|
||||
$red,
|
||||
$green,
|
||||
$blue
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +31,7 @@ final readonly class RGBColor
|
||||
$red = hexdec(substr($hex, 0, 2));
|
||||
$green = hexdec(substr($hex, 2, 2));
|
||||
$blue = hexdec(substr($hex, 4, 2));
|
||||
|
||||
return new self($red, $green, $blue);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Contact;
|
||||
@@ -10,9 +11,8 @@ final readonly class ContactMessage
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
|
||||
public string $email,
|
||||
|
||||
public string $message,
|
||||
){}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Contact;
|
||||
@@ -9,7 +10,8 @@ final readonly class ContactRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(ContactMessage $contact): void
|
||||
{
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Contact\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\Schema\Blueprint;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final readonly class CreateContactTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->create('contacts', function (Blueprint $blueprint) {
|
||||
$blueprint->id()->autoIncrement()->primary();
|
||||
$blueprint->string('name');
|
||||
$blueprint->string('email');
|
||||
$blueprint->text('message');
|
||||
});
|
||||
|
||||
$schema->execute();
|
||||
|
||||
/*$sql = <<<SQL
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
@@ -18,17 +33,22 @@ final readonly class CreateContactTable implements Migration
|
||||
message TEXT NOT NULL
|
||||
)
|
||||
SQL;
|
||||
$connection->execute($sql);
|
||||
$connection->execute($sql);*/
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$connection->execute('DROP TABLE IF EXISTS contacts');
|
||||
$schema = new Schema($connection);
|
||||
$schema->dropIfExists('contacts');
|
||||
|
||||
$schema->execute();
|
||||
|
||||
#$connection->execute('DROP TABLE IF EXISTS contacts');
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return '005';
|
||||
return MigrationVersion::fromTimestamp("2024_01_16_000005");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -98,6 +99,7 @@ final readonly class GdImageProcessor implements ImageProcessorInterface
|
||||
private function generateVariantFilename(string $originalFilename, string $type, string $size, string $format): string
|
||||
{
|
||||
$pathInfo = pathinfo($originalFilename);
|
||||
|
||||
return $pathInfo['filename'] . "_{$type}_{$size}.{$format}";
|
||||
}
|
||||
|
||||
@@ -138,6 +140,7 @@ final readonly class GdImageProcessor implements ImageProcessorInterface
|
||||
imagedestroy($source);
|
||||
$this->saveImageInFormat($dst, $destination, $format);
|
||||
imagedestroy($dst);
|
||||
|
||||
return ['width' => $newWidth, 'height' => $newHeight];
|
||||
}
|
||||
|
||||
@@ -204,7 +207,7 @@ final readonly class GdImageProcessor implements ImageProcessorInterface
|
||||
$sharpenMatrix = [
|
||||
[-1, -1, -1],
|
||||
[-1, 16, -1],
|
||||
[-1, -1, -1]
|
||||
[-1, -1, -1],
|
||||
];
|
||||
$divisor = 8;
|
||||
$offset = 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -37,5 +38,6 @@ final readonly class Image
|
||||
public string $path,
|
||||
#[Column(name: 'alt_text')]
|
||||
public string $altText,
|
||||
){}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
enum ImageFormat: string
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\Database\Attributes\Entity;
|
||||
@@ -7,5 +9,4 @@ use App\Framework\Database\Attributes\Entity;
|
||||
#[Entity(tableName: 'image_galleries')]
|
||||
class ImageGallery
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -21,6 +22,7 @@ final readonly class ImageProcessorFactory
|
||||
// Test ob ImageMagick funktioniert
|
||||
$test = new \Imagick();
|
||||
$test->clear();
|
||||
|
||||
return new ImagickImageProcessor();
|
||||
} catch (\Exception $e) {
|
||||
error_log('ImageMagick is installed but not working: ' . $e->getMessage());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\EntityManager;
|
||||
use App\Framework\Database\Transaction;
|
||||
|
||||
final readonly class ImageRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private ConnectionInterface $connection,
|
||||
) {}
|
||||
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(Image $image, string $tempPath): void
|
||||
{
|
||||
@@ -27,7 +25,6 @@ final readonly class ImageRepository
|
||||
return $this->entityManager->findOneBy(ImageSlot::class, ['slot_name' => $slotName])->image;
|
||||
}
|
||||
|
||||
|
||||
public function findById(string $id): ?Image
|
||||
{
|
||||
return $this->entityManager->find(Image::class, $id);
|
||||
@@ -43,8 +40,90 @@ final readonly class ImageRepository
|
||||
return $this->entityManager->findOneBy(Image::class, ['hash' => $hash]);
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
public function findAll(int $limit = 50, int $offset = 0, ?string $search = null): array
|
||||
{
|
||||
return $this->entityManager->findAll(Image::class);
|
||||
// Simplified version - use EntityManager::findAll for now
|
||||
$allImages = $this->entityManager->findAll(Image::class);
|
||||
|
||||
// Apply search filter if provided
|
||||
if ($search) {
|
||||
$allImages = array_filter($allImages, function ($image) use ($search) {
|
||||
return stripos($image->originalFilename, $search) !== false ||
|
||||
stripos($image->altText, $search) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply offset and limit
|
||||
return array_slice($allImages, $offset, $limit);
|
||||
}
|
||||
|
||||
public function count(?string $search = null): int
|
||||
{
|
||||
$allImages = $this->entityManager->findAll(Image::class);
|
||||
|
||||
if ($search) {
|
||||
$allImages = array_filter($allImages, function ($image) use ($search) {
|
||||
return stripos($image->originalFilename, $search) !== false ||
|
||||
stripos($image->altText, $search) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
return count($allImages);
|
||||
}
|
||||
|
||||
public function findByUlid(string $ulid): ?Image
|
||||
{
|
||||
return $this->entityManager->find(Image::class, $ulid);
|
||||
}
|
||||
|
||||
public function updateAltText(string $ulid, string $altText): void
|
||||
{
|
||||
$image = $this->findByUlid($ulid);
|
||||
|
||||
if ($image) {
|
||||
$image->altText = $altText;
|
||||
$this->entityManager->save($image);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateFilename(string $ulid, string $filename): void
|
||||
{
|
||||
$image = $this->findByUlid($ulid);
|
||||
|
||||
if ($image) {
|
||||
$image->filename = $filename;
|
||||
$this->entityManager->save($image);
|
||||
}
|
||||
}
|
||||
|
||||
public function search(string $query, ?string $type = null, int $minWidth = 0, int $minHeight = 0): array
|
||||
{
|
||||
$allImages = $this->entityManager->findAll(Image::class);
|
||||
|
||||
$filteredImages = array_filter($allImages, function ($image) use ($query, $type, $minWidth, $minHeight) {
|
||||
// Text search
|
||||
if ($query && ! (stripos($image->originalFilename, $query) !== false || stripos($image->altText, $query) !== false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if ($type && $image->mimeType !== 'image/' . $type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Size filters
|
||||
if ($minWidth > 0 && $image->width < $minWidth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($minHeight > 0 && $image->height < $minHeight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Limit to 100 results
|
||||
return array_slice($filteredImages, 0, 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Ulid\StringConverter;
|
||||
use App\Framework\Ulid\Ulid;
|
||||
|
||||
final readonly class ImageResizer
|
||||
{
|
||||
public function __construct() {}
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
Image $image,
|
||||
int $maxWidth,
|
||||
int $maxHeight,
|
||||
int $quality = 9
|
||||
): ImageVariant
|
||||
{
|
||||
): ImageVariant {
|
||||
$sourcePath = $image->path . $image->filename;
|
||||
|
||||
$filename = str_replace('original', 'thumbnail', $image->filename);
|
||||
@@ -26,7 +27,7 @@ final readonly class ImageResizer
|
||||
|
||||
$imageInfo = getimagesize($sourcePath);
|
||||
|
||||
if($imageInfo === false) {
|
||||
if ($imageInfo === false) {
|
||||
throw new \RuntimeException('Could not get image info');
|
||||
}
|
||||
|
||||
@@ -71,8 +72,8 @@ final readonly class ImageResizer
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private function createImageFromFile(string $sourcePath, mixed $imageType) {
|
||||
private function createImageFromFile(string $sourcePath, mixed $imageType): \GdImage
|
||||
{
|
||||
return match ($imageType) {
|
||||
IMAGETYPE_PNG => imagecreatefrompng($sourcePath),
|
||||
IMAGETYPE_JPEG => imagecreatefromjpeg($sourcePath),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
enum ImageSize: string
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\Database\Attributes\Column;
|
||||
@@ -17,5 +19,6 @@ final readonly class ImageSlot
|
||||
public string $slotName,
|
||||
#[Column(name: 'image_id')]
|
||||
public string $imageId,
|
||||
){}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\Database\EntityManager;
|
||||
@@ -8,7 +10,8 @@ final readonly class ImageSlotRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
){}
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSlots(): array
|
||||
{
|
||||
@@ -29,4 +32,48 @@ final readonly class ImageSlotRepository
|
||||
{
|
||||
return $this->entityManager->save($imageSlot);
|
||||
}
|
||||
|
||||
public function findAllWithImages(): array
|
||||
{
|
||||
$slots = $this->entityManager->findAll(ImageSlot::class);
|
||||
|
||||
return array_map(function (ImageSlot $slot) {
|
||||
$image = null;
|
||||
if ($slot->imageId) {
|
||||
$image = $this->entityManager->find(Image::class, $slot->imageId);
|
||||
}
|
||||
|
||||
return ImageSlotView::fromSlot($slot, $image);
|
||||
}, $slots);
|
||||
}
|
||||
|
||||
public function findByIdWithImage(string $id): ImageSlotView
|
||||
{
|
||||
$slot = $this->entityManager->find(ImageSlot::class, $id);
|
||||
|
||||
if (! $slot) {
|
||||
throw new \RuntimeException("ImageSlot with ID {$id} not found");
|
||||
}
|
||||
|
||||
$image = null;
|
||||
if ($slot->imageId) {
|
||||
$image = $this->entityManager->find(Image::class, $slot->imageId);
|
||||
}
|
||||
|
||||
return ImageSlotView::fromSlot($slot, $image);
|
||||
}
|
||||
|
||||
public function updateImageId(string $slotId, string $imageId): void
|
||||
{
|
||||
$slot = $this->findById($slotId);
|
||||
#$slot->imageId = $imageId;
|
||||
|
||||
$slot = new ImageSlot(
|
||||
$slot->id,
|
||||
$slot->slotName,
|
||||
$imageId
|
||||
);
|
||||
|
||||
$this->entityManager->save($slot);
|
||||
}
|
||||
}
|
||||
|
||||
26
src/Domain/Media/ImageSlotView.php
Normal file
26
src/Domain/Media/ImageSlotView.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
final readonly class ImageSlotView
|
||||
{
|
||||
public function __construct(
|
||||
public int $id,
|
||||
public string $slotName,
|
||||
public string $imageId,
|
||||
public ?Image $image
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromSlot(ImageSlot $slot, ?Image $image = null): self
|
||||
{
|
||||
return new self(
|
||||
$slot->id,
|
||||
$slot->slotName,
|
||||
$slot->imageId,
|
||||
$image
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -36,7 +37,7 @@ final readonly class ImageSourceSetGenerator
|
||||
$attributes['width'] = $fallbackImage->width;
|
||||
$attributes['height'] = $fallbackImage->height;
|
||||
|
||||
if (!isset($attributes['alt'])) {
|
||||
if (! isset($attributes['alt'])) {
|
||||
$attributes['alt'] = '';
|
||||
}
|
||||
|
||||
@@ -114,6 +115,11 @@ final readonly class ImageSourceSetGenerator
|
||||
{
|
||||
$variantsByFormat = [];
|
||||
|
||||
// Check if variants are loaded to avoid uninitialized readonly property error
|
||||
if (! isset($image->variants)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($image->variants as $variant) {
|
||||
if ($variant->variantType === $variantType) {
|
||||
$variantsByFormat[$variant->format][] = $variant;
|
||||
@@ -133,15 +139,16 @@ final readonly class ImageSourceSetGenerator
|
||||
private function getFallbackImage(array $variantsByFormat, Image $image): ImageVariant
|
||||
{
|
||||
// Bevorzugen JPEG als Fallback
|
||||
if (isset($variantsByFormat['jpeg']) && !empty($variantsByFormat['jpeg'])) {
|
||||
if (! empty($variantsByFormat['jpeg'])) {
|
||||
// Mittlere Größe als Fallback verwenden
|
||||
$variants = $variantsByFormat['jpeg'];
|
||||
|
||||
return $variants[min(1, count($variants) - 1)];
|
||||
}
|
||||
|
||||
// Alternativen, falls kein JPEG verfügbar
|
||||
foreach (['webp', 'avif'] as $format) {
|
||||
if (isset($variantsByFormat[$format]) && !empty($variantsByFormat[$format])) {
|
||||
if (! empty($variantsByFormat[$format])) {
|
||||
return $variantsByFormat[$format][0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -33,10 +34,10 @@ final readonly class ImageVariant
|
||||
public string $filename,
|
||||
#[Column(name: 'path')]
|
||||
public string $path,
|
||||
|
||||
#[Column(name: 'id', primary: true, autoIncrement: true)]
|
||||
public ?int $id = null,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -8,7 +9,7 @@ final readonly class ImageVariantConfig
|
||||
public static function getAllVariants(): array
|
||||
{
|
||||
$variants = [];
|
||||
|
||||
|
||||
foreach (ImageVariantType::cases() as $type) {
|
||||
foreach ($type->getSizes() as $size) {
|
||||
foreach (ImageFormat::cases() as $format) {
|
||||
@@ -21,14 +22,14 @@ final readonly class ImageVariantConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $variants;
|
||||
}
|
||||
|
||||
public static function getVariantsForType(ImageVariantType $type): array
|
||||
{
|
||||
$variants = [];
|
||||
|
||||
|
||||
foreach ($type->getSizes() as $size) {
|
||||
foreach (ImageFormat::cases() as $format) {
|
||||
$variants[] = [
|
||||
@@ -39,7 +40,7 @@ final readonly class ImageVariantConfig
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $variants;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
@@ -9,14 +10,16 @@ final class ImageVariantRepository
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManager $entityManager
|
||||
){}
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(ImageVariant $imageVariant): void
|
||||
{
|
||||
$this->entityManager->save($imageVariant);
|
||||
}
|
||||
|
||||
public function findByFilename(string $filename): ?ImageVariant {
|
||||
public function findByFilename(string $filename): ?ImageVariant
|
||||
{
|
||||
return $this->entityManager->findOneBy(ImageVariant::class, ['filename' => $filename]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
enum ImageVariantType: string
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use Imagick;
|
||||
use ImagickException;
|
||||
use ImagickPixel;
|
||||
|
||||
/**
|
||||
* ImageMagick-basierter Bildprozessor für optimale Bildqualität
|
||||
@@ -102,6 +102,7 @@ final readonly class ImagickImageProcessor implements ImageProcessorInterface
|
||||
private function generateVariantFilename(string $originalFilename, string $type, string $size, string $format): string
|
||||
{
|
||||
$pathInfo = pathinfo($originalFilename);
|
||||
|
||||
return $pathInfo['filename'] . "_{$type}_{$size}.{$format}";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final readonly class AddSizeToImageVariantsTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$connection->execute("ALTER TABLE image_variants ADD COLUMN size VARCHAR(25) NOT NULL DEFAULT ''");
|
||||
$schema = new Schema($connection);
|
||||
|
||||
/*$schema->table('image_variants', function ($table) {
|
||||
$table->string('size', 25)->default('');
|
||||
});*/
|
||||
|
||||
if (! $schema->hasColumn('image_variants', 'size')) {
|
||||
$connection->execute("ALTER TABLE image_variants ADD COLUMN size VARCHAR(25) NOT NULL DEFAULT ''");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$connection->execute("ALTER TABLE image_variants DROP COLUMN size");
|
||||
$schema = new Schema($connection);
|
||||
|
||||
if ($schema->hasColumn('image_variants', 'size')) {
|
||||
$connection->execute("ALTER TABLE image_variants DROP COLUMN size");
|
||||
}
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "005";
|
||||
return MigrationVersion::fromTimestamp("2024_01_16_000005");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
final readonly class CreateImageSlotsTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
@@ -31,9 +33,9 @@ SQL;
|
||||
$connection->execute("DROP TABLE IF EXISTS image_slots");
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "004";
|
||||
return MigrationVersion::fromTimestamp("2024_01_15_000004");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
final class CreateImageVariantsTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
@@ -44,9 +45,9 @@ SQL;
|
||||
$connection->execute("DROP TABLE IF EXISTS image_variants");
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "003";
|
||||
return MigrationVersion::fromTimestamp("2024_01_15_000003");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
final class CreateImagesTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
@@ -40,9 +41,9 @@ SQL;
|
||||
$connection->execute("DROP TABLE IF EXISTS images");
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "002";
|
||||
return MigrationVersion::fromTimestamp("2024_01_15_000002");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
62
src/Domain/Media/Migrations/CreateImagesTableWithSchema.php
Normal file
62
src/Domain/Media/Migrations/CreateImagesTableWithSchema.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
use App\Framework\Database\Schema\Blueprint;
|
||||
use App\Framework\Database\Schema\Schema;
|
||||
|
||||
final class CreateImagesTableWithSchema implements Migration
|
||||
{
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
|
||||
$schema->create('images', function (Blueprint $table) {
|
||||
$table->ulid('ulid')->primary();
|
||||
$table->string('filename', 255);
|
||||
$table->string('original_filename', 255);
|
||||
$table->string('mime_type', 100);
|
||||
$table->bigInteger('file_size');
|
||||
$table->unsignedInteger('width');
|
||||
$table->unsignedInteger('height');
|
||||
$table->string('hash', 255)->unique();
|
||||
$table->string('path', 500);
|
||||
$table->text('alt_text');
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes
|
||||
$table->unique(['hash'], 'uk_images_hash');
|
||||
$table->index(['mime_type']);
|
||||
$table->index(['created_at']);
|
||||
|
||||
// Table options
|
||||
$table->engine('InnoDB');
|
||||
$table->charset('utf8mb4');
|
||||
$table->collation('utf8mb4_unicode_ci');
|
||||
});
|
||||
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function down(ConnectionInterface $connection): void
|
||||
{
|
||||
$schema = new Schema($connection);
|
||||
$schema->dropIfExists('images');
|
||||
$schema->execute();
|
||||
}
|
||||
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return MigrationVersion::fromTimestamp("2024_01_17_000003");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Create Images Table with Schema Builder";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
final readonly class UpdateImageVariantsConstraint implements Migration
|
||||
{
|
||||
@@ -23,9 +26,9 @@ final readonly class UpdateImageVariantsConstraint implements Migration
|
||||
$connection->execute("ALTER TABLE image_variants ADD UNIQUE KEY uk_image_variants_combination (image_id, variant_type, format)");
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "006";
|
||||
return MigrationVersion::fromTimestamp("2024_01_18_000006");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Media;
|
||||
|
||||
use App\Framework\Http\UploadedFile;
|
||||
use function move_uploaded_file;
|
||||
|
||||
final readonly class SaveImageFile
|
||||
@@ -11,9 +11,10 @@ final readonly class SaveImageFile
|
||||
public function __invoke(Image $image, string $tempFileName): bool
|
||||
{
|
||||
$directory = $image->path;
|
||||
if (!is_dir($directory)) {
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
return move_uploaded_file($tempFileName, $image->path . $image->filename);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class MetaEntry
|
||||
|
||||
public function matchesRoute(string $route): bool
|
||||
{
|
||||
if (!$this->isRouteEntry()) {
|
||||
if (! $this->isRouteEntry()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ class MetaEntry
|
||||
|
||||
// Pattern-Matching (einfache Wildcard-Unterstützung)
|
||||
$pattern = str_replace(['*', '{', '}'], ['.*', '(?P<', '>[^/]+)'], $this->routePattern);
|
||||
|
||||
return (bool) preg_match('#^' . $pattern . '$#', $route);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ use App\Domain\Meta\Repository\MetaRepositoryInterface;
|
||||
use App\Domain\Meta\Service\MetaManager;
|
||||
use App\Domain\Meta\Service\MetaTemplateResolver;
|
||||
use App\Domain\Meta\ValueObject\MetaData;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\JsonResult;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Attributes\Route;
|
||||
use App\Framework\Router\Method;
|
||||
|
||||
final class MetaAdminController
|
||||
{
|
||||
@@ -21,7 +21,8 @@ final class MetaAdminController
|
||||
private readonly MetaRepositoryInterface $repository,
|
||||
private readonly MetaManager $metaManager,
|
||||
private readonly ?MetaTemplateResolver $templateResolver = null,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Listet alle Meta-Einträge auf
|
||||
@@ -46,18 +47,18 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => array_map(fn($entry) => $entry->toArray(), $entries),
|
||||
'data' => array_map(fn ($entry) => $entry->toArray(), $entries),
|
||||
'pagination' => [
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'total' => $total,
|
||||
'pages' => ceil($total / $limit),
|
||||
]
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -71,21 +72,21 @@ final class MetaAdminController
|
||||
try {
|
||||
$entry = $this->repository->findById($id);
|
||||
|
||||
if (!$entry) {
|
||||
if (! $entry) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Eintrag nicht gefunden'
|
||||
'error' => 'Meta-Eintrag nicht gefunden',
|
||||
], Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $entry->toArray()
|
||||
'data' => $entry->toArray(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -98,10 +99,10 @@ final class MetaAdminController
|
||||
{
|
||||
try {
|
||||
// Validierung: Entweder Route oder Entity muss angegeben sein
|
||||
if (!$request->routePattern && (!$request->entityType || !$request->entityId)) {
|
||||
if (! $request->routePattern && (! $request->entityType || ! $request->entityId)) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Entweder Route-Pattern oder Entity-Typ/ID muss angegeben werden'
|
||||
'error' => 'Entweder Route-Pattern oder Entity-Typ/ID muss angegeben werden',
|
||||
], Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ final class MetaAdminController
|
||||
if ($request->routePattern && $this->repository->routePatternExists($request->routePattern)) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Route-Pattern existiert bereits'
|
||||
'error' => 'Route-Pattern existiert bereits',
|
||||
], Status::CONFLICT);
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ final class MetaAdminController
|
||||
$this->repository->entityMetaExists($request->entityType, $request->entityId)) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Daten für diese Entity existieren bereits'
|
||||
'error' => 'Meta-Daten für diese Entity existieren bereits',
|
||||
], Status::CONFLICT);
|
||||
}
|
||||
|
||||
@@ -158,12 +159,12 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $savedEntry->toArray()
|
||||
'data' => $savedEntry->toArray(),
|
||||
], Status::CREATED);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -177,10 +178,10 @@ final class MetaAdminController
|
||||
try {
|
||||
$entry = $this->repository->findById($id);
|
||||
|
||||
if (!$entry) {
|
||||
if (! $entry) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Eintrag nicht gefunden'
|
||||
'error' => 'Meta-Eintrag nicht gefunden',
|
||||
], Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -189,7 +190,7 @@ final class MetaAdminController
|
||||
$this->repository->routePatternExists($request->routePattern, $id)) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Route-Pattern existiert bereits'
|
||||
'error' => 'Route-Pattern existiert bereits',
|
||||
], Status::CONFLICT);
|
||||
}
|
||||
|
||||
@@ -197,7 +198,7 @@ final class MetaAdminController
|
||||
$this->repository->entityMetaExists($request->entityType, $request->entityId, $id)) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Daten für diese Entity existieren bereits'
|
||||
'error' => 'Meta-Daten für diese Entity existieren bereits',
|
||||
], Status::CONFLICT);
|
||||
}
|
||||
|
||||
@@ -234,12 +235,12 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $savedEntry->toArray()
|
||||
'data' => $savedEntry->toArray(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -253,10 +254,10 @@ final class MetaAdminController
|
||||
try {
|
||||
$entry = $this->repository->findById($id);
|
||||
|
||||
if (!$entry) {
|
||||
if (! $entry) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Eintrag nicht gefunden'
|
||||
'error' => 'Meta-Eintrag nicht gefunden',
|
||||
], Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -268,18 +269,18 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'message' => 'Meta-Eintrag erfolgreich gelöscht'
|
||||
'message' => 'Meta-Eintrag erfolgreich gelöscht',
|
||||
]);
|
||||
} else {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Eintrag konnte nicht gelöscht werden'
|
||||
'error' => 'Meta-Eintrag konnte nicht gelöscht werden',
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -293,10 +294,10 @@ final class MetaAdminController
|
||||
try {
|
||||
$entry = $this->repository->findById($id);
|
||||
|
||||
if (!$entry) {
|
||||
if (! $entry) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => 'Meta-Eintrag nicht gefunden'
|
||||
'error' => 'Meta-Eintrag nicht gefunden',
|
||||
], Status::NOT_FOUND);
|
||||
}
|
||||
|
||||
@@ -313,12 +314,12 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $savedEntry->toArray()
|
||||
'data' => $savedEntry->toArray(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -332,10 +333,10 @@ final class MetaAdminController
|
||||
try {
|
||||
$template = $_POST['template'] ?? '';
|
||||
|
||||
if (!$this->templateResolver) {
|
||||
if (! $this->templateResolver) {
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'message' => 'Template-Validation nicht verfügbar'
|
||||
'message' => 'Template-Validation nicht verfügbar',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -347,13 +348,13 @@ final class MetaAdminController
|
||||
'data' => [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
'placeholders' => $placeholders
|
||||
]
|
||||
'placeholders' => $placeholders,
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -369,19 +370,19 @@ final class MetaAdminController
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'message' => 'Meta-Cache erfolgreich geleert'
|
||||
'message' => 'Meta-Cache erfolgreich geleert',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
], Status::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateTemplates(MetaRequest $request): void
|
||||
{
|
||||
if (!$this->templateResolver) {
|
||||
if (! $this->templateResolver) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -396,7 +397,7 @@ final class MetaAdminController
|
||||
foreach ($templates as $field => $template) {
|
||||
if ($template) {
|
||||
$errors = $this->templateResolver->validateTemplate($template);
|
||||
if (!empty($errors)) {
|
||||
if (! empty($errors)) {
|
||||
throw new \InvalidArgumentException("Template-Fehler in {$field}: " . implode(', ', $errors));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ final readonly class MetaMiddleware
|
||||
public function __construct(
|
||||
private MetaManager $metaManager,
|
||||
private ?RenderContext $renderContext = null,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Request $request, callable $next): Response
|
||||
{
|
||||
@@ -23,7 +24,7 @@ final readonly class MetaMiddleware
|
||||
$meta = $this->metaManager->resolveForRequest($request, $context);
|
||||
|
||||
// In RenderContext injizieren (falls verfügbar)
|
||||
if ($this->renderContext && !$meta->isEmpty()) {
|
||||
if ($this->renderContext && ! $meta->isEmpty()) {
|
||||
$this->renderContext->setMeta($meta);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Meta\Http\Request;
|
||||
|
||||
use App\Framework\Validation\Attributes\Required;
|
||||
use App\Framework\Validation\Attributes\Max;
|
||||
use App\Framework\Validation\Attributes\MaxLength;
|
||||
use App\Framework\Validation\Attributes\Min;
|
||||
use App\Framework\Validation\Attributes\Max;
|
||||
use App\Framework\Validation\Attributes\Required;
|
||||
use App\Framework\Validation\Attributes\Url;
|
||||
|
||||
final class MetaRequest
|
||||
|
||||
@@ -12,18 +12,21 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function findByRoute(string $route): ?MetaEntry
|
||||
{
|
||||
$entries = $this->findAllByRoute($route);
|
||||
|
||||
return $entries[0] ?? null;
|
||||
}
|
||||
|
||||
public function findByEntity(string $entityType, int $entityId): ?MetaEntry
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT * FROM meta_entries
|
||||
SELECT id, route_pattern, entity_type, entity_id, meta_data, priority, active, created_at, updated_at
|
||||
FROM meta_entries
|
||||
WHERE entity_type = ? AND entity_id = ? AND active = 1
|
||||
ORDER BY priority DESC
|
||||
LIMIT 1
|
||||
@@ -41,7 +44,8 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
public function findAllByRoute(string $route): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT * FROM meta_entries
|
||||
SELECT id, route_pattern, entity_type, entity_id, meta_data, priority, active, created_at, updated_at
|
||||
FROM meta_entries
|
||||
WHERE route_pattern IS NOT NULL
|
||||
AND active = 1
|
||||
ORDER BY priority DESC, LENGTH(route_pattern) DESC
|
||||
@@ -104,6 +108,7 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
]);
|
||||
|
||||
$entry->id = (int) $this->pdo->lastInsertId();
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
@@ -150,12 +155,16 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
{
|
||||
$stmt = $this->pdo->prepare('DELETE FROM meta_entries WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
public function findById(int $id): ?MetaEntry
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM meta_entries WHERE id = ?');
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT id, route_pattern, entity_type, entity_id, meta_data, priority, active, created_at, updated_at
|
||||
FROM meta_entries WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
@@ -168,7 +177,8 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
public function findAll(int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT * FROM meta_entries
|
||||
SELECT id, route_pattern, entity_type, entity_id, meta_data, priority, active, created_at, updated_at
|
||||
FROM meta_entries
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
');
|
||||
@@ -186,7 +196,8 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
public function search(string $query, int $limit = 50): array
|
||||
{
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT * FROM meta_entries
|
||||
SELECT id, route_pattern, entity_type, entity_id, meta_data, priority, active, created_at, updated_at
|
||||
FROM meta_entries
|
||||
WHERE route_pattern LIKE ?
|
||||
OR title LIKE ?
|
||||
OR description LIKE ?
|
||||
@@ -208,6 +219,7 @@ final readonly class DatabaseMetaRepository implements MetaRepositoryInterface
|
||||
public function count(): int
|
||||
{
|
||||
$stmt = $this->pdo->query('SELECT COUNT(*) FROM meta_entries');
|
||||
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Domain\Meta\Repository;
|
||||
|
||||
use App\Domain\Meta\Entity\MetaEntry;
|
||||
use App\Domain\Meta\ValueObject\MetaData;
|
||||
|
||||
interface MetaRepositoryInterface
|
||||
{
|
||||
|
||||
@@ -15,7 +15,8 @@ final readonly class MetaManager
|
||||
private MetaRepositoryInterface $repository,
|
||||
private ?CacheInterface $cache = null,
|
||||
private ?MetaTemplateResolver $templateResolver = null,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst Meta-Daten für einen Request auf
|
||||
@@ -43,7 +44,7 @@ final readonly class MetaManager
|
||||
}
|
||||
|
||||
// 3. Template-Verarbeitung
|
||||
if ($this->templateResolver && !empty($context)) {
|
||||
if ($this->templateResolver && ! empty($context)) {
|
||||
$meta = $this->templateResolver->resolve($meta, $context);
|
||||
}
|
||||
|
||||
@@ -66,7 +67,7 @@ final readonly class MetaManager
|
||||
$meta = $meta->merge($routeMeta->metaData);
|
||||
}
|
||||
|
||||
if ($this->templateResolver && !empty($context)) {
|
||||
if ($this->templateResolver && ! empty($context)) {
|
||||
$meta = $this->templateResolver->resolve($meta, $context);
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ final readonly class MetaManager
|
||||
$meta = $meta->merge($entityMeta->metaData);
|
||||
}
|
||||
|
||||
if ($this->templateResolver && !empty($context)) {
|
||||
if ($this->templateResolver && ! empty($context)) {
|
||||
$meta = $this->templateResolver->resolve($meta, $context);
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ final readonly class MetaManager
|
||||
*/
|
||||
public function invalidateCache(string $route): void
|
||||
{
|
||||
if (!$this->cache) {
|
||||
if (! $this->cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ final readonly class MetaManager
|
||||
*/
|
||||
public function invalidateAllCache(): void
|
||||
{
|
||||
if (!$this->cache) {
|
||||
if (! $this->cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,6 +151,7 @@ final readonly class MetaManager
|
||||
|
||||
if (count($parts) >= 2 && is_numeric($parts[1])) {
|
||||
$entityType = rtrim($parts[0], 's'); // Plural zu Singular
|
||||
|
||||
return $entityType;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ final readonly class MetaTemplateResolver
|
||||
}
|
||||
|
||||
// Unterstützt sowohl {variable} als auch {{variable}} Syntax
|
||||
$template = preg_replace_callback('/\{\{?(\w+(?:\.\w+)*)\}?\}/', function($matches) use ($context) {
|
||||
$template = preg_replace_callback('/\{\{?(\w+(?:\.\w+)*)\}?\}/', function ($matches) use ($context) {
|
||||
$key = $matches[1];
|
||||
$value = $this->getNestedValue($context, $key);
|
||||
|
||||
@@ -43,7 +43,7 @@ final readonly class MetaTemplateResolver
|
||||
}, $template);
|
||||
|
||||
// Unterstützt einfache Funktionen: {upper:variable}, {lower:variable}
|
||||
$template = preg_replace_callback('/\{(\w+):(\w+(?:\.\w+)*)\}/', function($matches) use ($context) {
|
||||
$template = preg_replace_callback('/\{(\w+):(\w+(?:\.\w+)*)\}/', function ($matches) use ($context) {
|
||||
$function = $matches[1];
|
||||
$key = $matches[2];
|
||||
$value = $this->getNestedValue($context, $key);
|
||||
@@ -127,7 +127,7 @@ final readonly class MetaTemplateResolver
|
||||
preg_match_all('/\{([^}]+)\}/', $template, $matches);
|
||||
|
||||
foreach ($matches[1] as $placeholder) {
|
||||
if (!preg_match('/^(\w+:)?[\w.]+$/', $placeholder)) {
|
||||
if (! preg_match('/^(\w+:)?[\w.]+$/', $placeholder)) {
|
||||
$errors[] = "Ungültige Template-Syntax: {{$placeholder}}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ readonly class MetaData
|
||||
public ?string $twitterSite = null,
|
||||
public ?string $canonical = null,
|
||||
public ?array $customMeta = null,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
@@ -68,7 +69,7 @@ readonly class MetaData
|
||||
'twitter_site' => $this->twitterSite,
|
||||
'canonical' => $this->canonical,
|
||||
'custom_meta' => $this->customMeta ? json_encode($this->customMeta) : null,
|
||||
], fn($value) => $value !== null);
|
||||
], fn ($value) => $value !== null);
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class QrCodeException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\Position;
|
||||
|
||||
class AlignmentPattern implements PatternInterface
|
||||
{
|
||||
// Tabelle der Ausrichtungsmuster-Positionen für Versionen 1-40
|
||||
private const POSITIONS = [
|
||||
1 => [],
|
||||
2 => [6, 18],
|
||||
3 => [6, 22],
|
||||
4 => [6, 26],
|
||||
5 => [6, 30],
|
||||
6 => [6, 34],
|
||||
7 => [6, 22, 38],
|
||||
8 => [6, 24, 42],
|
||||
9 => [6, 26, 46],
|
||||
10 => [6, 28, 50],
|
||||
// ... weitere Versionen hier hinzufügen
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly int $version
|
||||
) {
|
||||
}
|
||||
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
if ($this->version === 1) {
|
||||
return; // Version 1 hat keine Ausrichtungsmuster
|
||||
}
|
||||
|
||||
$positions = self::POSITIONS[$this->version] ?? $this->calculatePositions($size);
|
||||
|
||||
foreach ($positions as $centerRow) {
|
||||
foreach ($positions as $centerCol) {
|
||||
if ($this->overlapsWithFinderPattern($centerRow, $centerCol, $size)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->applyPattern($matrix, $centerRow, $centerCol, $size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function applyPattern(array &$matrix, int $centerRow, int $centerCol, int $size): void
|
||||
{
|
||||
// 5x5 Ausrichtungsmuster
|
||||
for ($r = -2; $r <= 2; $r++) {
|
||||
for ($c = -2; $c <= 2; $c++) {
|
||||
$position = new Position($centerRow + $r, $centerCol + $c);
|
||||
|
||||
if ($position->isWithinBounds($size)) {
|
||||
// Äußerer Rand oder Mittelpunkt
|
||||
$isBlack = abs($r) === 2 || abs($c) === 2 || ($r === 0 && $c === 0);
|
||||
$matrix[$centerRow + $r][$centerCol + $c] = $isBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function overlapsWithFinderPattern(int $row, int $col, int $size): bool
|
||||
{
|
||||
return ($row <= 8 && $col <= 8) ||
|
||||
($row <= 8 && $col >= $size - 9) ||
|
||||
($row >= $size - 9 && $col <= 8);
|
||||
}
|
||||
|
||||
private function calculatePositions(int $size): array
|
||||
{
|
||||
// Vereinfachte Berechnung für fehlende Versionen
|
||||
$positions = [6];
|
||||
|
||||
if ($size >= 25) { // Ab Version 2
|
||||
$positions[] = $size - 7;
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
/**
|
||||
* Stellt die Positionstabelle für die Ausrichtungsmuster (Alignment Patterns) zur Verfügung
|
||||
*/
|
||||
class AlignmentPatternTable
|
||||
{
|
||||
/**
|
||||
* Tabelle der Alignment Pattern Positionen für alle QR-Code-Versionen
|
||||
* Werte entsprechen den Mittelpunkten der Alignment Pattern im QR-Code
|
||||
*/
|
||||
public const POSITIONS = [
|
||||
1 => [], // Version 1 hat keine Alignment Patterns
|
||||
2 => [6, 18],
|
||||
3 => [6, 22],
|
||||
4 => [6, 26],
|
||||
5 => [6, 30],
|
||||
6 => [6, 34],
|
||||
7 => [6, 22, 38],
|
||||
8 => [6, 24, 42],
|
||||
9 => [6, 26, 46],
|
||||
10 => [6, 28, 50],
|
||||
11 => [6, 30, 54],
|
||||
12 => [6, 32, 58],
|
||||
13 => [6, 34, 62],
|
||||
14 => [6, 26, 46, 66],
|
||||
15 => [6, 26, 48, 70],
|
||||
16 => [6, 26, 50, 74],
|
||||
17 => [6, 30, 54, 78],
|
||||
18 => [6, 30, 56, 82],
|
||||
19 => [6, 30, 58, 86],
|
||||
20 => [6, 34, 62, 90],
|
||||
21 => [6, 28, 50, 72, 94],
|
||||
22 => [6, 26, 50, 74, 98],
|
||||
23 => [6, 30, 54, 78, 102],
|
||||
24 => [6, 28, 54, 80, 106],
|
||||
25 => [6, 32, 58, 84, 110],
|
||||
26 => [6, 30, 58, 86, 114],
|
||||
27 => [6, 34, 62, 90, 118],
|
||||
28 => [6, 26, 50, 74, 98, 122],
|
||||
29 => [6, 30, 54, 78, 102, 126],
|
||||
30 => [6, 26, 52, 78, 104, 130],
|
||||
31 => [6, 30, 56, 82, 108, 134],
|
||||
32 => [6, 34, 60, 86, 112, 138],
|
||||
33 => [6, 30, 58, 86, 114, 142],
|
||||
34 => [6, 34, 62, 90, 118, 146],
|
||||
35 => [6, 30, 54, 78, 102, 126, 150],
|
||||
36 => [6, 24, 50, 76, 102, 128, 154],
|
||||
37 => [6, 28, 54, 80, 106, 132, 158],
|
||||
38 => [6, 32, 58, 84, 110, 136, 162],
|
||||
39 => [6, 26, 54, 82, 110, 138, 166],
|
||||
40 => [6, 30, 58, 86, 114, 142, 170],
|
||||
];
|
||||
|
||||
/**
|
||||
* Gibt die Alignment Pattern Positionen für die angegebene QR-Code-Version zurück
|
||||
*
|
||||
* @param int $version QR-Code-Version (1-40)
|
||||
* @return array Array mit den Positionen der Alignment Patterns
|
||||
*/
|
||||
public static function getPositions(int $version): array
|
||||
{
|
||||
if ($version < 1 || $version > 40) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::POSITIONS[$version];
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
class DarkModulePattern implements PatternInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly int $version
|
||||
) {
|
||||
}
|
||||
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
// Für Version 1 muss das Dark Module bei (8, 13) sein
|
||||
if ($this->version === 1) {
|
||||
$matrix[8][13] = true;
|
||||
} else {
|
||||
// Für andere Versionen: Position (4V+9, 8)
|
||||
$matrix[(4 * $this->version) + 9][8] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\Position;
|
||||
|
||||
class FinderPattern implements PatternInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Position $position
|
||||
) {
|
||||
}
|
||||
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
$row = $this->position->getRow();
|
||||
$col = $this->position->getColumn();
|
||||
|
||||
// 7x7 Finder Pattern
|
||||
for ($i = 0; $i < 7; $i++) {
|
||||
for ($j = 0; $j < 7; $j++) {
|
||||
// Äußerer Rand oder inneres Quadrat
|
||||
$isBlack = ($i === 0 || $i === 6 || $j === 0 || $j === 6) ||
|
||||
($i >= 2 && $i <= 4 && $j >= 2 && $j <= 4);
|
||||
|
||||
$position = new Position($row + $i, $col + $j);
|
||||
if ($position->isWithinBounds($size)) {
|
||||
$matrix[$row + $i][$col + $j] = $isBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weißer Separator um das Finder Pattern
|
||||
$this->applySeparator($matrix, $size);
|
||||
}
|
||||
|
||||
private function applySeparator(array &$matrix, int $size): void
|
||||
{
|
||||
$row = $this->position->getRow();
|
||||
$col = $this->position->getColumn();
|
||||
|
||||
// Horizontaler Separator unten
|
||||
for ($j = 0; $j < 8; $j++) {
|
||||
$position = new Position($row + 7, $col + $j);
|
||||
if ($position->isWithinBounds($size)) {
|
||||
$matrix[$row + 7][$col + $j] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Vertikaler Separator rechts
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$position = new Position($row + $i, $col + 7);
|
||||
if ($position->isWithinBounds($size)) {
|
||||
$matrix[$row + $i][$col + 7] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function createAll(int $size): array
|
||||
{
|
||||
return [
|
||||
new self(new Position(0, 0)), // Oben links
|
||||
new self(new Position(0, $size - 7)), // Oben rechts
|
||||
new self(new Position($size - 7, 0)) // Unten links
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
|
||||
class FormatInfoPattern implements PatternInterface
|
||||
{
|
||||
// Format-Strings für verschiedene Error Correction Levels und Masken
|
||||
private const FORMAT_STRINGS = [
|
||||
'L' => [
|
||||
0 => '111011111000100',
|
||||
1 => '111001011110011',
|
||||
2 => '111110110101010',
|
||||
3 => '111100010011101',
|
||||
4 => '110011000101111',
|
||||
5 => '110001100011000',
|
||||
6 => '110110001000001',
|
||||
7 => '110100101110110',
|
||||
],
|
||||
'M' => [
|
||||
0 => '101010000010010',
|
||||
1 => '101000100100101',
|
||||
2 => '101111001111100',
|
||||
3 => '101101101001011',
|
||||
4 => '100010111111001',
|
||||
5 => '100000011001110',
|
||||
6 => '100111110010111',
|
||||
7 => '100101010100000',
|
||||
],
|
||||
'Q' => [
|
||||
0 => '011010101011111',
|
||||
1 => '011000001101000',
|
||||
2 => '011111100110001',
|
||||
3 => '011101000000110',
|
||||
4 => '010010010110100',
|
||||
5 => '010000110000011',
|
||||
6 => '010111011011010',
|
||||
7 => '010101111101101',
|
||||
],
|
||||
'H' => [
|
||||
0 => '001011010001001',
|
||||
1 => '001001110111110',
|
||||
2 => '001110011100111',
|
||||
3 => '001100111010000',
|
||||
4 => '000011101100010',
|
||||
5 => '000001001010101',
|
||||
6 => '000110100001100',
|
||||
7 => '000100000111011',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ErrorCorrectionLevel $level,
|
||||
private readonly int $maskPattern
|
||||
) {
|
||||
}
|
||||
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
$formatString = self::FORMAT_STRINGS[$this->level->name][$this->maskPattern];
|
||||
|
||||
// Format-Info um das obere linke Finder-Pattern
|
||||
for ($i = 0; $i < 15; $i++) {
|
||||
$bit = $formatString[$i] === '1';
|
||||
|
||||
// Format-Info horizontal
|
||||
if ($i < 6) {
|
||||
$matrix[8][$i] = $bit;
|
||||
} elseif ($i < 8) {
|
||||
$matrix[8][$i + 1] = $bit;
|
||||
} elseif ($i === 8) {
|
||||
$matrix[7][8] = $bit;
|
||||
} else {
|
||||
$matrix[14 - $i][8] = $bit;
|
||||
}
|
||||
|
||||
// Format-Info vertikal
|
||||
if ($i < 8) {
|
||||
$matrix[$size - 1 - $i][8] = $bit;
|
||||
} else {
|
||||
$matrix[8][$size - 15 + $i] = $bit;
|
||||
}
|
||||
}
|
||||
|
||||
// Für Version 1 muss das Dark Module bei (8, 13) sein
|
||||
// Dies ist für QR-Code-Scanner wichtig zur Orientierung
|
||||
$matrix[8][13] = true;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
/**
|
||||
* Stellt die Format-Information für QR-Codes bereit
|
||||
*/
|
||||
class FormatInfoTable
|
||||
{
|
||||
/**
|
||||
* Format-Information BCH-codiert (15 Bits)
|
||||
* Schlüssel: [EC-Level][Maskenmuster] = 15-Bit BCH-codierte Formatinformation
|
||||
*/
|
||||
public const FORMAT_INFO = [
|
||||
'L' => [
|
||||
0 => 0b111011111000100,
|
||||
1 => 0b111001011110011,
|
||||
2 => 0b111110110101010,
|
||||
3 => 0b111100010011101,
|
||||
4 => 0b110011000101111,
|
||||
5 => 0b110001100011000,
|
||||
6 => 0b110110001000001,
|
||||
7 => 0b110100101110110,
|
||||
],
|
||||
'M' => [
|
||||
0 => 0b101010000010010,
|
||||
1 => 0b101000100100101,
|
||||
2 => 0b101111001111100,
|
||||
3 => 0b101101101001011,
|
||||
4 => 0b100010111111001,
|
||||
5 => 0b100000011001110,
|
||||
6 => 0b100111110010111,
|
||||
7 => 0b100101010100000,
|
||||
],
|
||||
'Q' => [
|
||||
0 => 0b011010101011111,
|
||||
1 => 0b011000001101000,
|
||||
2 => 0b011111100110001,
|
||||
3 => 0b011101000000110,
|
||||
4 => 0b010010010110100,
|
||||
5 => 0b010000110000011,
|
||||
6 => 0b010111011011010,
|
||||
7 => 0b010101111101101,
|
||||
],
|
||||
'H' => [
|
||||
0 => 0b001011010001001,
|
||||
1 => 0b001001110111110,
|
||||
2 => 0b001110011100111,
|
||||
3 => 0b001100111010000,
|
||||
4 => 0b000011101100010,
|
||||
5 => 0b000001001010101,
|
||||
6 => 0b000110100001100,
|
||||
7 => 0b000100000111011,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Liefert die Formatinformation für das angegebene Error-Correction-Level und Maskenmuster
|
||||
*
|
||||
* @param string $ecLevel Error Correction Level (L, M, Q oder H)
|
||||
* @param int $maskPattern Maskenmuster (0-7)
|
||||
* @return int 15-Bit-Formatinformation
|
||||
*/
|
||||
public static function getFormatInfo(string $ecLevel, int $maskPattern): int
|
||||
{
|
||||
if (!isset(self::FORMAT_INFO[$ecLevel]) || !isset(self::FORMAT_INFO[$ecLevel][$maskPattern])) {
|
||||
// Fallback: L, Maske 0
|
||||
return self::FORMAT_INFO['L'][0];
|
||||
}
|
||||
|
||||
return self::FORMAT_INFO[$ecLevel][$maskPattern];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
interface PatternInterface
|
||||
{
|
||||
/**
|
||||
* Wendet das Muster auf die übergebene Matrix an
|
||||
*/
|
||||
public function apply(array &$matrix, int $size): void;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\Position;
|
||||
|
||||
class TimingPattern implements PatternInterface
|
||||
{
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
// Horizontales Timing Pattern
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$matrix[6][$i] = ($i % 2 === 0);
|
||||
}
|
||||
|
||||
// Vertikales Timing Pattern
|
||||
for ($i = 0; $i < $size; $i++) {
|
||||
$matrix[$i][6] = ($i % 2 === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Pattern;
|
||||
|
||||
/**
|
||||
* Implementiert das Version Information Pattern für QR-Codes ab Version 7
|
||||
*/
|
||||
class VersionInfoPattern implements PatternInterface
|
||||
{
|
||||
/**
|
||||
* Lookup-Tabelle für die Version-Information (BCH-kodiert)
|
||||
* Diese 18-Bit-Werte enthalten die Versionsinformation mit Fehlerkorrektur
|
||||
*/
|
||||
private const VERSION_INFO = [
|
||||
7 => 0b000111110010010100,
|
||||
8 => 0b001000010110111100,
|
||||
9 => 0b001001101010011001,
|
||||
10 => 0b001010010011010011,
|
||||
11 => 0b001011101111110110,
|
||||
12 => 0b001100011101100010,
|
||||
13 => 0b001101100001000111,
|
||||
14 => 0b001110011000001101,
|
||||
15 => 0b001111100100101000,
|
||||
16 => 0b010000101101111000,
|
||||
17 => 0b010001010001011101,
|
||||
18 => 0b010010101000010111,
|
||||
19 => 0b010011010100110010,
|
||||
20 => 0b010100100110100110,
|
||||
21 => 0b010101011010000011,
|
||||
22 => 0b010110100011001001,
|
||||
23 => 0b010111011111101100,
|
||||
24 => 0b011000111011000100,
|
||||
25 => 0b011001000111100001,
|
||||
26 => 0b011010111110101011,
|
||||
27 => 0b011011000010001110,
|
||||
28 => 0b011100110000011010,
|
||||
29 => 0b011101001100111111,
|
||||
30 => 0b011110110101110101,
|
||||
31 => 0b011111001001010000,
|
||||
32 => 0b100000100111010101,
|
||||
33 => 0b100001011011110000,
|
||||
34 => 0b100010100010111010,
|
||||
35 => 0b100011011110011111,
|
||||
36 => 0b100100101100001011,
|
||||
37 => 0b100101010000101110,
|
||||
38 => 0b100110101001100100,
|
||||
39 => 0b100111010101000001,
|
||||
40 => 0b101000110001101001,
|
||||
];
|
||||
|
||||
private int $version;
|
||||
|
||||
public function __construct(int $version)
|
||||
{
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
public function apply(array &$matrix, int $size): void
|
||||
{
|
||||
// Version Information Pattern nur für QR-Codes ab Version 7
|
||||
if ($this->version < 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
$versionInfo = self::VERSION_INFO[$this->version] ?? 0;
|
||||
|
||||
// Versionsinformation an zwei Positionen einfügen
|
||||
$this->placeVersionInfo($matrix, $versionInfo); // Platziere oben rechts
|
||||
$this->placeVersionInfoTransposed($matrix, $versionInfo); // Platziere unten links
|
||||
}
|
||||
|
||||
/**
|
||||
* Platziert die Versionsinformation unterhalb des Finder Patterns oben rechts
|
||||
*/
|
||||
private function placeVersionInfo(array &$matrix, int $versionInfo): void
|
||||
{
|
||||
// Position: unterhalb des rechten oberen Finder Patterns
|
||||
// 3 Zeilen x 6 Spalten = 18 Bits
|
||||
$row = 0;
|
||||
$col = $this->version * 4 + 6; // Rechts oben, die exakte Position hängt von der Version ab
|
||||
|
||||
// 18 Bits des Versionsinformationsblocks setzen
|
||||
for ($i = 0; $i < 18; $i++) {
|
||||
// Bit von rechts nach links extrahieren (LSB zuerst)
|
||||
$bit = ($versionInfo >> $i) & 1;
|
||||
|
||||
// Berechne Position: 6 Bits pro Zeile, 3 Zeilen
|
||||
$r = $row + ($i / 3);
|
||||
$c = $col - ($i % 3);
|
||||
|
||||
$matrix[(int)$r][(int)$c] = $bit === 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platziert die Versionsinformation rechts vom Finder Pattern unten links
|
||||
*/
|
||||
private function placeVersionInfoTransposed(array &$matrix, int $versionInfo): void
|
||||
{
|
||||
// Position: rechts vom linken unteren Finder Pattern
|
||||
// 6 Zeilen x 3 Spalten = 18 Bits
|
||||
$row = $this->version * 4 + 6; // Unten links, die exakte Position hängt von der Version ab
|
||||
$col = 0;
|
||||
|
||||
// 18 Bits des Versionsinformationsblocks setzen
|
||||
for ($i = 0; $i < 18; $i++) {
|
||||
// Bit von rechts nach links extrahieren (LSB zuerst)
|
||||
$bit = ($versionInfo >> $i) & 1;
|
||||
|
||||
// Berechne Position: 3 Bits pro Zeile, 6 Zeilen (transponiert)
|
||||
$r = $row - ($i % 3);
|
||||
$c = $col + ($i / 3);
|
||||
|
||||
$matrix[(int)$r][(int)$c] = $bit === 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode;
|
||||
|
||||
use App\Domain\QrCode\Exception\QrCodeException;
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeVersion;
|
||||
|
||||
readonly class QrCode
|
||||
{
|
||||
public function __construct(
|
||||
private string $data,
|
||||
private ErrorCorrectionLevel $errorCorrectionLevel = ErrorCorrectionLevel::M,
|
||||
private ?QrCodeVersion $version = null
|
||||
) {
|
||||
if (empty($data)) {
|
||||
throw new QrCodeException('QR Code Daten dürfen nicht leer sein');
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(): string
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function getErrorCorrectionLevel(): ErrorCorrectionLevel
|
||||
{
|
||||
return $this->errorCorrectionLevel;
|
||||
}
|
||||
|
||||
public function getVersion(): ?QrCodeVersion
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function withErrorCorrectionLevel(ErrorCorrectionLevel $level): self
|
||||
{
|
||||
return new self($this->data, $level, $this->version);
|
||||
}
|
||||
|
||||
public function withVersion(QrCodeVersion $version): self
|
||||
{
|
||||
return new self($this->data, $this->errorCorrectionLevel, $version);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
# QR-Code Modul
|
||||
|
||||
Dieses Modul bietet eine einfache API zum Generieren von QR-Codes in verschiedenen Formaten wie SVG, PNG und ASCII.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Generierung von QR-Codes in Versionen 1-40
|
||||
- Unterstützung verschiedener Fehlerkorrektur-Level (L, M, Q, H)
|
||||
- Automatische Versionsauswahl basierend auf Datenmenge
|
||||
- Vollständige Reed-Solomon-Fehlerkorrektur-Implementierung
|
||||
- Export als SVG, PNG oder ASCII-Art
|
||||
- Anpassbare Modulgröße und Ränder
|
||||
- Anpassbare Farben für SVG-Output
|
||||
|
||||
## Beispiele
|
||||
|
||||
### Einfache Verwendung
|
||||
|
||||
```php
|
||||
// QR-Code als SVG generieren
|
||||
$qrCodeService = new QrCodeService($qrCodeGenerator);
|
||||
$svg = $qrCodeService->generateSvg('https://example.com');
|
||||
|
||||
// QR-Code als PNG mit benutzerdefinierten Einstellungen
|
||||
$config = new QrCodeConfig(6, 8, '#000000', '#FFFFFF');
|
||||
$png = $qrCodeService->generatePng('Hallo Welt!', ErrorCorrectionLevel::H, $config);
|
||||
```
|
||||
|
||||
### Erweiterte Anpassung
|
||||
|
||||
```php
|
||||
// Explizite QR-Code-Version angeben
|
||||
$qrCode = new QrCode('Daten', ErrorCorrectionLevel::Q, new QrCodeVersion(5));
|
||||
$matrix = $qrCodeGenerator->generate($qrCode);
|
||||
|
||||
// QR-Code als ASCII-Art ausgeben (für Debugging)
|
||||
$ascii = $qrCodeService->generateAscii('Text');
|
||||
echo $ascii;
|
||||
```
|
||||
|
||||
## Architektur
|
||||
|
||||
Das Modul ist nach Domain-Driven Design strukturiert:
|
||||
|
||||
- **Entities und Value Objects**: QrCode, QrCodeVersion, ErrorCorrectionLevel, GaloisField, Polynomial, etc.
|
||||
- **Services**: QrCodeEncoder, QrCodeRenderer, QrCodeGenerator, ReedSolomon, ReedSolomonEncoder
|
||||
- **Patterns**: FinderPattern, AlignmentPattern, etc.
|
||||
|
||||
Die Trennung von Verantwortlichkeiten ermöglicht eine einfache Erweiterung und Wartung.
|
||||
|
||||
## Reed-Solomon-Fehlerkorrektur
|
||||
|
||||
Die Implementierung der Reed-Solomon-Fehlerkorrektur erfolgt in mehreren Schritten:
|
||||
|
||||
1. **Galois-Feld-Arithmetik**: Die `GaloisField`-Klasse implementiert die mathematischen Operationen im Galois-Feld GF(2^8).
|
||||
|
||||
2. **Polynom-Operationen**: Die `Polynomial`-Klasse ermöglicht Berechnungen mit Polynomen im Galois-Feld.
|
||||
|
||||
3. **Generator-Polynom**: Für die Fehlerkorrektur wird ein spezifisches Generator-Polynom basierend auf der gewünschten Anzahl von Fehlerkorrektur-Bytes erzeugt.
|
||||
|
||||
4. **Kodierung**: Der eigentliche Reed-Solomon-Algorithmus teilt die Daten in Blöcke ein, berechnet die Fehlerkorrektur-Bytes für jeden Block und interleaved die Ergebnisse.
|
||||
|
||||
Die Implementierung folgt dem Standard-Verfahren für QR-Codes und kann Fehler bis zur Hälfte der Anzahl der Fehlerkorrektur-Bytes korrigieren.
|
||||
|
||||
## Kodierungsmodi
|
||||
|
||||
Die Implementierung unterstützt folgende Kodierungsmodi gemäß der QR-Code-Spezifikation:
|
||||
|
||||
1. **Numerischer Modus**: Optimiert für Ziffern (0-9), packt jeweils 3 Ziffern in 10 Bits.
|
||||
2. **Alphanumerischer Modus**: Für Ziffern, Großbuchstaben und einige Sonderzeichen, packt jeweils 2 Zeichen in 11 Bits.
|
||||
3. **Byte-Modus**: Für beliebige 8-Bit-Daten, verwendet 8 Bits pro Zeichen.
|
||||
4. **Kanji-Modus**: Optimiert für japanische Schriftzeichen (vereinfachte Implementierung).
|
||||
|
||||
Der Encoder wählt automatisch den effizientesten Modus basierend auf den Eingabedaten.
|
||||
|
||||
## Limitationen
|
||||
|
||||
- Kanji-Modus ist nur teilweise implementiert
|
||||
- Strukturierte Anhänge werden nicht unterstützt
|
||||
- Keine Unterstützung für Micro-QR-Codes
|
||||
@@ -1,299 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\Exception\QrCodeException;
|
||||
use App\Domain\QrCode\QrCode;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeVersion;
|
||||
|
||||
class QrCodeEncoder
|
||||
{
|
||||
// Modus-Indikatoren (4 Bit)
|
||||
private const MODE_NUMERIC = '0001';
|
||||
private const MODE_ALPHANUMERIC = '0010';
|
||||
private const MODE_BYTE = '0100';
|
||||
private const MODE_KANJI = '1000';
|
||||
|
||||
// Zeichensatz für alphanumerische Kodierung
|
||||
private const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
||||
|
||||
// Character Count Indicator Längen basierend auf QR-Version und Modus
|
||||
private const CHARACTER_COUNT_BITS = [
|
||||
// Modus => [Versionsbereich => Bits]
|
||||
'numeric' => [
|
||||
[1, 9, 10], // Versionen 1-9: 10 Bits
|
||||
[10, 26, 12], // Versionen 10-26: 12 Bits
|
||||
[27, 40, 14], // Versionen 27-40: 14 Bits
|
||||
],
|
||||
'alphanumeric' => [
|
||||
[1, 9, 9], // Versionen 1-9: 9 Bits
|
||||
[10, 26, 11], // Versionen 10-26: 11 Bits
|
||||
[27, 40, 13], // Versionen 27-40: 13 Bits
|
||||
],
|
||||
'byte' => [
|
||||
[1, 9, 8], // Versionen 1-9: 8 Bits
|
||||
[10, 26, 16], // Versionen 10-26: 16 Bits
|
||||
[27, 40, 16], // Versionen 27-40: 16 Bits
|
||||
],
|
||||
'kanji' => [
|
||||
[1, 9, 8], // Versionen 1-9: 8 Bits
|
||||
[10, 26, 10], // Versionen 10-26: 10 Bits
|
||||
[27, 40, 12], // Versionen 27-40: 12 Bits
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Kodiert die Daten in ein QR-Code-Bitstream
|
||||
*
|
||||
* @param QrCode $qrCode QR-Code mit zu kodierenden Daten
|
||||
* @param QrCodeVersion $version Zu verwendende QR-Code-Version
|
||||
* @return string Binärer String ('0' und '1')
|
||||
* @throws QrCodeException Bei Kodierungsproblemen
|
||||
*/
|
||||
public function encode(QrCode $qrCode, QrCodeVersion $version): string
|
||||
{
|
||||
$data = $qrCode->getData();
|
||||
|
||||
// 1. Bestimme den optimalen Modus
|
||||
$mode = $this->determineEncodingMode($data);
|
||||
|
||||
// 2. Füge Mode Indicator hinzu (4 Bits)
|
||||
$encoded = $this->getModeIndicator($mode);
|
||||
|
||||
// 3. Füge Character Count Indicator hinzu
|
||||
$encoded .= $this->getCharacterCountIndicator(strlen($data), $mode, $version);
|
||||
|
||||
// 4. Kodiere die eigentlichen Daten
|
||||
$encoded .= $this->encodeData($data, $mode);
|
||||
|
||||
// 5. Füge Terminator hinzu und fülle auf Byte-Grenze auf
|
||||
$encoded = $this->addTerminator($encoded, $version, $qrCode->getErrorCorrectionLevel());
|
||||
$encoded = $this->padToByteBoundary($encoded);
|
||||
|
||||
// 6. Fülle mit Pad-Bytes auf die volle Kapazität
|
||||
$encoded = $this->addPadBytes($encoded, $version, $qrCode->getErrorCorrectionLevel());
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den optimalen Kodierungsmodus für die Daten
|
||||
*/
|
||||
private function determineEncodingMode(string $data): string
|
||||
{
|
||||
// Prüfe, ob die Daten nur Ziffern enthalten
|
||||
if (preg_match('/^[0-9]+$/', $data)) {
|
||||
return 'numeric';
|
||||
}
|
||||
|
||||
// Prüfe, ob die Daten nur alphanumerische Zeichen enthalten
|
||||
if (preg_match('/^[0-9A-Z $%*+\-.\/:]+$/', $data)) {
|
||||
return 'alphanumeric';
|
||||
}
|
||||
|
||||
// Kanji-Erkennung ist komplex und wird hier nicht implementiert
|
||||
// Bei Bedarf könnte hier eine Kanji-Zeichenerkennung erfolgen
|
||||
|
||||
// Standardmodus: Byte-Modus
|
||||
return 'byte';
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Mode Indicator für den angegebenen Modus
|
||||
*/
|
||||
private function getModeIndicator(string $mode): string
|
||||
{
|
||||
return match ($mode) {
|
||||
'numeric' => self::MODE_NUMERIC,
|
||||
'alphanumeric' => self::MODE_ALPHANUMERIC,
|
||||
'kanji' => self::MODE_KANJI,
|
||||
default => self::MODE_BYTE,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt den Character Count Indicator
|
||||
*/
|
||||
private function getCharacterCountIndicator(int $length, string $mode, QrCodeVersion $version): string
|
||||
{
|
||||
$versionNumber = $version->getValue();
|
||||
$bits = 0;
|
||||
|
||||
// Bestimme die Anzahl der Bits für den Character Count Indicator
|
||||
foreach (self::CHARACTER_COUNT_BITS[$mode] as $range) {
|
||||
if ($versionNumber >= $range[0] && $versionNumber <= $range[1]) {
|
||||
$bits = $range[2];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Konvertiere die Länge in binäre Darstellung mit der richtigen Anzahl von Bits
|
||||
return str_pad(decbin($length), $bits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert die Daten im angegebenen Modus
|
||||
*/
|
||||
private function encodeData(string $data, string $mode): string
|
||||
{
|
||||
return match ($mode) {
|
||||
'numeric' => $this->encodeNumeric($data),
|
||||
'alphanumeric' => $this->encodeAlphanumeric($data),
|
||||
'kanji' => $this->encodeKanji($data),
|
||||
default => $this->encodeByte($data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert Daten im numerischen Modus
|
||||
* Im numerischen Modus werden jeweils 3 Ziffern zu 10 Bits gruppiert
|
||||
*/
|
||||
private function encodeNumeric(string $data): string
|
||||
{
|
||||
$result = '';
|
||||
$length = strlen($data);
|
||||
|
||||
for ($i = 0; $i < $length; $i += 3) {
|
||||
$chunk = substr($data, $i, min(3, $length - $i));
|
||||
$value = (int)$chunk;
|
||||
|
||||
// Bestimme die Anzahl der Bits basierend auf der Chunk-Länge
|
||||
$numBits = match (strlen($chunk)) {
|
||||
1 => 4,
|
||||
2 => 7,
|
||||
3 => 10,
|
||||
};
|
||||
|
||||
// Konvertiere den Wert in binäre Darstellung
|
||||
$result .= str_pad(decbin($value), $numBits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert Daten im alphanumerischen Modus
|
||||
* Im alphanumerischen Modus werden jeweils 2 Zeichen zu 11 Bits gruppiert
|
||||
*/
|
||||
private function encodeAlphanumeric(string $data): string
|
||||
{
|
||||
$result = '';
|
||||
$length = strlen($data);
|
||||
|
||||
for ($i = 0; $i < $length; $i += 2) {
|
||||
if ($i + 1 < $length) {
|
||||
// Zwei Zeichen verarbeiten
|
||||
$char1 = strpos(self::ALPHANUMERIC_CHARS, $data[$i]);
|
||||
$char2 = strpos(self::ALPHANUMERIC_CHARS, $data[$i + 1]);
|
||||
|
||||
if ($char1 === false || $char2 === false) {
|
||||
throw new QrCodeException("Ungültiges alphanumerisches Zeichen: {$data[$i]} oder {$data[$i+1]}");
|
||||
}
|
||||
|
||||
$value = $char1 * 45 + $char2;
|
||||
$result .= str_pad(decbin($value), 11, '0', STR_PAD_LEFT);
|
||||
} else {
|
||||
// Einzelnes letztes Zeichen
|
||||
$char = strpos(self::ALPHANUMERIC_CHARS, $data[$i]);
|
||||
|
||||
if ($char === false) {
|
||||
throw new QrCodeException("Ungültiges alphanumerisches Zeichen: {$data[$i]}");
|
||||
}
|
||||
|
||||
$result .= str_pad(decbin($char), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert Daten im Byte-Modus
|
||||
* Im Byte-Modus wird jedes Zeichen als 8-Bit-Wert kodiert
|
||||
*/
|
||||
private function encodeByte(string $data): string
|
||||
{
|
||||
$result = '';
|
||||
$length = strlen($data);
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
// Konvertiere das Zeichen in seinen ASCII-Wert
|
||||
$value = ord($data[$i]);
|
||||
$result .= str_pad(decbin($value), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert Daten im Kanji-Modus
|
||||
* Dieser Modus ist für japanische Schriftzeichen optimiert
|
||||
* In dieser vereinfachten Implementierung wird er nicht vollständig unterstützt
|
||||
*/
|
||||
private function encodeKanji(string $data): string
|
||||
{
|
||||
// Vereinfachte Implementierung - in einer realen Anwendung würde hier
|
||||
// eine Konvertierung von Kanji-Zeichen in die entsprechenden Werte erfolgen
|
||||
return $this->encodeByte($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt den Terminator (0000) hinzu und füllt auf bis zur maximalen Kapazität
|
||||
*/
|
||||
private function addTerminator(string $encoded, QrCodeVersion $version, $errorCorrectionLevel): string
|
||||
{
|
||||
$dataCapacityBits = $version->getDataCapacity($errorCorrectionLevel) * 8;
|
||||
$currentLength = strlen($encoded);
|
||||
|
||||
// Berechne, wie viele Terminator-Bits hinzugefügt werden können
|
||||
$terminatorLength = max(0, min(4, $dataCapacityBits - $currentLength));
|
||||
|
||||
// Füge den Terminator hinzu
|
||||
$encoded .= str_repeat('0', $terminatorLength);
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Füllt den Bitstream auf eine Byte-Grenze (Multiple von 8 Bits) auf
|
||||
*/
|
||||
private function padToByteBoundary(string $encoded): string
|
||||
{
|
||||
$remainder = strlen($encoded) % 8;
|
||||
|
||||
if ($remainder > 0) {
|
||||
$encoded .= str_repeat('0', 8 - $remainder);
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Füllt den Bitstream mit Pad-Bytes auf bis zur maximalen Kapazität
|
||||
*/
|
||||
private function addPadBytes(string $encoded, QrCodeVersion $version, $errorCorrectionLevel): string
|
||||
{
|
||||
$dataCapacityBits = $version->getDataCapacity($errorCorrectionLevel) * 8;
|
||||
$currentLength = strlen($encoded);
|
||||
|
||||
// Wenn die Daten bereits die Kapazität erreicht haben, nichts tun
|
||||
if ($currentLength >= $dataCapacityBits) {
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
// Pad-Bytes-Muster (wechselt zwischen 11101100 und 00010001)
|
||||
$padBytes = ['11101100', '00010001'];
|
||||
$padIndex = 0;
|
||||
|
||||
// Füge Pad-Bytes hinzu, bis die Kapazität erreicht ist
|
||||
while ($currentLength < $dataCapacityBits) {
|
||||
$encoded .= $padBytes[$padIndex];
|
||||
$padIndex = ($padIndex + 1) % 2;
|
||||
$currentLength += 8;
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\QrCode;
|
||||
use App\Domain\QrCode\Exception\QrCodeException;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeMatrix;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeVersion;
|
||||
|
||||
class QrCodeGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly QrCodeEncoder $encoder,
|
||||
private readonly QrCodeRenderer $renderer
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code aus den gegebenen Daten
|
||||
* @throws QrCodeException
|
||||
*/
|
||||
public function generate(QrCode $qrCode): QrCodeMatrix
|
||||
{
|
||||
$version = $this->determineVersion($qrCode);
|
||||
$encodedData = $this->encoder->encode($qrCode, $version);
|
||||
|
||||
return $this->renderer->render($encodedData, $version, $qrCode->getErrorCorrectionLevel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt die optimale QR-Code-Version
|
||||
* @throws QrCodeException
|
||||
*/
|
||||
private function determineVersion(QrCode $qrCode): QrCodeVersion
|
||||
{
|
||||
if ($qrCode->getVersion() !== null) {
|
||||
return $qrCode->getVersion();
|
||||
}
|
||||
|
||||
$dataLength = strlen($qrCode->getData());
|
||||
|
||||
// Bei längeren Daten mit größeren Versionen beginnen, um Berechnungszeit zu sparen
|
||||
$startVersion = match (true) {
|
||||
$dataLength < 30 => 1,
|
||||
$dataLength < 100 => 5,
|
||||
$dataLength < 500 => 15,
|
||||
default => 25,
|
||||
};
|
||||
|
||||
for ($version = $startVersion; $version <= 40; $version++) {
|
||||
$qrVersion = new QrCodeVersion($version);
|
||||
if ($qrVersion->getDataCapacity($qrCode->getErrorCorrectionLevel()) >= $dataLength) {
|
||||
return $qrVersion;
|
||||
}
|
||||
}
|
||||
|
||||
throw new QrCodeException('Daten zu groß für QR-Code');
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\MaskPattern;
|
||||
|
||||
class QrCodeMasker
|
||||
{
|
||||
/**
|
||||
* Findet die beste Maske für die Matrix
|
||||
*/
|
||||
public function findBestMask(array $matrix, int $size): MaskPattern
|
||||
{
|
||||
$lowestPenalty = PHP_INT_MAX;
|
||||
$bestMask = MaskPattern::PATTERN_0;
|
||||
|
||||
foreach (MaskPattern::cases() as $mask) {
|
||||
$testMatrix = $matrix;
|
||||
$this->applyMask($testMatrix, $size, $mask);
|
||||
$penalty = $this->calculateMaskPenalty($testMatrix, $size);
|
||||
|
||||
if ($penalty < $lowestPenalty) {
|
||||
$lowestPenalty = $penalty;
|
||||
$bestMask = $mask;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestMask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet eine Maske auf die Matrix an
|
||||
*/
|
||||
public function applyMask(array &$matrix, int $size, MaskPattern $maskPattern): void
|
||||
{
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
if ($matrix[$row][$col] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isFunctionModule($row, $col, $size)) {
|
||||
if ($maskPattern->shouldMask($row, $col)) {
|
||||
$matrix[$row][$col] = !$matrix[$row][$col];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Strafpunkte für eine maskierte Matrix
|
||||
*/
|
||||
public function calculateMaskPenalty(array $matrix, int $size): int
|
||||
{
|
||||
$penalty = 0;
|
||||
|
||||
// Regel 1: Aufeinanderfolgende Module gleicher Farbe
|
||||
$penalty += $this->evaluateConsecutiveModulesPenalty($matrix, $size);
|
||||
|
||||
// Regel 2: Blöcke gleicher Farbe
|
||||
$penalty += $this->evaluateSameColorBlocksPenalty($matrix, $size);
|
||||
|
||||
// Regel 3: Muster, die Finder-Pattern ähneln
|
||||
$penalty += $this->evaluateFinderPatternLikePenalty($matrix, $size);
|
||||
|
||||
// Regel 4: Ausgewogenheit schwarzer und weißer Module
|
||||
$penalty += $this->evaluateBalancePenalty($matrix, $size);
|
||||
|
||||
return $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluiert die Strafe für aufeinanderfolgende Module gleicher Farbe
|
||||
*/
|
||||
private function evaluateConsecutiveModulesPenalty(array $matrix, int $size): int
|
||||
{
|
||||
$penalty = 0;
|
||||
|
||||
// Zeilen prüfen
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
$count = 1;
|
||||
for ($col = 1; $col < $size; $col++) {
|
||||
if ($matrix[$row][$col] === $matrix[$row][$col - 1]) {
|
||||
$count++;
|
||||
} else {
|
||||
if ($count >= 5) {
|
||||
$penalty += 3 + ($count - 5);
|
||||
}
|
||||
$count = 1;
|
||||
}
|
||||
}
|
||||
if ($count >= 5) {
|
||||
$penalty += 3 + ($count - 5);
|
||||
}
|
||||
}
|
||||
|
||||
// Spalten prüfen
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
$count = 1;
|
||||
for ($row = 1; $row < $size; $row++) {
|
||||
if ($matrix[$row][$col] === $matrix[$row - 1][$col]) {
|
||||
$count++;
|
||||
} else {
|
||||
if ($count >= 5) {
|
||||
$penalty += 3 + ($count - 5);
|
||||
}
|
||||
$count = 1;
|
||||
}
|
||||
}
|
||||
if ($count >= 5) {
|
||||
$penalty += 3 + ($count - 5);
|
||||
}
|
||||
}
|
||||
|
||||
return $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluiert die Strafe für 2x2 Blöcke gleicher Farbe
|
||||
*/
|
||||
private function evaluateSameColorBlocksPenalty(array $matrix, int $size): int
|
||||
{
|
||||
$penalty = 0;
|
||||
|
||||
for ($row = 0; $row < $size - 1; $row++) {
|
||||
for ($col = 0; $col < $size - 1; $col++) {
|
||||
$color = $matrix[$row][$col];
|
||||
if ($color === $matrix[$row][$col + 1] &&
|
||||
$color === $matrix[$row + 1][$col] &&
|
||||
$color === $matrix[$row + 1][$col + 1]) {
|
||||
$penalty += 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluiert die Strafe für Muster, die wie Finder-Pattern aussehen
|
||||
*/
|
||||
private function evaluateFinderPatternLikePenalty(array $matrix, int $size): int
|
||||
{
|
||||
$penalty = 0;
|
||||
// Vereinfachte Implementation
|
||||
return $penalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluiert die Strafe für unausgewogene schwarze/weiße Module
|
||||
*/
|
||||
private function evaluateBalancePenalty(array $matrix, int $size): int
|
||||
{
|
||||
$darkCount = 0;
|
||||
$totalCount = $size * $size;
|
||||
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
if ($matrix[$row][$col] === true) {
|
||||
$darkCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$darkPercentage = $darkCount * 100 / $totalCount;
|
||||
$deviation = abs($darkPercentage - 50) / 5;
|
||||
|
||||
return (int) ($deviation * 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein Modul ein Funktionsmodul ist (also nicht maskiert werden sollte)
|
||||
*/
|
||||
private function isFunctionModule(int $row, int $col, int $size): bool
|
||||
{
|
||||
// Finder-Pattern und Separatoren
|
||||
if (($row < 9 && $col < 9) ||
|
||||
($row < 9 && $col >= $size - 8) ||
|
||||
($row >= $size - 8 && $col < 9)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Timing-Pattern
|
||||
if ($row === 6 || $col === 6) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Format-Informationen
|
||||
if (($row < 9 && $col === 8) || ($row === 8 && $col < 9) ||
|
||||
($row === 8 && $col >= $size - 8) || ($row >= $size - 8 && $col === 8)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Dark Module bei Version 1
|
||||
if ($row === 8 && $col === 13) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\Pattern\AlignmentPattern;
|
||||
use App\Domain\QrCode\Pattern\DarkModulePattern;
|
||||
use App\Domain\QrCode\Pattern\FinderPattern;
|
||||
use App\Domain\QrCode\Pattern\FormatInfoPattern;
|
||||
use App\Domain\QrCode\Pattern\TimingPattern;
|
||||
use App\Domain\QrCode\Pattern\VersionInfoPattern;
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
use App\Domain\QrCode\ValueObject\MaskPattern;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeMatrix;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeVersion;
|
||||
|
||||
class QrCodeRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly QrCodeMasker $masker = new QrCodeMasker(),
|
||||
private readonly ReedSolomon $reedSolomon = new ReedSolomon()
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert einen QR-Code basierend auf den kodierten Daten
|
||||
*/
|
||||
public function render(string $data, QrCodeVersion $version, ErrorCorrectionLevel $level): QrCodeMatrix
|
||||
{
|
||||
$size = $version->getSize();
|
||||
$matrix = array_fill(0, $size, array_fill(0, $size, null));
|
||||
|
||||
// 1. Funktionsmuster hinzufügen
|
||||
$this->applyFunctionalPatterns($matrix, $version);
|
||||
|
||||
// 2. Daten mit Error Correction platzieren
|
||||
$dataWithEcc = $this->reedSolomon->addErrorCorrection(
|
||||
$data,
|
||||
$version->getDataCapacity($level),
|
||||
$level
|
||||
);
|
||||
$this->placeData($matrix, $dataWithEcc, $size);
|
||||
|
||||
// 3. Beste Maske finden und anwenden
|
||||
$bestMask = $this->masker->findBestMask($matrix, $size);
|
||||
$this->masker->applyMask($matrix, $size, $bestMask);
|
||||
|
||||
// 4. Format-Informationen hinzufügen
|
||||
$formatPattern = new FormatInfoPattern($level, $bestMask->value);
|
||||
$formatPattern->apply($matrix, $size);
|
||||
|
||||
// null-Werte durch false ersetzen
|
||||
for ($r = 0; $r < $size; $r++) {
|
||||
for ($c = 0; $c < $size; $c++) {
|
||||
if ($matrix[$r][$c] === null) {
|
||||
$matrix[$r][$c] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new QrCodeMatrix($matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet alle funktionalen Muster auf die Matrix an
|
||||
*/
|
||||
private function applyFunctionalPatterns(array &$matrix, QrCodeVersion $version): void
|
||||
{
|
||||
$size = $version->getSize();
|
||||
$versionValue = $version->getValue();
|
||||
|
||||
// 1. Finder-Muster
|
||||
$finderPatterns = FinderPattern::createAll($size);
|
||||
foreach ($finderPatterns as $pattern) {
|
||||
$pattern->apply($matrix, $size);
|
||||
}
|
||||
|
||||
// 2. Timing-Muster
|
||||
$timingPattern = new TimingPattern();
|
||||
$timingPattern->apply($matrix, $size);
|
||||
|
||||
// 3. Ausrichtungsmuster (für Versionen > 1)
|
||||
if ($versionValue > 1) {
|
||||
$alignmentPattern = new AlignmentPattern($versionValue);
|
||||
$alignmentPattern->apply($matrix, $size);
|
||||
}
|
||||
|
||||
// 4. Dunkles Modul
|
||||
$darkModule = new DarkModulePattern($versionValue);
|
||||
$darkModule->apply($matrix, $size);
|
||||
|
||||
// 5. Versions-Information (für Versionen >= 7)
|
||||
if ($versionValue >= 7) {
|
||||
$versionInfoPattern = new VersionInfoPattern($versionValue);
|
||||
$versionInfoPattern->apply($matrix, $size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platziert die kodierten Daten in der Matrix
|
||||
*/
|
||||
private function placeData(array &$matrix, string $data, int $size): void
|
||||
{
|
||||
$dataIndex = 0;
|
||||
$direction = -1; // Starte mit Aufwärtsbewegung
|
||||
|
||||
// Starte von unten rechts und bewege dich im Zickzack-Muster
|
||||
for ($col = $size - 1; $col > 0; $col -= 2) {
|
||||
// Timing-Spalte überspringen
|
||||
if ($col === 6) $col--;
|
||||
|
||||
for ($count = 0; $count < $size; $count++) {
|
||||
$row = $direction === -1 ? $size - 1 - $count : $count;
|
||||
|
||||
for ($c = 0; $c < 2; $c++) {
|
||||
$currentCol = $col - $c;
|
||||
|
||||
// Nur in leere Zellen (null) Daten einfügen
|
||||
if ($row >= 0 && $row < $size && $currentCol >= 0 && $currentCol < $size &&
|
||||
$matrix[$row][$currentCol] === null) {
|
||||
|
||||
if ($dataIndex < strlen($data)) {
|
||||
$matrix[$row][$currentCol] = ($data[$dataIndex] === '1');
|
||||
$dataIndex++;
|
||||
} else {
|
||||
$matrix[$row][$currentCol] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$direction *= -1; // Richtung umkehren
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
|
||||
/**
|
||||
* Implementiert Reed-Solomon-Fehlerkorrektur für QR-Codes
|
||||
*/
|
||||
class ReedSolomon
|
||||
{
|
||||
private ReedSolomonEncoder $encoder;
|
||||
|
||||
// Lookup-Tabelle für ECC-Wörter pro Block basierend auf QR-Version und EC-Level
|
||||
private const ECC_WORDS_PER_BLOCK = [
|
||||
// Version => [L, M, Q, H]
|
||||
1 => [7, 10, 13, 17],
|
||||
2 => [10, 16, 22, 28],
|
||||
3 => [15, 26, 36, 44],
|
||||
4 => [20, 36, 52, 64],
|
||||
5 => [26, 48, 72, 88],
|
||||
6 => [36, 64, 96, 112],
|
||||
7 => [40, 72, 108, 130],
|
||||
8 => [48, 88, 132, 156],
|
||||
9 => [60, 110, 160, 192],
|
||||
10 => [72, 130, 192, 224],
|
||||
11 => [80, 150, 224, 264],
|
||||
12 => [96, 176, 260, 308],
|
||||
13 => [104, 198, 288, 352],
|
||||
14 => [120, 216, 320, 384],
|
||||
15 => [132, 240, 360, 432],
|
||||
16 => [144, 280, 408, 480],
|
||||
17 => [168, 308, 448, 532],
|
||||
18 => [180, 338, 504, 588],
|
||||
19 => [196, 364, 546, 650],
|
||||
20 => [224, 416, 600, 700],
|
||||
21 => [224, 442, 644, 750],
|
||||
22 => [252, 476, 690, 816],
|
||||
23 => [270, 504, 750, 900],
|
||||
24 => [300, 560, 810, 960],
|
||||
25 => [312, 588, 870, 1050],
|
||||
26 => [336, 644, 952, 1110],
|
||||
27 => [360, 700, 1020, 1200],
|
||||
28 => [390, 728, 1050, 1260],
|
||||
29 => [420, 784, 1140, 1350],
|
||||
30 => [450, 812, 1200, 1440],
|
||||
31 => [480, 868, 1290, 1530],
|
||||
32 => [510, 924, 1350, 1620],
|
||||
33 => [540, 980, 1440, 1710],
|
||||
34 => [570, 1036, 1530, 1800],
|
||||
35 => [570, 1064, 1590, 1890],
|
||||
36 => [600, 1120, 1680, 1980],
|
||||
37 => [630, 1204, 1770, 2100],
|
||||
38 => [660, 1260, 1860, 2220],
|
||||
39 => [720, 1316, 1950, 2310],
|
||||
40 => [750, 1372, 2040, 2430],
|
||||
];
|
||||
|
||||
// Lookup-Tabelle für die Anzahl der Daten- und ECC-Blöcke
|
||||
// [Gruppe1Blöcke, Gruppe1Wörter, Gruppe2Blöcke, Gruppe2Wörter]
|
||||
private const BLOCKS_PER_VERSION = [
|
||||
1 => [
|
||||
'L' => [1, 19, 0, 0],
|
||||
'M' => [1, 16, 0, 0],
|
||||
'Q' => [1, 13, 0, 0],
|
||||
'H' => [1, 9, 0, 0],
|
||||
],
|
||||
2 => [
|
||||
'L' => [1, 34, 0, 0],
|
||||
'M' => [1, 28, 0, 0],
|
||||
'Q' => [1, 22, 0, 0],
|
||||
'H' => [1, 16, 0, 0],
|
||||
],
|
||||
3 => [
|
||||
'L' => [1, 55, 0, 0],
|
||||
'M' => [1, 44, 0, 0],
|
||||
'Q' => [2, 17, 0, 0],
|
||||
'H' => [2, 13, 0, 0],
|
||||
],
|
||||
4 => [
|
||||
'L' => [1, 80, 0, 0],
|
||||
'M' => [2, 32, 0, 0],
|
||||
'Q' => [2, 24, 0, 0],
|
||||
'H' => [4, 9, 0, 0],
|
||||
],
|
||||
5 => [
|
||||
'L' => [1, 108, 0, 0],
|
||||
'M' => [2, 43, 0, 0],
|
||||
'Q' => [2, 15, 2, 16],
|
||||
'H' => [2, 11, 2, 12],
|
||||
],
|
||||
6 => [
|
||||
'L' => [2, 68, 0, 0],
|
||||
'M' => [4, 27, 0, 0],
|
||||
'Q' => [4, 19, 0, 0],
|
||||
'H' => [4, 15, 0, 0],
|
||||
],
|
||||
7 => [
|
||||
'L' => [2, 78, 0, 0],
|
||||
'M' => [4, 31, 0, 0],
|
||||
'Q' => [2, 14, 4, 15],
|
||||
'H' => [4, 13, 1, 14],
|
||||
],
|
||||
8 => [
|
||||
'L' => [2, 97, 0, 0],
|
||||
'M' => [2, 38, 2, 39],
|
||||
'Q' => [4, 18, 2, 19],
|
||||
'H' => [4, 14, 2, 15],
|
||||
],
|
||||
9 => [
|
||||
'L' => [2, 116, 0, 0],
|
||||
'M' => [3, 36, 2, 37],
|
||||
'Q' => [4, 16, 4, 17],
|
||||
'H' => [4, 12, 4, 13],
|
||||
],
|
||||
10 => [
|
||||
'L' => [2, 68, 2, 69],
|
||||
'M' => [4, 43, 1, 44],
|
||||
'Q' => [6, 19, 2, 20],
|
||||
'H' => [6, 15, 2, 16],
|
||||
],
|
||||
11 => [
|
||||
'L' => [4, 81, 0, 0],
|
||||
'M' => [1, 50, 4, 51],
|
||||
'Q' => [4, 22, 4, 23],
|
||||
'H' => [3, 12, 8, 13],
|
||||
],
|
||||
12 => [
|
||||
'L' => [2, 92, 2, 93],
|
||||
'M' => [6, 36, 2, 37],
|
||||
'Q' => [4, 20, 6, 21],
|
||||
'H' => [7, 14, 4, 15],
|
||||
],
|
||||
13 => [
|
||||
'L' => [4, 107, 0, 0],
|
||||
'M' => [8, 37, 1, 38],
|
||||
'Q' => [8, 20, 4, 21],
|
||||
'H' => [12, 11, 4, 12],
|
||||
],
|
||||
14 => [
|
||||
'L' => [3, 115, 1, 116],
|
||||
'M' => [4, 40, 5, 41],
|
||||
'Q' => [11, 16, 5, 17],
|
||||
'H' => [11, 12, 5, 13],
|
||||
],
|
||||
15 => [
|
||||
'L' => [5, 87, 1, 88],
|
||||
'M' => [5, 41, 5, 42],
|
||||
'Q' => [5, 24, 7, 25],
|
||||
'H' => [11, 12, 7, 13],
|
||||
],
|
||||
16 => [
|
||||
'L' => [5, 98, 1, 99],
|
||||
'M' => [7, 45, 3, 46],
|
||||
'Q' => [15, 19, 2, 20],
|
||||
'H' => [3, 15, 13, 16],
|
||||
],
|
||||
17 => [
|
||||
'L' => [1, 107, 5, 108],
|
||||
'M' => [10, 46, 1, 47],
|
||||
'Q' => [1, 22, 15, 23],
|
||||
'H' => [2, 14, 17, 15],
|
||||
],
|
||||
18 => [
|
||||
'L' => [5, 120, 1, 121],
|
||||
'M' => [9, 43, 4, 44],
|
||||
'Q' => [17, 22, 1, 23],
|
||||
'H' => [2, 14, 19, 15],
|
||||
],
|
||||
19 => [
|
||||
'L' => [3, 113, 4, 114],
|
||||
'M' => [3, 44, 11, 45],
|
||||
'Q' => [17, 21, 4, 22],
|
||||
'H' => [9, 13, 16, 14],
|
||||
],
|
||||
20 => [
|
||||
'L' => [3, 107, 5, 108],
|
||||
'M' => [3, 41, 13, 42],
|
||||
'Q' => [15, 24, 5, 25],
|
||||
'H' => [15, 15, 10, 16],
|
||||
],
|
||||
21 => [
|
||||
'L' => [4, 116, 4, 117],
|
||||
'M' => [17, 42, 0, 0],
|
||||
'Q' => [17, 22, 6, 23],
|
||||
'H' => [19, 16, 6, 17],
|
||||
],
|
||||
22 => [
|
||||
'L' => [2, 111, 7, 112],
|
||||
'M' => [17, 46, 0, 0],
|
||||
'Q' => [7, 24, 16, 25],
|
||||
'H' => [34, 13, 0, 0],
|
||||
],
|
||||
23 => [
|
||||
'L' => [4, 121, 5, 122],
|
||||
'M' => [4, 47, 14, 48],
|
||||
'Q' => [11, 24, 14, 25],
|
||||
'H' => [16, 15, 14, 16],
|
||||
],
|
||||
24 => [
|
||||
'L' => [6, 117, 4, 118],
|
||||
'M' => [6, 45, 14, 46],
|
||||
'Q' => [11, 24, 16, 25],
|
||||
'H' => [30, 16, 2, 17],
|
||||
],
|
||||
25 => [
|
||||
'L' => [8, 106, 4, 107],
|
||||
'M' => [8, 47, 13, 48],
|
||||
'Q' => [7, 24, 22, 25],
|
||||
'H' => [22, 15, 13, 16],
|
||||
],
|
||||
26 => [
|
||||
'L' => [10, 114, 2, 115],
|
||||
'M' => [19, 46, 4, 47],
|
||||
'Q' => [28, 22, 6, 23],
|
||||
'H' => [33, 16, 4, 17],
|
||||
],
|
||||
27 => [
|
||||
'L' => [8, 122, 4, 123],
|
||||
'M' => [22, 45, 3, 46],
|
||||
'Q' => [8, 23, 26, 24],
|
||||
'H' => [12, 15, 28, 16],
|
||||
],
|
||||
28 => [
|
||||
'L' => [3, 117, 10, 118],
|
||||
'M' => [3, 45, 23, 46],
|
||||
'Q' => [4, 24, 31, 25],
|
||||
'H' => [11, 15, 31, 16],
|
||||
],
|
||||
29 => [
|
||||
'L' => [7, 116, 7, 117],
|
||||
'M' => [21, 45, 7, 46],
|
||||
'Q' => [1, 23, 37, 24],
|
||||
'H' => [19, 15, 26, 16],
|
||||
],
|
||||
30 => [
|
||||
'L' => [5, 115, 10, 116],
|
||||
'M' => [19, 47, 10, 48],
|
||||
'Q' => [15, 24, 25, 25],
|
||||
'H' => [23, 15, 25, 16],
|
||||
],
|
||||
31 => [
|
||||
'L' => [13, 115, 3, 116],
|
||||
'M' => [2, 46, 29, 47],
|
||||
'Q' => [42, 24, 1, 25],
|
||||
'H' => [23, 15, 28, 16],
|
||||
],
|
||||
32 => [
|
||||
'L' => [17, 115, 0, 0],
|
||||
'M' => [10, 46, 23, 47],
|
||||
'Q' => [10, 24, 35, 25],
|
||||
'H' => [19, 15, 35, 16],
|
||||
],
|
||||
33 => [
|
||||
'L' => [17, 115, 1, 116],
|
||||
'M' => [14, 46, 21, 47],
|
||||
'Q' => [29, 24, 19, 25],
|
||||
'H' => [11, 15, 46, 16],
|
||||
],
|
||||
34 => [
|
||||
'L' => [13, 115, 6, 116],
|
||||
'M' => [14, 46, 23, 47],
|
||||
'Q' => [44, 24, 7, 25],
|
||||
'H' => [59, 16, 1, 17],
|
||||
],
|
||||
35 => [
|
||||
'L' => [12, 121, 7, 122],
|
||||
'M' => [12, 47, 26, 48],
|
||||
'Q' => [39, 24, 14, 25],
|
||||
'H' => [22, 15, 41, 16],
|
||||
],
|
||||
36 => [
|
||||
'L' => [6, 121, 14, 122],
|
||||
'M' => [6, 47, 34, 48],
|
||||
'Q' => [46, 24, 10, 25],
|
||||
'H' => [2, 15, 64, 16],
|
||||
],
|
||||
37 => [
|
||||
'L' => [17, 122, 4, 123],
|
||||
'M' => [29, 46, 14, 47],
|
||||
'Q' => [49, 24, 10, 25],
|
||||
'H' => [24, 15, 46, 16],
|
||||
],
|
||||
38 => [
|
||||
'L' => [4, 122, 18, 123],
|
||||
'M' => [13, 46, 32, 47],
|
||||
'Q' => [48, 24, 14, 25],
|
||||
'H' => [42, 15, 32, 16],
|
||||
],
|
||||
39 => [
|
||||
'L' => [20, 117, 4, 118],
|
||||
'M' => [40, 47, 7, 48],
|
||||
'Q' => [43, 24, 22, 25],
|
||||
'H' => [10, 15, 67, 16],
|
||||
],
|
||||
40 => [
|
||||
'L' => [19, 118, 6, 119],
|
||||
'M' => [18, 47, 31, 48],
|
||||
'Q' => [34, 24, 34, 25],
|
||||
'H' => [20, 15, 61, 16],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->encoder = new ReedSolomonEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt Reed-Solomon-Fehlerkorrektur zu den Daten hinzu
|
||||
*
|
||||
* @param string $data Binäre Daten ('0' und '1')
|
||||
* @param int $dataCapacity Kapazität in Bytes
|
||||
* @param ErrorCorrectionLevel $level Fehlerkorrektur-Level
|
||||
* @return string Binäre Daten mit Fehlerkorrektur
|
||||
*/
|
||||
public function addErrorCorrection(
|
||||
string $data,
|
||||
int $dataCapacity,
|
||||
ErrorCorrectionLevel $level
|
||||
): string {
|
||||
// Konvertiere binäre Daten in Byte-Array
|
||||
$bytes = $this->binaryToBytes($data);
|
||||
|
||||
// Berechne die QR-Version basierend auf der Kapazität
|
||||
$version = $this->estimateVersionFromCapacity($dataCapacity);
|
||||
|
||||
// Bestimme die Anzahl der ECC-Wörter pro Block für diese Version und EC-Level
|
||||
$eccWordsPerBlock = $this->getEccWordsPerBlock($version, $level);
|
||||
|
||||
// Organisiere Daten in Blöcke gemäß QR-Code-Spezifikation
|
||||
$blocks = $this->organizeDataBlocks($bytes, $version, $level);
|
||||
|
||||
// Kodiere jeden Block mit Reed-Solomon
|
||||
$encodedBlocks = [];
|
||||
foreach ($blocks as $block) {
|
||||
$encodedBlocks[] = $this->encoder->encode($block, $eccWordsPerBlock);
|
||||
}
|
||||
|
||||
// Interleave die Blöcke gemäß QR-Code-Spezifikation
|
||||
$interleavedData = $this->interleaveBlocks($encodedBlocks);
|
||||
|
||||
// Konvertiere zurück zu binärer Darstellung
|
||||
return $this->bytesToBinary($interleavedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert binäre Daten ('0' und '1') in ein Byte-Array
|
||||
*/
|
||||
private function binaryToBytes(string $binary): array
|
||||
{
|
||||
$bytes = [];
|
||||
$length = strlen($binary);
|
||||
|
||||
// Verarbeite vollständige Bytes
|
||||
for ($i = 0; $i < $length; $i += 8) {
|
||||
if ($i + 8 <= $length) {
|
||||
$byte = bindec(substr($binary, $i, 8));
|
||||
$bytes[] = $byte;
|
||||
} else {
|
||||
// Letztes unvollständiges Byte mit Nullen auffüllen
|
||||
$partialByte = substr($binary, $i);
|
||||
$paddedByte = str_pad($partialByte, 8, '0', STR_PAD_RIGHT);
|
||||
$bytes[] = bindec($paddedByte);
|
||||
}
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert ein Byte-Array zurück in binäre Darstellung
|
||||
*/
|
||||
private function bytesToBinary(array $bytes): string
|
||||
{
|
||||
$binary = '';
|
||||
|
||||
foreach ($bytes as $byte) {
|
||||
$binary .= str_pad(decbin($byte), 8, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schätzt die QR-Version basierend auf der Kapazität
|
||||
*/
|
||||
private function estimateVersionFromCapacity(int $capacity): int
|
||||
{
|
||||
// Vereinfachte Schätzung
|
||||
if ($capacity <= 17) return 1;
|
||||
if ($capacity <= 32) return 2;
|
||||
if ($capacity <= 53) return 3;
|
||||
if ($capacity <= 78) return 4;
|
||||
if ($capacity <= 106) return 5;
|
||||
if ($capacity <= 134) return 6;
|
||||
if ($capacity <= 154) return 7;
|
||||
if ($capacity <= 192) return 8;
|
||||
if ($capacity <= 230) return 9;
|
||||
if ($capacity <= 271) return 10;
|
||||
|
||||
// Fallback für größere Versionen
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die Anzahl der ECC-Wörter pro Block für die angegebene Version und EC-Level
|
||||
*/
|
||||
private function getEccWordsPerBlock(int $version, ErrorCorrectionLevel $level): int
|
||||
{
|
||||
$index = match ($level) {
|
||||
ErrorCorrectionLevel::L => 0,
|
||||
ErrorCorrectionLevel::M => 1,
|
||||
ErrorCorrectionLevel::Q => 2,
|
||||
ErrorCorrectionLevel::H => 3,
|
||||
};
|
||||
|
||||
return self::ECC_WORDS_PER_BLOCK[$version][$index] ?? 10; // Fallback-Wert
|
||||
}
|
||||
|
||||
/**
|
||||
* Organisiert die Daten in Blöcke gemäß QR-Code-Spezifikation
|
||||
*/
|
||||
private function organizeDataBlocks(array $data, int $version, ErrorCorrectionLevel $level): array
|
||||
{
|
||||
$blocks = [];
|
||||
|
||||
// Hol Block-Informationen
|
||||
$blockInfo = self::BLOCKS_PER_VERSION[$version][$level->name] ?? [1, count($data), 0, 0];
|
||||
|
||||
$group1Blocks = $blockInfo[0];
|
||||
$group1Words = $blockInfo[1];
|
||||
$group2Blocks = $blockInfo[2];
|
||||
$group2Words = $blockInfo[3];
|
||||
|
||||
$dataIndex = 0;
|
||||
|
||||
// Gruppe 1 Blöcke
|
||||
for ($i = 0; $i < $group1Blocks; $i++) {
|
||||
$block = array_slice($data, $dataIndex, $group1Words);
|
||||
$blocks[] = $block;
|
||||
$dataIndex += $group1Words;
|
||||
}
|
||||
|
||||
// Gruppe 2 Blöcke
|
||||
for ($i = 0; $i < $group2Blocks; $i++) {
|
||||
$block = array_slice($data, $dataIndex, $group2Words);
|
||||
$blocks[] = $block;
|
||||
$dataIndex += $group2Words;
|
||||
}
|
||||
|
||||
// Wenn nur ein Block, verwende alle Daten
|
||||
if (empty($blocks)) {
|
||||
$blocks[] = $data;
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interleaved die kodierten Blöcke gemäß QR-Code-Spezifikation
|
||||
*/
|
||||
private function interleaveBlocks(array $blocks): array
|
||||
{
|
||||
$result = [];
|
||||
$maxLength = 0;
|
||||
|
||||
// Finde die maximale Blocklänge
|
||||
foreach ($blocks as $block) {
|
||||
$maxLength = max($maxLength, count($block));
|
||||
}
|
||||
|
||||
// Interleave Daten- und ECC-Bytes
|
||||
for ($i = 0; $i < $maxLength; $i++) {
|
||||
foreach ($blocks as $block) {
|
||||
if ($i < count($block)) {
|
||||
$result[] = $block[$i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\Service;
|
||||
|
||||
use App\Domain\QrCode\ValueObject\GaloisField;
|
||||
use App\Domain\QrCode\ValueObject\Polynomial;
|
||||
|
||||
/**
|
||||
* Reed-Solomon Encoder für QR-Code-Fehlerkorrektur
|
||||
*/
|
||||
class ReedSolomonEncoder
|
||||
{
|
||||
private GaloisField $field;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->field = new GaloisField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kodiert die Daten mit Reed-Solomon-Fehlerkorrektur
|
||||
*
|
||||
* @param array $data Daten als Array von Bytes
|
||||
* @param int $eccLength Anzahl der Error-Correction-Codewords
|
||||
* @return array Kodierte Daten mit ECC-Bytes
|
||||
*/
|
||||
public function encode(array $data, int $eccLength): array
|
||||
{
|
||||
if ($eccLength === 0) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Erzeuge das Generator-Polynom für die gegebene Anzahl von ECC-Bytes
|
||||
$generator = Polynomial::buildGenerator($this->field, $eccLength);
|
||||
|
||||
// Konvertiere Daten in ein Polynom
|
||||
$dataPolynomial = new Polynomial($this->field, $data);
|
||||
|
||||
// Multipliziere mit x^eccLength, um Platz für ECC-Bytes zu schaffen
|
||||
$paddedData = array_merge(
|
||||
array_fill(0, $eccLength, 0),
|
||||
$dataPolynomial->getCoefficients()
|
||||
);
|
||||
$paddedPolynomial = new Polynomial($this->field, $paddedData);
|
||||
|
||||
// Dividiere durch das Generator-Polynom, der Rest ist der ECC
|
||||
$divResult = $paddedPolynomial->divideAndRemainder($generator);
|
||||
$remainder = $divResult['remainder']->getCoefficients();
|
||||
|
||||
// Fülle den Rest mit Nullen auf, falls er zu kurz ist
|
||||
if (count($remainder) < $eccLength) {
|
||||
$remainder = array_merge(
|
||||
array_fill(0, $eccLength - count($remainder), 0),
|
||||
$remainder
|
||||
);
|
||||
}
|
||||
|
||||
// Konkateniere Daten und ECC-Bytes
|
||||
return array_merge($data, $remainder);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
enum ErrorCorrectionLevel: string
|
||||
{
|
||||
case L = 'L'; // ~7% Korrektur
|
||||
case M = 'M'; // ~15% Korrektur
|
||||
case Q = 'Q'; // ~25% Korrektur
|
||||
case H = 'H'; // ~30% Korrektur
|
||||
|
||||
public function getCapacityReduction(): float
|
||||
{
|
||||
return match ($this) {
|
||||
self::L => 0.07,
|
||||
self::M => 0.15,
|
||||
self::Q => 0.25,
|
||||
self::H => 0.30,
|
||||
};
|
||||
}
|
||||
|
||||
public function getNumeric(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::L => 1,
|
||||
self::M => 0,
|
||||
self::Q => 3,
|
||||
self::H => 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
/**
|
||||
* Repräsentiert ein Galois-Feld GF(2^8) für Reed-Solomon-Berechnungen
|
||||
*/
|
||||
class GaloisField
|
||||
{
|
||||
// Generator-Polynom für GF(2^8)
|
||||
private const PRIMITIVE_POLY = 0x11D; // x^8 + x^4 + x^3 + x^2 + 1
|
||||
|
||||
// Lookup-Tabellen für Multiplikation und Division
|
||||
private array $expTable = [];
|
||||
private array $logTable = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert die Lookup-Tabellen für schnelle Berechnungen
|
||||
*/
|
||||
private function initTables(): void
|
||||
{
|
||||
// Setze Tabellen-Größe
|
||||
$this->expTable = array_fill(0, 256, 0);
|
||||
$this->logTable = array_fill(0, 256, 0);
|
||||
|
||||
// Berechne die Exponentiation und Logarithmus-Tabellen
|
||||
$x = 1;
|
||||
for ($i = 0; $i < 255; $i++) {
|
||||
$this->expTable[$i] = $x;
|
||||
$this->logTable[$x] = $i;
|
||||
|
||||
// Multiplikation mit x in GF(2^8)
|
||||
$x = ($x << 1) ^ ($x & 0x80 ? self::PRIMITIVE_POLY : 0);
|
||||
$x &= 0xFF;
|
||||
}
|
||||
|
||||
// Setze expTable[255] = expTable[0]
|
||||
$this->expTable[255] = $this->expTable[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Addiert zwei Elemente im Galois-Feld
|
||||
*/
|
||||
public function add(int $a, int $b): int
|
||||
{
|
||||
return $a ^ $b; // In GF(2^8) ist Addition = XOR
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtrahiert zwei Elemente im Galois-Feld
|
||||
* In GF(2^8) ist Subtraktion = Addition = XOR
|
||||
*/
|
||||
public function subtract(int $a, int $b): int
|
||||
{
|
||||
return $this->add($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multipliziert zwei Elemente im Galois-Feld
|
||||
*/
|
||||
public function multiply(int $a, int $b): int
|
||||
{
|
||||
// Spezialfall: Null-Multiplikation
|
||||
if ($a === 0 || $b === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Benutze Log-Tabellen für Multiplikation
|
||||
$logA = $this->logTable[$a];
|
||||
$logB = $this->logTable[$b];
|
||||
$sumLog = ($logA + $logB) % 255;
|
||||
|
||||
return $this->expTable[$sumLog];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dividiert zwei Elemente im Galois-Feld
|
||||
*/
|
||||
public function divide(int $a, int $b): int
|
||||
{
|
||||
if ($b === 0) {
|
||||
throw new \DivisionByZeroError('Division by zero in Galois field');
|
||||
}
|
||||
|
||||
if ($a === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Benutze Log-Tabellen für Division
|
||||
$logA = $this->logTable[$a];
|
||||
$logB = $this->logTable[$b];
|
||||
$diffLog = ($logA - $logB + 255) % 255;
|
||||
|
||||
return $this->expTable[$diffLog];
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Potenz eines Elements im Galois-Feld
|
||||
*/
|
||||
public function power(int $a, int $power): int
|
||||
{
|
||||
if ($a === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($power === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$logA = $this->logTable[$a];
|
||||
$resultLog = ($logA * $power) % 255;
|
||||
|
||||
return $this->expTable[$resultLog];
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Logarithmus-Tabelle
|
||||
*/
|
||||
public function getLogTable(): array
|
||||
{
|
||||
return $this->logTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Exponentiations-Tabelle
|
||||
*/
|
||||
public function getExpTable(): array
|
||||
{
|
||||
return $this->expTable;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
enum MaskPattern: int
|
||||
{
|
||||
case PATTERN_0 = 0; // (row + column) mod 2 == 0
|
||||
case PATTERN_1 = 1; // row mod 2 == 0
|
||||
case PATTERN_2 = 2; // column mod 3 == 0
|
||||
case PATTERN_3 = 3; // (row + column) mod 3 == 0
|
||||
case PATTERN_4 = 4; // (floor(row/2) + floor(column/3)) mod 2 == 0
|
||||
case PATTERN_5 = 5; // ((row * column) mod 2) + ((row * column) mod 3) == 0
|
||||
case PATTERN_6 = 6; // (((row * column) mod 2) + ((row * column) mod 3)) mod 2 == 0
|
||||
case PATTERN_7 = 7; // (((row + column) mod 2) + ((row * column) mod 3)) mod 2 == 0
|
||||
|
||||
public function shouldMask(int $row, int $col): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::PATTERN_0 => ($row + $col) % 2 === 0,
|
||||
self::PATTERN_1 => $row % 2 === 0,
|
||||
self::PATTERN_2 => $col % 3 === 0,
|
||||
self::PATTERN_3 => ($row + $col) % 3 === 0,
|
||||
self::PATTERN_4 => (intval($row / 2) + intval($col / 3)) % 2 === 0,
|
||||
self::PATTERN_5 => ($row * $col) % 2 + ($row * $col) % 3 === 0,
|
||||
self::PATTERN_6 => (($row * $col) % 2 + ($row * $col) % 3) % 2 === 0,
|
||||
self::PATTERN_7 => (($row + $col) % 2 + ($row * $col) % 3) % 2 === 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
/**
|
||||
* Repräsentiert ein Polynom im Galois-Feld für Reed-Solomon-Berechnungen
|
||||
*/
|
||||
class Polynomial
|
||||
{
|
||||
private array $coefficients;
|
||||
|
||||
public function __construct(
|
||||
private readonly GaloisField $field,
|
||||
array $coefficients
|
||||
) {
|
||||
// Entferne führende Nullen
|
||||
$this->coefficients = $this->removeLeadingZeros($coefficients);
|
||||
|
||||
// Leeres Polynom = [0]
|
||||
if (empty($this->coefficients)) {
|
||||
$this->coefficients = [0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt führende Nullen aus dem Koeffizienten-Array
|
||||
*/
|
||||
private function removeLeadingZeros(array $coefficients): array
|
||||
{
|
||||
$i = 0;
|
||||
while ($i < count($coefficients) - 1 && $coefficients[$i] === 0) {
|
||||
$i++;
|
||||
}
|
||||
|
||||
return array_slice($coefficients, $i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Koeffizienten des Polynoms
|
||||
*/
|
||||
public function getCoefficients(): array
|
||||
{
|
||||
return $this->coefficients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert den Grad des Polynoms
|
||||
*/
|
||||
public function getDegree(): int
|
||||
{
|
||||
return count($this->coefficients) - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wertet das Polynom an der Stelle x aus
|
||||
*/
|
||||
public function evaluateAt(int $x): int
|
||||
{
|
||||
if ($x === 0) {
|
||||
// f(0) ist der letzte Koeffizient
|
||||
return $this->coefficients[count($this->coefficients) - 1];
|
||||
}
|
||||
|
||||
$result = 0;
|
||||
$degree = $this->getDegree();
|
||||
|
||||
for ($i = 0; $i <= $degree; $i++) {
|
||||
// result = result * x + coef[i]
|
||||
$term = $this->field->multiply(
|
||||
$result,
|
||||
$x
|
||||
);
|
||||
$result = $this->field->add($term, $this->coefficients[$i]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Addiert zwei Polynome
|
||||
*/
|
||||
public function add(Polynomial $other): Polynomial
|
||||
{
|
||||
if ($this->isZero()) {
|
||||
return $other;
|
||||
}
|
||||
|
||||
if ($other->isZero()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$thisCoeffs = $this->coefficients;
|
||||
$otherCoeffs = $other->getCoefficients();
|
||||
|
||||
$resultLength = max(count($thisCoeffs), count($otherCoeffs));
|
||||
$result = array_fill(0, $resultLength, 0);
|
||||
|
||||
// Addiere die Koeffizienten
|
||||
for ($i = 0; $i < count($thisCoeffs); $i++) {
|
||||
$result[$i + ($resultLength - count($thisCoeffs))] = $thisCoeffs[$i];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($otherCoeffs); $i++) {
|
||||
$idx = $i + ($resultLength - count($otherCoeffs));
|
||||
$result[$idx] = $this->field->add($result[$idx], $otherCoeffs[$i]);
|
||||
}
|
||||
|
||||
return new Polynomial($this->field, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multipliziert zwei Polynome
|
||||
*/
|
||||
public function multiply(Polynomial $other): Polynomial
|
||||
{
|
||||
if ($this->isZero() || $other->isZero()) {
|
||||
return new Polynomial($this->field, [0]);
|
||||
}
|
||||
|
||||
$thisCoeffs = $this->coefficients;
|
||||
$otherCoeffs = $other->getCoefficients();
|
||||
|
||||
$resultLength = count($thisCoeffs) + count($otherCoeffs) - 1;
|
||||
$result = array_fill(0, $resultLength, 0);
|
||||
|
||||
// Multipliziere die Koeffizienten
|
||||
for ($i = 0; $i < count($thisCoeffs); $i++) {
|
||||
for ($j = 0; $j < count($otherCoeffs); $j++) {
|
||||
$idx = $i + $j;
|
||||
$product = $this->field->multiply($thisCoeffs[$i], $otherCoeffs[$j]);
|
||||
$result[$idx] = $this->field->add($result[$idx], $product);
|
||||
}
|
||||
}
|
||||
|
||||
return new Polynomial($this->field, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multipliziert das Polynom mit einem Skalar
|
||||
*/
|
||||
public function multiplyByScalar(int $scalar): Polynomial
|
||||
{
|
||||
if ($scalar === 0) {
|
||||
return new Polynomial($this->field, [0]);
|
||||
}
|
||||
|
||||
if ($scalar === 1) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($this->coefficients as $coefficient) {
|
||||
$result[] = $this->field->multiply($coefficient, $scalar);
|
||||
}
|
||||
|
||||
return new Polynomial($this->field, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dividiert ein Polynom durch ein anderes und gibt Quotient und Rest zurück
|
||||
*/
|
||||
public function divideAndRemainder(Polynomial $other): array
|
||||
{
|
||||
if ($other->isZero()) {
|
||||
throw new \DivisionByZeroError('Division by zero polynomial');
|
||||
}
|
||||
|
||||
if ($this->isZero()) {
|
||||
return [
|
||||
'quotient' => new Polynomial($this->field, [0]),
|
||||
'remainder' => new Polynomial($this->field, [0])
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->getDegree() < $other->getDegree()) {
|
||||
return [
|
||||
'quotient' => new Polynomial($this->field, [0]),
|
||||
'remainder' => $this
|
||||
];
|
||||
}
|
||||
|
||||
$quotientCoeffs = array_fill(0, $this->getDegree() - $other->getDegree() + 1, 0);
|
||||
$remainderCoeffs = $this->coefficients;
|
||||
|
||||
$denomLeadingTerm = $other->getCoefficients()[0];
|
||||
|
||||
for ($i = 0; $i <= $this->getDegree() - $other->getDegree(); $i++) {
|
||||
if ($remainderCoeffs[$i] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quotientCoeffs[$i] = $this->field->divide($remainderCoeffs[$i], $denomLeadingTerm);
|
||||
|
||||
for ($j = 0; $j <= $other->getDegree(); $j++) {
|
||||
$idx = $i + $j;
|
||||
$product = $this->field->multiply($quotientCoeffs[$i], $other->getCoefficients()[$j]);
|
||||
$remainderCoeffs[$idx] = $this->field->subtract($remainderCoeffs[$idx], $product);
|
||||
}
|
||||
}
|
||||
|
||||
// Extrahiere den Rest
|
||||
$remainderDegree = $other->getDegree() - 1;
|
||||
$remainder = array_slice($remainderCoeffs, count($remainderCoeffs) - $remainderDegree - 1);
|
||||
|
||||
return [
|
||||
'quotient' => new Polynomial($this->field, $quotientCoeffs),
|
||||
'remainder' => new Polynomial($this->field, $remainder)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob das Polynom das Nullpolynom ist
|
||||
*/
|
||||
public function isZero(): bool
|
||||
{
|
||||
return count($this->coefficients) === 1 && $this->coefficients[0] === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt ein Generatorpolynom für Reed-Solomon-Codes
|
||||
*/
|
||||
public static function buildGenerator(GaloisField $field, int $degree): Polynomial
|
||||
{
|
||||
// g(x) = (x-α^0)(x-α^1)...(x-α^(degree-1))
|
||||
$generator = new Polynomial($field, [1]); // Beginne mit g(x) = 1
|
||||
|
||||
for ($i = 0; $i < $degree; $i++) {
|
||||
// Faktor (x-α^i)
|
||||
$factor = new Polynomial($field, [
|
||||
1, // x^1
|
||||
$field->power(2, $i) // -α^i (In GF(2^m) ist -a = a)
|
||||
]);
|
||||
$generator = $generator->multiply($factor);
|
||||
}
|
||||
|
||||
return $generator;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
readonly class Position
|
||||
{
|
||||
public function __construct(
|
||||
private int $row,
|
||||
private int $column
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRow(): int
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
||||
public function getColumn(): int
|
||||
{
|
||||
return $this->column;
|
||||
}
|
||||
|
||||
public function offset(int $rowOffset, int $columnOffset): self
|
||||
{
|
||||
return new self(
|
||||
$this->row + $rowOffset,
|
||||
$this->column + $columnOffset
|
||||
);
|
||||
}
|
||||
|
||||
public function isWithinBounds(int $size): bool
|
||||
{
|
||||
return $this->row >= 0 && $this->row < $size &&
|
||||
$this->column >= 0 && $this->column < $size;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
readonly class QrCodeConfig
|
||||
{
|
||||
/**
|
||||
* @param int $moduleSize Größe eines QR-Code-Moduls in Pixeln
|
||||
* @param int $margin Rand um den QR-Code in Modulen
|
||||
* @param string $foregroundColor Vordergrundfarbe (für SVG)
|
||||
* @param string $backgroundColor Hintergrundfarbe (für SVG)
|
||||
*/
|
||||
public function __construct(
|
||||
private int $moduleSize = 4,
|
||||
private int $margin = 4,
|
||||
private string $foregroundColor = '#000000',
|
||||
private string $backgroundColor = '#FFFFFF'
|
||||
) {
|
||||
}
|
||||
|
||||
public function getModuleSize(): int
|
||||
{
|
||||
return $this->moduleSize;
|
||||
}
|
||||
|
||||
public function getMargin(): int
|
||||
{
|
||||
return $this->margin;
|
||||
}
|
||||
|
||||
public function getForegroundColor(): string
|
||||
{
|
||||
return $this->foregroundColor;
|
||||
}
|
||||
|
||||
public function getBackgroundColor(): string
|
||||
{
|
||||
return $this->backgroundColor;
|
||||
}
|
||||
|
||||
public function withModuleSize(int $moduleSize): self
|
||||
{
|
||||
return new self(
|
||||
$moduleSize,
|
||||
$this->margin,
|
||||
$this->foregroundColor,
|
||||
$this->backgroundColor
|
||||
);
|
||||
}
|
||||
|
||||
public function withMargin(int $margin): self
|
||||
{
|
||||
return new self(
|
||||
$this->moduleSize,
|
||||
$margin,
|
||||
$this->foregroundColor,
|
||||
$this->backgroundColor
|
||||
);
|
||||
}
|
||||
|
||||
public function withColors(string $foreground, string $background): self
|
||||
{
|
||||
return new self(
|
||||
$this->moduleSize,
|
||||
$this->margin,
|
||||
$foreground,
|
||||
$background
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
readonly class QrCodeMatrix
|
||||
{
|
||||
public function __construct(
|
||||
private array $matrix
|
||||
) {
|
||||
}
|
||||
|
||||
public function getMatrix(): array
|
||||
{
|
||||
return $this->matrix;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return count($this->matrix);
|
||||
}
|
||||
|
||||
public function getModule(int $row, int $col): bool
|
||||
{
|
||||
return $this->matrix[$row][$col] ?? false;
|
||||
}
|
||||
|
||||
public function toSvg(int $moduleSize = 4, int $margin = 4, string $foreground = '#000000', string $background = '#FFFFFF'): string
|
||||
{
|
||||
// Stellen Sie sicher, dass der Margin mindestens 4 Module beträgt (Quiet Zone)
|
||||
$margin = max(4, $margin);
|
||||
|
||||
$size = $this->getSize();
|
||||
$totalSize = ($size + 2 * $margin) * $moduleSize;
|
||||
|
||||
$svg = sprintf(
|
||||
'<svg width="%d" height="%d" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg">',
|
||||
$totalSize, $totalSize, $totalSize, $totalSize
|
||||
);
|
||||
|
||||
$svg .= sprintf('<rect width="100%%" height="100%%" fill="%s"/>', $background);
|
||||
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
if ($this->getModule($row, $col)) {
|
||||
$x = ($col + $margin) * $moduleSize;
|
||||
$y = ($row + $margin) * $moduleSize;
|
||||
$svg .= sprintf(
|
||||
'<rect x="%d" y="%d" width="%d" height="%d" fill="%s"/>',
|
||||
$x, $y, $moduleSize, $moduleSize, $foreground
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$svg .= '</svg>';
|
||||
|
||||
return $svg;
|
||||
}
|
||||
|
||||
public function toPng(int $moduleSize = 4, int $margin = 4): string
|
||||
{
|
||||
// Stellen Sie sicher, dass der Margin mindestens 4 Module beträgt (Quiet Zone)
|
||||
$margin = max(4, $margin);
|
||||
|
||||
$size = $this->getSize();
|
||||
$totalSize = ($size + 2 * $margin) * $moduleSize;
|
||||
|
||||
$image = imagecreate($totalSize, $totalSize);
|
||||
$white = imagecolorallocate($image, 255, 255, 255);
|
||||
$black = imagecolorallocate($image, 0, 0, 0);
|
||||
|
||||
imagefill($image, 0, 0, $white);
|
||||
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
if ($this->getModule($row, $col)) {
|
||||
$x1 = ($col + $margin) * $moduleSize;
|
||||
$y1 = ($row + $margin) * $moduleSize;
|
||||
$x2 = $x1 + $moduleSize - 1;
|
||||
$y2 = $y1 + $moduleSize - 1;
|
||||
|
||||
imagefilledrectangle($image, $x1, $y1, $x2, $y2, $black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
imagepng($image);
|
||||
$pngData = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
imagedestroy($image);
|
||||
|
||||
return $pngData;
|
||||
}
|
||||
|
||||
public function toAscii(): string
|
||||
{
|
||||
$size = $this->getSize();
|
||||
$result = '';
|
||||
|
||||
for ($row = 0; $row < $size; $row++) {
|
||||
for ($col = 0; $col < $size; $col++) {
|
||||
$result .= $this->getModule($row, $col) ? '██' : ' ';
|
||||
}
|
||||
$result .= "\n";
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
enum QrCodeMode: int
|
||||
{
|
||||
case NUMERIC = 1;
|
||||
case ALPHANUMERIC = 2;
|
||||
case BYTE = 4;
|
||||
case KANJI = 8;
|
||||
|
||||
public function getCharacterCountBits(QrCodeVersion $version): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::NUMERIC => match (true) {
|
||||
$version->getValue() <= 9 => 10,
|
||||
$version->getValue() <= 26 => 12,
|
||||
default => 14,
|
||||
},
|
||||
self::ALPHANUMERIC => match (true) {
|
||||
$version->getValue() <= 9 => 9,
|
||||
$version->getValue() <= 26 => 11,
|
||||
default => 13,
|
||||
},
|
||||
self::BYTE => match (true) {
|
||||
$version->getValue() <= 9 => 8,
|
||||
$version->getValue() <= 26 => 16,
|
||||
default => 16,
|
||||
},
|
||||
self::KANJI => match (true) {
|
||||
$version->getValue() <= 9 => 8,
|
||||
$version->getValue() <= 26 => 10,
|
||||
default => 12,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\QrCode\ValueObject;
|
||||
|
||||
use App\Domain\QrCode\Exception\QrCodeException;
|
||||
|
||||
readonly class QrCodeVersion
|
||||
{
|
||||
private int $version;
|
||||
|
||||
/**
|
||||
* Datenbyte-Kapazität für QR-Codes (nach QR-Code-Spezifikation)
|
||||
* Version => [L, M, Q, H] in Bytes
|
||||
*/
|
||||
private const DATA_CAPACITY = [
|
||||
// Version => [L, M, Q, H] (in Bytes)
|
||||
1 => [19, 16, 13, 9],
|
||||
2 => [34, 28, 22, 16],
|
||||
3 => [55, 44, 34, 26],
|
||||
4 => [80, 64, 48, 36],
|
||||
5 => [108, 86, 62, 46],
|
||||
6 => [136, 108, 76, 60],
|
||||
7 => [156, 124, 88, 66],
|
||||
8 => [194, 154, 110, 86],
|
||||
9 => [232, 182, 132, 100],
|
||||
10 => [274, 216, 154, 122],
|
||||
11 => [324, 254, 180, 140],
|
||||
12 => [370, 290, 206, 158],
|
||||
13 => [428, 334, 244, 180],
|
||||
14 => [461, 365, 261, 197],
|
||||
15 => [523, 415, 295, 223],
|
||||
16 => [589, 453, 325, 253],
|
||||
17 => [647, 507, 367, 283],
|
||||
18 => [721, 563, 397, 313],
|
||||
19 => [795, 627, 445, 341],
|
||||
20 => [861, 669, 485, 385],
|
||||
21 => [932, 714, 512, 406],
|
||||
22 => [1006, 782, 568, 442],
|
||||
23 => [1094, 860, 614, 464],
|
||||
24 => [1174, 914, 664, 514],
|
||||
25 => [1276, 1000, 718, 538],
|
||||
26 => [1370, 1062, 754, 596],
|
||||
27 => [1468, 1128, 808, 628],
|
||||
28 => [1531, 1193, 871, 661],
|
||||
29 => [1631, 1267, 911, 701],
|
||||
30 => [1735, 1373, 985, 745],
|
||||
31 => [1843, 1455, 1033, 793],
|
||||
32 => [1955, 1541, 1115, 845],
|
||||
33 => [2071, 1631, 1171, 901],
|
||||
34 => [2191, 1725, 1231, 961],
|
||||
35 => [2306, 1812, 1286, 986],
|
||||
36 => [2434, 1914, 1354, 1054],
|
||||
37 => [2566, 1992, 1426, 1096],
|
||||
38 => [2702, 2102, 1502, 1142],
|
||||
39 => [2812, 2216, 1582, 1222],
|
||||
40 => [2956, 2334, 1666, 1276],
|
||||
];
|
||||
|
||||
/**
|
||||
* Kapazitätstabelle für Zeichen in verschiedenen Modi
|
||||
* Dies sind Näherungswerte für die Anzahl der Zeichen
|
||||
*/
|
||||
private const CHARACTER_CAPACITY = [
|
||||
'numeric' => [ // Numerische Kapazität (nur Ziffern)
|
||||
1 => [41, 34, 27, 17],
|
||||
2 => [77, 63, 48, 34],
|
||||
5 => [255, 202, 144, 106],
|
||||
10 => [652, 513, 364, 288],
|
||||
20 => [2061, 1600, 1156, 909],
|
||||
30 => [4158, 3289, 2358, 1782],
|
||||
40 => [7089, 5596, 3993, 3057],
|
||||
],
|
||||
'alphanumeric' => [ // Alphanumerische Kapazität (Zahlen, Großbuchstaben, einige Symbole)
|
||||
1 => [25, 20, 16, 10],
|
||||
2 => [47, 38, 29, 20],
|
||||
5 => [154, 122, 87, 64],
|
||||
10 => [395, 311, 220, 174],
|
||||
20 => [1249, 969, 701, 551],
|
||||
30 => [2520, 1991, 1429, 1080],
|
||||
40 => [4296, 3391, 2420, 1852],
|
||||
],
|
||||
'byte' => [ // Byte-Kapazität (8-Bit-Bytes)
|
||||
1 => [17, 14, 11, 7],
|
||||
2 => [32, 26, 20, 14],
|
||||
5 => [106, 84, 60, 44],
|
||||
10 => [271, 213, 151, 119],
|
||||
20 => [858, 666, 482, 378],
|
||||
30 => [1732, 1370, 982, 742],
|
||||
40 => [2953, 2331, 1663, 1273],
|
||||
],
|
||||
'kanji' => [ // Kanji-Kapazität (Japanische Schriftzeichen)
|
||||
1 => [10, 8, 7, 4],
|
||||
2 => [20, 16, 12, 8],
|
||||
5 => [65, 52, 37, 27],
|
||||
10 => [167, 131, 93, 74],
|
||||
20 => [528, 410, 297, 232],
|
||||
30 => [1066, 841, 604, 457],
|
||||
40 => [1817, 1435, 1024, 784],
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(int $version)
|
||||
{
|
||||
if ($version < 1 || $version > 40) {
|
||||
throw new QrCodeException('QR Code Version muss zwischen 1 und 40 liegen');
|
||||
}
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
public function getValue(): int
|
||||
{
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
public function getSize(): int
|
||||
{
|
||||
return 21 + ($this->version - 1) * 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Datenbyte-Kapazität für das angegebene Error-Correction-Level
|
||||
*/
|
||||
public function getDataCapacity(ErrorCorrectionLevel $level): int
|
||||
{
|
||||
$index = match ($level) {
|
||||
ErrorCorrectionLevel::L => 0,
|
||||
ErrorCorrectionLevel::M => 1,
|
||||
ErrorCorrectionLevel::Q => 2,
|
||||
ErrorCorrectionLevel::H => 3,
|
||||
};
|
||||
|
||||
return self::DATA_CAPACITY[$this->version][$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Kapazität in Zeichen für einen bestimmten Kodierungsmodus zurück
|
||||
*/
|
||||
public function getCharacterCapacity(string $mode, ErrorCorrectionLevel $level): int
|
||||
{
|
||||
if (!isset(self::CHARACTER_CAPACITY[$mode])) {
|
||||
throw new QrCodeException("Unbekannter Kodierungsmodus: $mode");
|
||||
}
|
||||
|
||||
$index = match ($level) {
|
||||
ErrorCorrectionLevel::L => 0,
|
||||
ErrorCorrectionLevel::M => 1,
|
||||
ErrorCorrectionLevel::Q => 2,
|
||||
ErrorCorrectionLevel::H => 3,
|
||||
};
|
||||
|
||||
// Finde die nächste Version in der Kapazitätstabelle
|
||||
$capacityVersion = $this->version;
|
||||
while (!isset(self::CHARACTER_CAPACITY[$mode][$capacityVersion]) && $capacityVersion > 0) {
|
||||
$capacityVersion--;
|
||||
}
|
||||
|
||||
// Wenn keine passende Version gefunden wurde, verwende die niedrigste
|
||||
if ($capacityVersion === 0) {
|
||||
$capacityVersion = min(array_keys(self::CHARACTER_CAPACITY[$mode]));
|
||||
}
|
||||
|
||||
return self::CHARACTER_CAPACITY[$mode][$capacityVersion][$index];
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User\Migrations;
|
||||
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use App\Framework\Database\Migration\Migration;
|
||||
use App\Framework\Database\Migration\MigrationVersion;
|
||||
|
||||
final readonly class CreateUsersTable implements Migration
|
||||
{
|
||||
|
||||
public function up(ConnectionInterface $connection): void
|
||||
{
|
||||
$connection->query("CREATE TABLE IF NOT EXISTS users (
|
||||
$connection->query(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ulid BINARY(16) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL
|
||||
@@ -24,9 +26,9 @@ final readonly class CreateUsersTable implements Migration
|
||||
$connection->query("DROP TABLE users");
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
public function getVersion(): MigrationVersion
|
||||
{
|
||||
return "001";
|
||||
return MigrationVersion::fromTimestamp("2024_01_15_000001");
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\User;
|
||||
|
||||
use App\Framework\Database\Attributes\Column;
|
||||
@@ -11,7 +13,6 @@ final readonly class User
|
||||
public function __construct(
|
||||
#[Column(name: 'id', primary: true)]
|
||||
public string $id,
|
||||
|
||||
#[Column(name: 'name')]
|
||||
public string $name,
|
||||
|
||||
@@ -19,5 +20,6 @@ final readonly class User
|
||||
|
||||
/*#[Column(name: 'email')]
|
||||
public string $email*/
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ final readonly class EmailAddress
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
if (! filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new \InvalidArgumentException("Invalid email address: {$value}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user