feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\ModulePosition;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
/**
* QR Code Alignment Pattern Generator
*
* Generates 5x5 alignment patterns for Version 2+ QR Codes
* Alignment patterns help scanners correct for distortion
*
* Pattern structure (5x5):
* █████
* █░░░█
* █░█░█
* █░░░█
* █████
*/
final readonly class AlignmentPattern
{
/**
* Alignment pattern center positions by version
* ISO/IEC 18004:2015 Table E.1
*/
private const ALIGNMENT_PATTERN_LOCATIONS = [
1 => [], // No 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],
];
/**
* Apply all alignment patterns to matrix
*/
public static function apply(QrCodeMatrix $matrix): QrCodeMatrix
{
$version = $matrix->getVersion()->getVersionNumber();
if (!isset(self::ALIGNMENT_PATTERN_LOCATIONS[$version])) {
// No alignment patterns for this version (should not happen)
return $matrix;
}
$locations = self::ALIGNMENT_PATTERN_LOCATIONS[$version];
if (empty($locations)) {
// Version 1 has no alignment patterns
return $matrix;
}
// Generate alignment patterns at all valid positions
foreach ($locations as $row) {
foreach ($locations as $col) {
// Skip if this position overlaps with a finder pattern
if (self::overlapsWithFinderPattern($row, $col, $matrix->getSize())) {
continue;
}
$center = ModulePosition::at($row, $col);
$matrix = self::drawPattern($matrix, $center);
}
}
return $matrix;
}
/**
* Draw single 5x5 alignment pattern centered at position
*/
private static function drawPattern(QrCodeMatrix $matrix, ModulePosition $center): QrCodeMatrix
{
$centerRow = $center->row;
$centerCol = $center->column;
// 5x5 pattern centered at position
for ($dr = -2; $dr <= 2; $dr++) {
for ($dc = -2; $dc <= 2; $dc++) {
$row = $centerRow + $dr;
$col = $centerCol + $dc;
$position = ModulePosition::at($row, $col);
// Outer border (always dark)
if (abs($dr) === 2 || abs($dc) === 2) {
$matrix = $matrix->setModule($position, Module::dark());
}
// Center module (dark)
elseif ($dr === 0 && $dc === 0) {
$matrix = $matrix->setModule($position, Module::dark());
}
// Inner ring (light)
else {
$matrix = $matrix->setModule($position, Module::light());
}
}
}
return $matrix;
}
/**
* Check if alignment pattern position overlaps with finder pattern
*/
private static function overlapsWithFinderPattern(int $row, int $col, int $size): bool
{
// Finder patterns occupy 9x9 area (7x7 pattern + 1 module separator)
// Top-left finder pattern (0-8, 0-8)
if ($row <= 8 && $col <= 8) {
return true;
}
// Top-right finder pattern (0-8, size-9 to size-1)
if ($row <= 8 && $col >= $size - 8) {
return true;
}
// Bottom-left finder pattern (size-9 to size-1, 0-8)
if ($row >= $size - 8 && $col <= 8) {
return true;
}
return false;
}
/**
* Get alignment pattern center positions for version
*/
public static function getLocations(int $version): array
{
return self::ALIGNMENT_PATTERN_LOCATIONS[$version] ?? [];
}
}

View File

@@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
use App\Framework\QrCode\ReedSolomon\ReedSolomonEncoder;
/**
* QR Code Block Structure Manager
*
* Handles data block division and error correction code generation
* according to QR code specification
*/
final class BlockStructure
{
private ReedSolomonEncoder $encoder;
public function __construct()
{
$this->encoder = new ReedSolomonEncoder();
}
/**
* Create blocks from data codewords
*/
public function createBlocks(array $dataCodewords, int $version, string $errorLevel): array
{
$blockInfo = $this->getBlockInfo($version, $errorLevel);
$blocks = [];
$dataOffset = 0;
// Create Group 1 blocks (if any)
for ($i = 0; $i < $blockInfo['group1_blocks']; $i++) {
$blockData = array_slice(
$dataCodewords,
$dataOffset,
$blockInfo['group1_data_per_block']
);
$errorCodewords = $this->encoder->encode(
$blockData,
$blockInfo['error_correction_per_block']
);
$blocks[] = new QrCodeBlock(
$blockData,
array_slice($errorCodewords, count($blockData))
);
$dataOffset += $blockInfo['group1_data_per_block'];
}
// Create Group 2 blocks (if any)
for ($i = 0; $i < $blockInfo['group2_blocks']; $i++) {
$blockData = array_slice(
$dataCodewords,
$dataOffset,
$blockInfo['group2_data_per_block']
);
$errorCodewords = $this->encoder->encode(
$blockData,
$blockInfo['error_correction_per_block']
);
$blocks[] = new QrCodeBlock(
$blockData,
array_slice($errorCodewords, count($blockData))
);
$dataOffset += $blockInfo['group2_data_per_block'];
}
return $blocks;
}
/**
* Interleave data and error correction codewords from blocks
*/
public function interleaveBlocks(array $blocks): array
{
if (empty($blocks)) {
return [];
}
$result = [];
// Interleave data codewords
$maxDataLength = max(array_map(fn ($block) => $block->getDataLength(), $blocks));
for ($i = 0; $i < $maxDataLength; $i++) {
foreach ($blocks as $block) {
if ($i < $block->getDataLength()) {
$result[] = $block->dataCodewords[$i];
}
}
}
// Interleave error correction codewords
$maxEcLength = max(array_map(fn ($block) => $block->getErrorCorrectionLength(), $blocks));
for ($i = 0; $i < $maxEcLength; $i++) {
foreach ($blocks as $block) {
if ($i < $block->getErrorCorrectionLength()) {
$result[] = $block->errorCorrectionCodewords[$i];
}
}
}
return $result;
}
/**
* Get block structure information for version and error level
*/
private function getBlockInfo(int $version, string $errorLevel): array
{
// Block structure lookup table for QR versions 1-10 (simplified for now)
$blockStructures = [
1 => [
'L' => ['group1_blocks' => 1, 'group1_data_per_block' => 19, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 7],
'M' => ['group1_blocks' => 1, 'group1_data_per_block' => 16, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 10],
'Q' => ['group1_blocks' => 1, 'group1_data_per_block' => 13, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 13],
'H' => ['group1_blocks' => 1, 'group1_data_per_block' => 9, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 17],
],
2 => [
'L' => ['group1_blocks' => 1, 'group1_data_per_block' => 34, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 10],
'M' => ['group1_blocks' => 1, 'group1_data_per_block' => 28, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 16],
'Q' => ['group1_blocks' => 1, 'group1_data_per_block' => 22, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 22],
'H' => ['group1_blocks' => 1, 'group1_data_per_block' => 16, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 28],
],
3 => [
'L' => ['group1_blocks' => 1, 'group1_data_per_block' => 55, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 15],
'M' => ['group1_blocks' => 1, 'group1_data_per_block' => 44, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 26],
'Q' => ['group1_blocks' => 2, 'group1_data_per_block' => 17, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 18],
'H' => ['group1_blocks' => 2, 'group1_data_per_block' => 13, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 22],
],
4 => [
'L' => ['group1_blocks' => 1, 'group1_data_per_block' => 80, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 20],
'M' => ['group1_blocks' => 2, 'group1_data_per_block' => 32, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 18],
'Q' => ['group1_blocks' => 2, 'group1_data_per_block' => 24, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 26],
'H' => ['group1_blocks' => 4, 'group1_data_per_block' => 9, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 16],
],
5 => [
'L' => ['group1_blocks' => 1, 'group1_data_per_block' => 108, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 26],
'M' => ['group1_blocks' => 2, 'group1_data_per_block' => 43, 'group2_blocks' => 0, 'group2_data_per_block' => 0, 'error_correction_per_block' => 24],
'Q' => ['group1_blocks' => 2, 'group1_data_per_block' => 15, 'group2_blocks' => 2, 'group2_data_per_block' => 16, 'error_correction_per_block' => 18],
'H' => ['group1_blocks' => 2, 'group1_data_per_block' => 11, 'group2_blocks' => 2, 'group2_data_per_block' => 12, 'error_correction_per_block' => 22],
],
];
if (! isset($blockStructures[$version][$errorLevel])) {
// Default fallback for unsupported versions
return [
'group1_blocks' => 1,
'group1_data_per_block' => ReedSolomonEncoder::getDataCapacity($version, $errorLevel),
'group2_blocks' => 0,
'group2_data_per_block' => 0,
'error_correction_per_block' => ReedSolomonEncoder::getErrorCorrectionCodewords($version, $errorLevel),
];
}
return $blockStructures[$version][$errorLevel];
}
}

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\ModulePosition;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
/**
* QR Code Finder Pattern Generator
*
* Generates the three 7x7 finder patterns (position detection patterns)
* Located in three corners: top-left, top-right, bottom-left
*
* Pattern structure (7x7):
* █████████
* █░░░░░░█
* █░█████░█
* █░█░░░█░█
* █░█████░█
* █░░░░░░█
* █████████
*/
final readonly class FinderPattern
{
/**
* Apply all three finder patterns to matrix
*/
public static function apply(QrCodeMatrix $matrix): QrCodeMatrix
{
$size = $matrix->getSize();
// Top-left corner
$matrix = self::drawPattern($matrix, ModulePosition::at(0, 0));
// Top-right corner
$matrix = self::drawPattern($matrix, ModulePosition::at(0, $size - 7));
// Bottom-left corner
$matrix = self::drawPattern($matrix, ModulePosition::at($size - 7, 0));
return $matrix;
}
/**
* Draw single 7x7 finder pattern at position
*
* QR Code Finder Pattern (1:1:3:1:1 ratio):
* - Outer ring (1 module): DARK
* - White ring (1 module): LIGHT
* - Inner 3x3 center (3 modules): DARK
*/
private static function drawPattern(QrCodeMatrix $matrix, ModulePosition $topLeft): QrCodeMatrix
{
// 7x7 finder pattern with inner 3x3 dark, 1-module white ring, outer dark ring
for ($r = 0; $r < 7; $r++) {
for ($c = 0; $c < 7; $c++) {
$row = $topLeft->row + $r;
$col = $topLeft->column + $c;
$pos = ModulePosition::at($row, $col);
// Outer ring (r==0 || r==6 || c==0 || c==6) => DARK
if ($r === 0 || $r === 6 || $c === 0 || $c === 6) {
$matrix = $matrix->setModule($pos, Module::dark());
continue;
}
// White ring (r==1 || r==5 || c==1 || c==5) => LIGHT
if ($r === 1 || $r === 5 || $c === 1 || $c === 5) {
$matrix = $matrix->setModule($pos, Module::light());
continue;
}
// Inner 3x3 (r,c in 2..4) => DARK
$matrix = $matrix->setModule($pos, Module::dark());
}
}
return $matrix;
}
/**
* Add separator (1 module white border around finder pattern)
* The separator is the 8th row/column around the 7x7 finder pattern
*/
public static function applySeparators(QrCodeMatrix $matrix): QrCodeMatrix
{
$size = $matrix->getSize();
// Top-left separator (row 7 and column 7)
for ($i = 0; $i < 8; $i++) {
$matrix = $matrix->setModuleAt(7, $i, Module::light()); // Row 7
$matrix = $matrix->setModuleAt($i, 7, Module::light()); // Column 7
}
// Top-right separator (row 7 and column size-8)
for ($i = 0; $i < 8; $i++) {
$matrix = $matrix->setModuleAt(7, $size - 1 - $i, Module::light()); // Row 7
$matrix = $matrix->setModuleAt($i, $size - 8, Module::light()); // Column size-8
}
// Bottom-left separator (row size-8 and column 7)
for ($i = 0; $i < 8; $i++) {
$matrix = $matrix->setModuleAt($size - 8, $i, Module::light()); // Row size-8
$matrix = $matrix->setModuleAt($size - 1 - $i, 7, Module::light()); // Column 7
}
return $matrix;
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
use App\Framework\QrCode\ValueObjects\ErrorCorrectionLevel;
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\ModulePosition;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
/**
* QR Code Format Information
*
* Encodes error correction level and mask pattern into 15-bit format string
* Uses BCH(15,5) error correction
*/
final readonly class FormatInformation
{
// Format information with error correction for each EC level and mask pattern
// Pre-calculated to avoid BCH encoding at runtime
private const FORMAT_INFO_TABLE = [
'L' => [
0 => 0b111011111000100, // Level L, Mask 0
1 => 0b111001011110011,
2 => 0b111110110101010,
3 => 0b111100010011101,
4 => 0b110011000101111,
5 => 0b110001100011000,
6 => 0b110110001000001,
7 => 0b110100101110110,
],
'M' => [
0 => 0b101010000010010, // Level M, Mask 0
1 => 0b101000100100101,
2 => 0b101111001111100,
3 => 0b101101101001011,
4 => 0b100010111111001,
5 => 0b100000011001110,
6 => 0b100111110010111,
7 => 0b100101010100000,
],
'Q' => [
0 => 0b011010101011111, // Level Q, Mask 0
1 => 0b011000001101000,
2 => 0b011111100110001,
3 => 0b011101000000110,
4 => 0b010010010110100,
5 => 0b010000110000011,
6 => 0b010111011011010,
7 => 0b010101111101101,
],
'H' => [
0 => 0b001011010001001, // Level H, Mask 0
1 => 0b001001110111110,
2 => 0b001110011100111,
3 => 0b001100111010000,
4 => 0b000011101100010,
5 => 0b000001001010101,
6 => 0b000110100001100,
7 => 0b000100000111011,
],
];
/**
* Apply format information to matrix
*/
public static function apply(
QrCodeMatrix $matrix,
ErrorCorrectionLevel $level,
int $maskPattern
): QrCodeMatrix {
$formatBits = self::getFormatBits($level, $maskPattern);
// Apply format information in two locations
$matrix = self::applyFormatBitsHorizontal($matrix, $formatBits);
$matrix = self::applyFormatBitsVertical($matrix, $formatBits);
return $matrix;
}
/**
* Get 15-bit format information
*/
private static function getFormatBits(ErrorCorrectionLevel $level, int $maskPattern): int
{
$levelValue = $level->value;
if (!isset(self::FORMAT_INFO_TABLE[$levelValue][$maskPattern])) {
throw new \InvalidArgumentException(
"Invalid EC level ({$levelValue}) or mask pattern ({$maskPattern})"
);
}
return self::FORMAT_INFO_TABLE[$levelValue][$maskPattern];
}
/**
* Apply format bits horizontally (top-left to top-right)
*
* ISO/IEC 18004:2015 Section 7.9
* Bits are placed sequentially (0-14), but the COLUMN ORDER creates
* the appearance of bit swaps when read left-to-right
*/
private static function applyFormatBitsHorizontal(QrCodeMatrix $matrix, int $formatBits): QrCodeMatrix
{
$size = $matrix->getSize();
// Horizontal placement columns (size-dependent, non-sequential due to timing separator)
// First 8 bits: columns 0-5, 7-8 (skip timing column 6)
// Last 7 bits: columns from right edge backwards (size-1 down to size-7)
$columns = [
0, 1, 2, 3, 4, 5, 7, 8, // First 8 bits (left side)
$size - 1, $size - 2, $size - 3, $size - 4, $size - 5, $size - 6, $size - 7 // Last 7 bits (right side)
];
// Place bits SEQUENTIALLY - the column order creates the swap effect
for ($i = 0; $i < 15; $i++) {
$bit = ($formatBits >> (14 - $i)) & 1;
$module = $bit === 1 ? Module::dark() : Module::light();
$matrix = $matrix->setModuleAt(8, $columns[$i], $module);
}
return $matrix;
}
/**
* Apply format bits vertically (top-left to bottom-left)
*/
private static function applyFormatBitsVertical(QrCodeMatrix $matrix, int $formatBits): QrCodeMatrix
{
$size = $matrix->getSize();
// Format bits are placed MSB-first
// Bits 0-6: Column 8, rows from bottom (MSB first)
for ($i = 0; $i < 7; $i++) {
$bit = ($formatBits >> (14 - $i)) & 1;
$module = $bit === 1 ? Module::dark() : Module::light();
$matrix = $matrix->setModuleAt($size - 1 - $i, 8, $module);
}
// Bits 7-14: Column 8, rows 8 down to 0 (skip row 6 - timing)
// Rows 8, 7, 5, 4, 3, 2, 1, 0 (skip 6)
$rows = [8, 7, 5, 4, 3, 2, 1, 0];
for ($i = 0; $i < 8; $i++) {
$bit = ($formatBits >> (14 - (7 + $i))) & 1;
$module = $bit === 1 ? Module::dark() : Module::light();
$matrix = $matrix->setModuleAt($rows[$i], 8, $module);
}
return $matrix;
}
}

View File

@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
/**
* QR Code Data Block
*
* Represents a single data block with error correction codewords
*/
final readonly class QrCodeBlock
{
public function __construct(
public array $dataCodewords,
public array $errorCorrectionCodewords
) {
}
/**
* Get total codewords (data + error correction)
*/
public function getTotalCodewords(): array
{
return array_merge($this->dataCodewords, $this->errorCorrectionCodewords);
}
/**
* Get number of data codewords
*/
public function getDataLength(): int
{
return count($this->dataCodewords);
}
/**
* Get number of error correction codewords
*/
public function getErrorCorrectionLength(): int
{
return count($this->errorCorrectionCodewords);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\QrCode\Structure;
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\ModulePosition;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
/**
* QR Code Timing Pattern Generator
*
* Generates alternating dark/light modules between finder patterns
* Horizontal: Row 6, columns between finder patterns
* Vertical: Column 6, rows between finder patterns
*
* Used for coordinate detection and alignment
*/
final readonly class TimingPattern
{
/**
* Apply timing patterns to matrix
*/
public static function apply(QrCodeMatrix $matrix): QrCodeMatrix
{
$size = $matrix->getSize();
// Horizontal timing pattern (row 6)
for ($col = 8; $col < $size - 8; $col++) {
$module = ($col % 2 === 0) ? Module::dark() : Module::light();
$matrix = $matrix->setModule(
ModulePosition::at(6, $col),
$module
);
}
// Vertical timing pattern (column 6)
for ($row = 8; $row < $size - 8; $row++) {
$module = ($row % 2 === 0) ? Module::dark() : Module::light();
$matrix = $matrix->setModule(
ModulePosition::at($row, 6),
$module
);
}
return $matrix;
}
}