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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -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) {

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\AI;

View File

@@ -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
) {}
) {
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\AI;

View File

@@ -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
) {}
) {
}
}

View File

@@ -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,
])
);
}
}

View File

@@ -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';

View File

@@ -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);
}

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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,
){}
) {
}
}

View File

@@ -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
{

View File

@@ -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

View File

@@ -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;

View File

@@ -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,
){}
) {
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\Media;
enum ImageFormat: string

View File

@@ -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
{
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\Media;

View File

@@ -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());

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Domain\Media;

View File

@@ -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);
}
}

View File

@@ -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),

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\Media;
enum ImageSize: string

View File

@@ -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,
){}
) {
}
}

View File

@@ -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);
}
}

View 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
);
}
}

View File

@@ -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];
}
}

View File

@@ -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
{

View File

@@ -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;
}
}

View File

@@ -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]);
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Domain\Media;
enum ImageVariantType: string

View File

@@ -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}";
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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";
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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
{

View File

@@ -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;
}

View File

@@ -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}}";
}
}

View File

@@ -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

View File

@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Domain\QrCode\Exception;
use Exception;
class QrCodeException extends Exception
{
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
];
}
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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');
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
},
};
}
}

View File

@@ -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];
}
}

View File

@@ -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

View File

@@ -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*/
) {}
) {
}
}

View File

@@ -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}");
}
}