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,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\DateTime\Timezone;
@@ -11,8 +13,34 @@ final readonly class AppConfig
public string $version = '1.0.0',
public string $environment = 'production',
public bool $debug = false,
public Timezone $timezone = Timezone::DEFAULT,
public Timezone $timezone = Timezone::EuropeBerlin,
public string $locale = 'en',
public EnvironmentType $type = EnvironmentType::DEV
) {}
) {
}
public function isProduction(): bool
{
return $this->type->isProduction();
}
public function isDevelopment(): bool
{
return $this->type->isDevelopment();
}
public function isStaging(): bool
{
return $this->type->isStaging();
}
public function isProductionLike(): bool
{
return $this->type->isProductionLike();
}
public function isDebugEnabled(): bool
{
return $this->debug || $this->type->isDebugEnabled();
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
/**
* Lightweight boot-time configuration validator.
*
* Non-invasive: reports issues via error_log and returns a list of problems
* without throwing, so existing environments are not broken inadvertently.
*/
final readonly class ConfigValidator
{
public function __construct(private Environment $env)
{
}
/**
* Validate selected environment variables and return a list of issues.
* Each issue contains: key, issue, severity, recommendation.
*/
public function validate(): array
{
$issues = [];
// APP_ENV validation
$allowedEnvs = ['development', 'testing', 'production'];
$appEnv = $this->env->getString(EnvKey::APP_ENV, 'production');
if (!in_array($appEnv, $allowedEnvs, true)) {
$issues[] = [
'key' => 'APP_ENV',
'issue' => 'invalid_value',
'severity' => 'medium',
'recommendation' => 'Setze APP_ENV auf einen der Werte: development | testing | production',
];
}
// APP_DEBUG should be boolean-like if present
if ($this->env->has(EnvKey::APP_DEBUG)) {
$raw = $this->env->get(EnvKey::APP_DEBUG);
if (!is_bool($raw) && !is_string($raw)) {
$issues[] = [
'key' => 'APP_DEBUG',
'issue' => 'invalid_type',
'severity' => 'low',
'recommendation' => 'APP_DEBUG sollte true/false (oder "true"/"false") sein.',
];
}
}
// APP_PORT if present should be a valid TCP port
if ($this->env->has('APP_PORT')) {
$port = (int) $this->env->get('APP_PORT');
if ($port < 1 || $port > 65535) {
$issues[] = [
'key' => 'APP_PORT',
'issue' => 'out_of_range',
'severity' => 'medium',
'recommendation' => 'APP_PORT muss zwischen 1 und 65535 liegen.',
];
}
}
// Redis port if present
if ($this->env->has('REDIS_PORT')) {
$redisPort = (int) $this->env->get('REDIS_PORT');
if ($redisPort < 1 || $redisPort > 65535) {
$issues[] = [
'key' => 'REDIS_PORT',
'issue' => 'out_of_range',
'severity' => 'low',
'recommendation' => 'REDIS_PORT muss zwischen 1 und 65535 liegen.',
];
}
}
// Rate limit values if present: must be non-negative
foreach ([
'RATE_LIMIT_DEFAULT',
'RATE_LIMIT_WINDOW',
'RATE_LIMIT_AUTH',
'RATE_LIMIT_AUTH_WINDOW',
'RATE_LIMIT_API',
'RATE_LIMIT_API_WINDOW',
] as $rateKey) {
if ($this->env->has($rateKey)) {
$value = (int) $this->env->get($rateKey);
if ($value < 0) {
$issues[] = [
'key' => $rateKey,
'issue' => 'negative_value',
'severity' => 'low',
'recommendation' => $rateKey . ' sollte eine nicht-negative Ganzzahl sein.',
];
}
}
}
return $issues;
}
/**
* Validate and log issues. Returns the list of issues for optional handling.
*/
public function validateAndReport(): array
{
$issues = $this->validate();
foreach ($issues as $issue) {
$msg = sprintf(
'[CONFIG] key=%s issue=%s severity=%s recommendation=%s',
$issue['key'],
$issue['issue'],
$issue['severity'],
$issue['recommendation']
);
error_log($msg);
}
return $issues;
}
}

View File

@@ -1,117 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
class Configuration
{
private array $config = [];
/**
* Lädt Konfigurationen aus einer oder mehreren Quellen
*/
public function __construct(array $config = [])
{
$this->config = $config;
}
/**
* Lädt Konfigurationen aus PHP-Dateien
*/
public function loadFromFile(string $path): self
{
if (file_exists($path) && is_readable($path)) {
$fileConfig = require $path;
if (is_array($fileConfig)) {
$this->config = array_merge($this->config, $fileConfig);
}
}
return $this;
}
/**
* Lädt Konfigurationen aus einem Verzeichnis
*/
public function loadFromDirectory(string $directory): self
{
if (!is_dir($directory)) {
return $this;
}
foreach (glob($directory . '/*.php') as $file) {
$this->loadFromFile($file);
}
return $this;
}
/**
* Gibt einen Konfigurationswert zurück
*/
public function get(string $key, mixed $default = null): mixed
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $segment) {
if (!is_array($value) || !array_key_exists($segment, $value)) {
return $default;
}
$value = $value[$segment];
}
return $value;
}
/**
* Setzt einen Konfigurationswert
*/
public function set(string $key, mixed $value): self
{
$keys = explode('.', $key);
$config = &$this->config;
foreach ($keys as $i => $segment) {
if ($i === count($keys) - 1) {
$config[$segment] = $value;
break;
}
if (!isset($config[$segment]) || !is_array($config[$segment])) {
$config[$segment] = [];
}
$config = &$config[$segment];
}
return $this;
}
/**
* Prüft, ob ein Konfigurationswert existiert
*/
public function has(string $key): bool
{
$keys = explode('.', $key);
$config = $this->config;
foreach ($keys as $segment) {
if (!is_array($config) || !array_key_exists($segment, $config)) {
return false;
}
$config = $config[$segment];
}
return true;
}
/**
* Gibt alle Konfigurationswerte zurück
*/
public function all(): array
{
return $this->config;
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\Console;
use App\Framework\Config\EncryptedEnvLoader;
use App\Framework\Config\Environment;
use App\Framework\Config\SecretManager;
use App\Framework\Console\ConsoleColor;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ConsoleInput;
use App\Framework\Console\ConsoleOutput;
use App\Framework\Console\ExitCode;
use App\Framework\Encryption\EncryptionFactory;
use App\Framework\Random\RandomGenerator;
/**
* Console commands for secrets management
*/
final readonly class SecretsCommand
{
public function __construct(
private RandomGenerator $randomGenerator,
private EncryptionFactory $encryptionFactory,
private ?SecretManager $secretManager = null
) {
}
#[ConsoleCommand('secrets:generate-key', 'Generate a secure encryption key')]
public function generateKey(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$output->writeLine('🔐 Generating encryption key...', ConsoleColor::CYAN);
$output->newLine();
try {
$length = 32;
$keyBytes = $this->randomGenerator->bytes($length);
$encodedKey = base64_encode($keyBytes);
$availableMethods = $this->encryptionFactory->getAvailableMethods();
$recommendedMethod = $this->encryptionFactory->getRecommendedMethod();
$output->writeLine("Available methods: " . implode(', ', $availableMethods));
$output->writeLine("Using recommended method: {$recommendedMethod}", ConsoleColor::GREEN);
$output->newLine();
$output->writeSuccess('✅ Encryption key generated successfully!');
$output->newLine();
$output->writeLine('ENCRYPTION_KEY=' . $encodedKey, ConsoleColor::YELLOW);
$output->newLine();
$output->writeWarning('🚨 SECURITY WARNINGS:');
$output->writeLine('• Store this key securely - it cannot be recovered if lost');
$output->writeLine('• Add this to your .env file (never commit to version control)');
$output->writeLine('• Use different keys for different environments');
$output->newLine();
$output->writeInfo('📋 Next steps:');
$output->writeLine('1. Add the key to your .env file');
$output->writeLine('2. Create .env.secrets file: php console.php secrets:init');
$output->writeLine('3. Encrypt secrets: php console.php secrets:encrypt "secret-value"');
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to generate encryption key: ' . $e->getMessage());
return ExitCode::GENERAL_ERROR;
}
}
#[ConsoleCommand('secrets:encrypt', 'Encrypt a secret value')]
public function encrypt(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
if ($this->secretManager === null) {
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
return ExitCode::CONFIG_ERROR;
}
$value = $input->getArgument(0);
if (! $value) {
$value = $output->askPassword('Enter secret value to encrypt:');
}
if (empty($value)) {
$output->writeError('❌ No value provided to encrypt');
return ExitCode::CONFIG_ERROR;
}
try {
$encrypted = $this->secretManager->encryptSecret($value);
$output->writeSuccess('✅ Value encrypted successfully!');
$output->newLine();
$output->writeLine('Encrypted value:', ConsoleColor::CYAN);
$output->writeLine($encrypted, ConsoleColor::YELLOW);
$output->newLine();
$output->writeInfo('💡 Add this to your .env.secrets file like:');
$output->writeLine('SECRET_YOUR_KEY=' . $encrypted, ConsoleColor::GRAY);
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to encrypt value: ' . $e->getMessage());
return ExitCode::SOFTWARE_ERROR;
}
}
#[ConsoleCommand('secrets:decrypt', 'Decrypt a secret value (for debugging)')]
public function decrypt(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
if ($this->secretManager === null) {
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
return ExitCode::CONFIG_ERROR;
}
$encryptedValue = $input->getArgument(0);
if (! $encryptedValue) {
$encryptedValue = $output->askQuestion('Enter encrypted value to decrypt:');
}
if (empty($encryptedValue)) {
$output->writeError('❌ No encrypted value provided');
return ExitCode::INVALID_INPUT;
}
try {
if (! $this->secretManager->isEncrypted($encryptedValue)) {
$output->writeWarning('⚠️ Value does not appear to be encrypted');
return ExitCode::INVALID_INPUT;
}
$decrypted = $this->secretManager->getSecret('TEMP_DECRYPT', $encryptedValue);
$output->writeSuccess('✅ Value decrypted successfully!');
$output->newLine();
$output->writeLine('Decrypted value:', ConsoleColor::CYAN);
$output->writeLine($decrypted, ConsoleColor::YELLOW);
$output->newLine();
$output->writeWarning('🔒 This value should be kept secure!');
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to decrypt value: ' . $e->getMessage());
return ExitCode::SOFTWARE_ERROR;
}
}
#[ConsoleCommand('secrets:init', 'Initialize .env.secrets file')]
public function init(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$basePath = getcwd();
$secretsFile = $basePath . '/.env.secrets';
if (file_exists($secretsFile)) {
if (! $output->confirm('⚠️ .env.secrets already exists. Overwrite?', false)) {
$output->writeInfo('Operation cancelled');
return ExitCode::SUCCESS;
}
}
try {
$encryptedLoader = new EncryptedEnvLoader($this->encryptionFactory, $this->randomGenerator);
$filePath = $encryptedLoader->generateSecretsTemplate($basePath);
$output->writeSuccess('✅ .env.secrets template created!');
$output->writeLine("File created at: {$filePath}", ConsoleColor::GRAY);
$output->newLine();
$output->writeInfo('📋 Next steps:');
$output->writeLine('1. Edit .env.secrets and add your secret keys');
$output->writeLine('2. Encrypt values: php console.php secrets:encrypt "your-value"');
$output->writeLine('3. Add encrypted values to .env.secrets');
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to create .env.secrets template: ' . $e->getMessage());
return ExitCode::CANT_CREATE;
}
}
#[ConsoleCommand('secrets:list', 'List all secret keys')]
public function list(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
if ($this->secretManager === null) {
$output->writeError('❌ SecretManager not available. Make sure ENCRYPTION_KEY is set in .env');
return ExitCode::CONFIG_ERROR;
}
try {
$secretKeys = $this->secretManager->getSecretKeys();
if (empty($secretKeys)) {
$output->writeInfo(' No secret keys found');
return ExitCode::SUCCESS;
}
$output->writeLine('🔐 Found secret keys:', ConsoleColor::CYAN);
$output->newLine();
foreach ($secretKeys as $key) {
$output->writeLine("{$key}", ConsoleColor::WHITE);
}
$output->newLine();
$output->writeLine("Total: " . count($secretKeys) . " secrets", ConsoleColor::GRAY);
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
$output->writeError('❌ Failed to list secrets: ' . $e->getMessage());
return ExitCode::CONFIG_ERROR;
}
}
#[ConsoleCommand('secrets:validate', 'Validate secrets setup')]
public function validate(ConsoleInput $input, ConsoleOutput $output): ExitCode
{
$basePath = getcwd();
$encryptedLoader = new EncryptedEnvLoader($this->encryptionFactory, $this->randomGenerator);
$env = Environment::fromFile($basePath . '/.env');
$encryptionKey = $env->get('ENCRYPTION_KEY');
$output->writeLine('🔍 Validating secrets setup...', ConsoleColor::CYAN);
$output->newLine();
$issues = $encryptedLoader->validateEncryptionSetup($basePath, $encryptionKey);
if (empty($issues)) {
$output->writeSuccess('✅ Secrets setup is valid!');
if ($this->secretManager !== null) {
$context = $this->secretManager->getSecurityContext();
$output->newLine();
$output->writeInfo('Security context:');
$output->writeLine("• HTTPS: " . ($context->isHttps ? 'Yes' : 'No'));
$output->writeLine("• Encryption: {$context->encryptionMethod}");
$output->writeLine("• Server: {$context->serverName}");
$output->writeLine("• Risk Level: {$context->getRiskLevel()->getDisplayName()}");
$output->writeLine("• Summary: {$context->getSummary()}");
}
return ExitCode::SUCCESS;
}
$output->writeWarning('⚠️ Found issues with secrets setup:');
$output->newLine();
foreach ($issues as $issue) {
$color = match ($issue['severity']) {
'high' => ConsoleColor::RED,
'medium' => ConsoleColor::YELLOW,
default => ConsoleColor::WHITE
};
$output->writeLine("{$issue['message']}", $color);
}
return ExitCode::CONFIG_ERROR;
}
}

View File

@@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Core\AttributeMapper;
final readonly class DiscoveryConfig
{
/**
* Konstanten für Standardwerte
*/
public const int DEFAULT_CHUNK_SIZE = 100;
public const int DEFAULT_MAX_RETRIES = 3;
public const float DEFAULT_MEMORY_THRESHOLD_HIGH = 0.8;
public const float DEFAULT_MEMORY_THRESHOLD_CRITICAL = 0.9;
public const int DEFAULT_WORKER_COUNT = 4;
public const int DEFAULT_CACHE_TTL = 3600; // 1 hour
/**
* @param array<AttributeMapper> $attributeMappers
* @param array<class-string> $targetInterfaces
*/
public function __construct(
// Basis-Konfiguration
public bool $useCache = true,
public bool $showProgress = false,
public bool $useContextAwareInitializers = false,
public bool $enableCircuitBreaker = true,
public array $attributeMappers = [],
public array $targetInterfaces = [],
// Memory-Management-Konfiguration
public bool $enableAdaptiveChunking = true,
public int $chunkSize = self::DEFAULT_CHUNK_SIZE,
public float $memoryThresholdHigh = self::DEFAULT_MEMORY_THRESHOLD_HIGH,
public float $memoryThresholdCritical = self::DEFAULT_MEMORY_THRESHOLD_CRITICAL,
// Asynchrone Verarbeitung
public bool $enableAsyncProcessing = false,
public ?int $workerCount = null, // null = automatisch bestimmen
public bool $enableWorkloadBalancing = true,
// Fehlerbehandlung
public int $maxRetries = self::DEFAULT_MAX_RETRIES,
public string $logLevel = 'info', // 'debug', 'info', 'warning', 'error'
public bool $continueOnError = true,
// Cache-Validierung
public int $cacheTtl = self::DEFAULT_CACHE_TTL,
public bool $enableChecksumValidation = true,
public bool $enableCacheWarming = false,
) {
// Validiere Werte
$this->validateConfig();
}
/**
* Validiert die Konfigurationswerte
* @throws \InvalidArgumentException wenn ungültige Werte gefunden werden
*/
private function validateConfig(): void
{
// Validiere numerische Werte
if ($this->chunkSize < 1) {
throw new \InvalidArgumentException('Chunk size must be at least 1');
}
if ($this->memoryThresholdHigh <= 0 || $this->memoryThresholdHigh >= 1) {
throw new \InvalidArgumentException('Memory threshold high must be between 0 and 1');
}
if ($this->memoryThresholdCritical <= $this->memoryThresholdHigh || $this->memoryThresholdCritical >= 1) {
throw new \InvalidArgumentException('Memory threshold critical must be between memory threshold high and 1');
}
if ($this->workerCount !== null && $this->workerCount < 1) {
throw new \InvalidArgumentException('Worker count must be at least 1');
}
if ($this->maxRetries < 0) {
throw new \InvalidArgumentException('Max retries must be at least 0');
}
if ($this->cacheTtl < 0) {
throw new \InvalidArgumentException('Cache TTL must be at least 0');
}
// Validiere Enum-Werte
$validLogLevels = ['debug', 'info', 'warning', 'error'];
if (! in_array($this->logLevel, $validLogLevels, true)) {
throw new \InvalidArgumentException('Log level must be one of: ' . implode(', ', $validLogLevels));
}
}
/**
* Erstellt Konfiguration aus Umgebungsvariablen
*/
public static function fromEnvironment(Environment $env): self
{
return new self(
// Basis-Konfiguration
useCache: $env->getBool('DISCOVERY_USE_CACHE', true),
showProgress: $env->getBool('DISCOVERY_SHOW_PROGRESS', false),
useContextAwareInitializers: $env->getBool('ENABLE_CONTEXT_AWARE_INITIALIZERS', false),
enableCircuitBreaker: $env->getBool('DISCOVERY_ENABLE_CIRCUIT_BREAKER', true),
attributeMappers: [], // Will be set to defaults in DiscoveryServiceBootstrapper
targetInterfaces: [], // Will be set to defaults in DiscoveryServiceBootstrapper
// Memory-Management-Konfiguration
enableAdaptiveChunking: $env->getBool('DISCOVERY_ENABLE_ADAPTIVE_CHUNKING', true),
chunkSize: $env->getInt('DISCOVERY_CHUNK_SIZE', self::DEFAULT_CHUNK_SIZE),
memoryThresholdHigh: $env->getFloat('DISCOVERY_MEMORY_THRESHOLD_HIGH', self::DEFAULT_MEMORY_THRESHOLD_HIGH),
memoryThresholdCritical: $env->getFloat('DISCOVERY_MEMORY_THRESHOLD_CRITICAL', self::DEFAULT_MEMORY_THRESHOLD_CRITICAL),
// Asynchrone Verarbeitung
enableAsyncProcessing: $env->getBool('DISCOVERY_ENABLE_ASYNC', false),
workerCount: $env->has('DISCOVERY_WORKER_COUNT') ? $env->getInt('DISCOVERY_WORKER_COUNT') : null,
enableWorkloadBalancing: $env->getBool('DISCOVERY_ENABLE_WORKLOAD_BALANCING', true),
// Fehlerbehandlung
maxRetries: $env->getInt('DISCOVERY_MAX_RETRIES', self::DEFAULT_MAX_RETRIES),
logLevel: $env->getString('DISCOVERY_LOG_LEVEL', 'info'),
continueOnError: $env->getBool('DISCOVERY_CONTINUE_ON_ERROR', true),
// Cache-Validierung
cacheTtl: $env->getInt('DISCOVERY_CACHE_TTL', self::DEFAULT_CACHE_TTL),
enableChecksumValidation: $env->getBool('DISCOVERY_ENABLE_CHECKSUM_VALIDATION', true),
enableCacheWarming: $env->getBool('DISCOVERY_ENABLE_CACHE_WARMING', false)
);
}
/**
* Erstellt eine Entwicklungskonfiguration mit detailliertem Logging und Robustheit
*/
public static function development(): self
{
return new self(
// Basis-Konfiguration
useCache: true,
showProgress: true,
useContextAwareInitializers: true,
enableCircuitBreaker: true,
// Memory-Management-Konfiguration
enableAdaptiveChunking: true,
chunkSize: 50, // Kleinere Chunks für bessere Responsiveness
// Asynchrone Verarbeitung
enableAsyncProcessing: false, // Synchron für einfacheres Debugging
// Fehlerbehandlung
maxRetries: 3,
logLevel: 'debug', // Detailliertes Logging
continueOnError: true,
// Cache-Validierung
cacheTtl: 1800, // 30 Minuten (kürzere Zeit für Entwicklung)
enableChecksumValidation: true,
enableCacheWarming: false
);
}
/**
* Erstellt eine Produktionskonfiguration mit optimaler Performance
*/
public static function production(): self
{
return new self(
// Basis-Konfiguration
useCache: true,
showProgress: false,
useContextAwareInitializers: true,
enableCircuitBreaker: true,
// Memory-Management-Konfiguration
enableAdaptiveChunking: true,
chunkSize: 100,
// Asynchrone Verarbeitung
enableAsyncProcessing: true, // Asynchron für bessere Performance
workerCount: null, // Automatisch bestimmen
enableWorkloadBalancing: true,
// Fehlerbehandlung
maxRetries: 2,
logLevel: 'info',
continueOnError: true,
// Cache-Validierung
cacheTtl: 86400, // 24 Stunden
enableChecksumValidation: true,
enableCacheWarming: true
);
}
/**
* Erstellt eine Konfiguration für Umgebungen mit wenig Speicher
*/
public static function lowMemory(): self
{
return new self(
// Basis-Konfiguration
useCache: true,
showProgress: false,
useContextAwareInitializers: false, // Reduziert Speicherverbrauch
enableCircuitBreaker: true,
// Memory-Management-Konfiguration
enableAdaptiveChunking: true,
chunkSize: 25, // Sehr kleine Chunks
memoryThresholdHigh: 0.7, // Früher eingreifen
memoryThresholdCritical: 0.8,
// Asynchrone Verarbeitung
enableAsyncProcessing: false, // Synchron für weniger Speicherverbrauch
// Fehlerbehandlung
maxRetries: 1, // Minimale Retries
logLevel: 'warning', // Minimales Logging
continueOnError: true,
// Cache-Validierung
cacheTtl: 172800, // 48 Stunden (längere Cache-Zeit)
enableChecksumValidation: false, // Deaktiviert für weniger Speicherverbrauch
enableCacheWarming: false
);
}
/**
* Erstellt eine Konfiguration für Hochleistungsserver
*/
public static function highPerformance(): self
{
return new self(
// Basis-Konfiguration
useCache: true,
showProgress: false,
useContextAwareInitializers: true,
enableCircuitBreaker: true,
// Memory-Management-Konfiguration
enableAdaptiveChunking: true,
chunkSize: 200, // Größere Chunks
// Asynchrone Verarbeitung
enableAsyncProcessing: true,
workerCount: 8, // Feste Anzahl für optimale Parallelisierung
enableWorkloadBalancing: true,
// Fehlerbehandlung
maxRetries: 2,
logLevel: 'info',
continueOnError: true,
// Cache-Validierung
cacheTtl: 43200, // 12 Stunden
enableChecksumValidation: true,
enableCacheWarming: true
);
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Encryption\EncryptionFactory;
use App\Framework\Filesystem\FilePath;
use App\Framework\Random\RandomGenerator;
/**
* Enhanced environment loader with encryption support
*/
final readonly class EncryptedEnvLoader
{
public function __construct(
private EncryptionFactory $encryptionFactory,
private RandomGenerator $randomGenerator
) {
}
/**
* Load environment from multiple files with encryption support
*/
public function loadEnvironment(FilePath|string $basePath, ?string $encryptionKey = null): Environment
{
$variables = [];
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
// Load base .env file
$envFile = $baseDir->join('.env');
if ($envFile->exists()) {
$variables = array_merge($variables, $this->parseEnvFile($envFile));
}
// Load .env.secrets file if it exists and encryption key is provided
$secretsFile = $baseDir->join('.env.secrets');
if ($secretsFile->exists() && $encryptionKey !== null) {
$secretsVariables = $this->parseEnvFile($secretsFile);
$variables = array_merge($variables, $secretsVariables);
}
// Load environment-specific files
$appEnv = $variables['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'development';
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
if ($envSpecificFile->exists()) {
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
$variables = array_merge($variables, $envSpecificVariables);
}
return new Environment($variables);
}
/**
* Parse a single .env file
*/
private function parseEnvFile(FilePath|string $filePath): array
{
$variables = [];
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
if (! $file->isReadable()) {
return $variables;
}
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
return $variables;
}
foreach ($lines as $lineNumber => $line) {
$line = trim($line);
// Skip comments and empty lines
if (str_starts_with($line, '#') || ! str_contains($line, '=')) {
continue;
}
// Parse key=value pairs
$equalPos = strpos($line, '=');
if ($equalPos === false) {
continue;
}
$name = trim(substr($line, 0, $equalPos));
$value = trim(substr($line, $equalPos + 1));
// Remove surrounding quotes
$value = $this->removeQuotes($value);
// Handle multiline values (basic support)
if (str_ends_with($value, '\\')) {
$value = rtrim($value, '\\');
// In a full implementation, you'd continue reading the next line
}
$variables[$name] = $this->castValue($value);
}
return $variables;
}
/**
* Remove surrounding quotes from values
*/
private function removeQuotes(string $value): string
{
// Remove double quotes
if (str_starts_with($value, '"') && str_ends_with($value, '"') && strlen($value) > 1) {
return substr($value, 1, -1);
}
// Remove single quotes
if (str_starts_with($value, "'") && str_ends_with($value, "'") && strlen($value) > 1) {
return substr($value, 1, -1);
}
return $value;
}
/**
* Cast string values to appropriate types
*/
private function castValue(string $value): mixed
{
return match (strtolower($value)) {
'true' => true,
'false' => false,
'null' => null,
default => is_numeric($value) ? (str_contains($value, '.') ? (float) $value : (int) $value) : $value
};
}
/**
* Generate a .env.secrets template file
*/
public function generateSecretsTemplate(FilePath|string $basePath, array $secretKeys = []): FilePath
{
$template = "# .env.secrets - Encrypted secrets file\n";
$template .= "# Generated on " . date('Y-m-d H:i:s') . "\n";
$template .= "# \n";
$template .= "# Instructions:\n";
$template .= "# 1. Set ENCRYPTION_KEY in your main .env file\n";
$template .= "# 2. Use SecretManager::encryptSecret() to encrypt sensitive values\n";
$template .= "# 3. Store encrypted values in this file with ENC[...] format\n";
$template .= "# \n";
$template .= "# Example:\n";
$template .= "# SECRET_API_KEY=ENC[base64encodedencryptedvalue]\n";
$template .= "# SECRET_DATABASE_PASSWORD=ENC[anotherencryptedvalue]\n";
$template .= "\n";
// Add predefined secret keys
$defaultSecretKeys = [
'SECRET_ENCRYPTION_KEY',
'SECRET_DATABASE_PASSWORD',
'SECRET_API_KEY',
'SECRET_JWT_SECRET',
'SECRET_OAUTH_CLIENT_SECRET',
'SECRET_SMTP_PASSWORD',
'SECRET_REDIS_PASSWORD',
];
$allSecretKeys = array_unique(array_merge($defaultSecretKeys, $secretKeys));
foreach ($allSecretKeys as $key) {
$template .= "# {$key}=ENC[your_encrypted_value_here]\n";
}
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
$filePath = $baseDir->join('.env.secrets');
file_put_contents($filePath->toString(), $template);
return $filePath;
}
/**
* Encrypt secrets in an existing .env file
*/
public function encryptSecretsInFile(FilePath|string $filePath, string $encryptionKey, array $keysToEncrypt = []): int
{
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
if (! $file->exists() || ! $file->isReadable()) {
return 0;
}
$encryption = $this->encryptionFactory->createBest($encryptionKey);
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 0;
}
$encryptedCount = 0;
$newLines = [];
foreach ($lines as $line) {
$trimmedLine = trim($line);
// Skip comments and empty lines
if (str_starts_with($trimmedLine, '#') || ! str_contains($trimmedLine, '=')) {
$newLines[] = $line;
continue;
}
$equalPos = strpos($trimmedLine, '=');
if ($equalPos === false) {
$newLines[] = $line;
continue;
}
$name = trim(substr($trimmedLine, 0, $equalPos));
$value = trim(substr($trimmedLine, $equalPos + 1));
// Check if this key should be encrypted
$shouldEncrypt = empty($keysToEncrypt) ? str_starts_with($name, 'SECRET_') : in_array($name, $keysToEncrypt, true);
if ($shouldEncrypt && ! $encryption->isEncrypted($value)) {
$value = $this->removeQuotes($value);
$encryptedValue = $encryption->encrypt($value);
$newLines[] = "{$name}={$encryptedValue}";
$encryptedCount++;
} else {
$newLines[] = $line;
}
}
// Write back to file
file_put_contents($file->toString(), implode("\n", $newLines));
return $encryptedCount;
}
/**
* Validate encryption setup
*/
public function validateEncryptionSetup(FilePath|string $basePath, ?string $encryptionKey = null): array
{
$issues = [];
// Check if encryption key is provided
if ($encryptionKey === null) {
$issues[] = [
'type' => 'missing_key',
'message' => 'No encryption key provided',
'severity' => 'high',
];
return $issues;
}
// Check key strength
if (strlen($encryptionKey) < 32) {
$issues[] = [
'type' => 'weak_key',
'message' => 'Encryption key should be at least 32 characters',
'severity' => 'high',
];
}
// Check if .env.secrets exists
$baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath);
$secretsFile = $baseDir->join('.env.secrets');
if (! $secretsFile->exists()) {
$issues[] = [
'type' => 'missing_secrets_file',
'message' => '.env.secrets file not found',
'severity' => 'medium',
];
}
// Check encryption availability
$availableMethods = $this->encryptionFactory->getAvailableMethods();
if (! in_array('AES', $availableMethods, true)) {
$issues[] = [
'type' => 'weak_encryption',
'message' => 'OpenSSL not available, using basic encryption',
'severity' => 'medium',
];
}
return $issues;
}
}

View File

@@ -1,11 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
enum EnvKey: string
{
case APP_NAME = 'APP_NAME';
case APP_DEBUG = 'APP_DEBUG';
case APP_ENV = 'APP_ENV';
case APP_KEY = 'APP_KEY';
case APP_TIMEZONE = 'APP_TIMEZONE';
case APP_LOCALE = 'APP_LOCALE';
// Feature Flags
case ENABLE_CONTEXT_AWARE_INITIALIZERS = 'ENABLE_CONTEXT_AWARE_INITIALIZERS';
// Database
case DB_DRIVER = 'DB_DRIVER';
case DB_HOST = 'DB_HOST';
case DB_PORT = 'DB_PORT';
case DB_DATABASE = 'DB_DATABASE';
case DB_USERNAME = 'DB_USERNAME';
case DB_PASSWORD = 'DB_PASSWORD';
case DB_CHARSET = 'DB_CHARSET';
// External APIs
case SHOPIFY_WEBHOOK_SECRET = 'SHOPIFY_WEBHOOK_SECRET';
case RAPIDMAIL_USERNAME = 'RAPIDMAIL_USERNAME';
case RAPIDMAIL_PASSWORD = 'RAPIDMAIL_PASSWORD';
case RAPIDMAIL_TEST_MODE = 'RAPIDMAIL_TEST_MODE';
// ETag Configuration
case ETAG_ENABLED = 'ETAG_ENABLED';
case ETAG_PREFER_WEAK = 'ETAG_PREFER_WEAK';
case ETAG_MIDDLEWARE_ENABLED = 'ETAG_MIDDLEWARE_ENABLED';
case ETAG_EXCLUDE_PATHS = 'ETAG_EXCLUDE_PATHS';
case ETAG_EXCLUDE_CONTENT_TYPES = 'ETAG_EXCLUDE_CONTENT_TYPES';
}

View File

@@ -5,16 +5,19 @@ declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
use App\Framework\Filesystem\FilePath;
final readonly class Environment
{
public function __construct(
private array $variables = []
){}
) {
}
public function get(EnvKey|string $key, mixed $default = null): mixed
{
$key = $this->keyToString($key);
// Priorität: 1. System ENV, 2. Loaded variables, 3. Default
return $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key) ?: $this->variables[$key] ?? $default;
}
@@ -26,15 +29,24 @@ final readonly class Environment
if ($value === null) {
throw new RequiredEnvironmentVariableException($key);
}
return $value;
}
public function getInt(EnvKey|string $key, int $default = 0): int
{
$key = $this->keyToString($key);
return (int) $this->get($key, $default);
}
public function getFloat(EnvKey|string $key, float $default = 0.0): float
{
$key = $this->keyToString($key);
return (float) $this->get($key, $default);
}
public function getBool(EnvKey|string $key, bool $default = false): bool
{
$key = $this->keyToString($key);
@@ -46,18 +58,21 @@ final readonly class Environment
default => $default
};
}
return (bool) $value;
}
public function getString(EnvKey|string $key, string $default = ''): string
{
$key = $this->keyToString($key);
return (string) $this->get($key, $default);
}
public function getEnum(EnvKey|string $key, string $enumClass, \BackedEnum $default):object
public function getEnum(EnvKey|string $key, string $enumClass, \BackedEnum $default): object
{
$key = $this->keyToString($key);
return forward_static_call([$enumClass, 'tryFrom'], $this->get($key, $default));
#$enumClass::tryFrom($this->get($key, $default));
}
@@ -65,6 +80,7 @@ final readonly class Environment
public function has(EnvKey|string $key): bool
{
$key = $this->keyToString($key);
return $this->get($key) !== null;
}
@@ -80,15 +96,16 @@ final readonly class Environment
/**
* Factory method für .env file loading
*/
public static function fromFile(string $envPath): self
public static function fromFile(FilePath|string $envPath): self
{
$variables = [];
$filePath = $envPath instanceof FilePath ? $envPath : FilePath::create($envPath);
if (file_exists($envPath) && is_readable($envPath)) {
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($filePath->exists() && $filePath->isReadable()) {
$lines = file($filePath->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) {
if (str_starts_with(trim($line), '#') || ! str_contains($line, '=')) {
continue;
}
@@ -118,11 +135,12 @@ final readonly class Environment
};
}
public function keyToString(EnvKey|string $key):string
public function keyToString(EnvKey|string $key): string
{
if(is_string($key)) {
if (is_string($key)) {
return $key;
}
return $key->value;
}
@@ -133,6 +151,7 @@ final readonly class Environment
{
$variables = $this->variables;
$variables[$key] = $value;
return new self($variables);
}

View File

@@ -1,9 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
enum EnvironmentType: string
{
case DEV = 'development';
case STAGING = 'staging';
case PROD = 'production';
public function isProduction(): bool
{
return $this === self::PROD;
}
public function isDevelopment(): bool
{
return $this === self::DEV;
}
public function isStaging(): bool
{
return $this === self::STAGING;
}
public function isDebugEnabled(): bool
{
return $this === self::DEV;
}
public function isProductionLike(): bool
{
return $this === self::PROD || $this === self::STAGING;
}
public static function fromEnvironment(Environment $env): self
{
$envValue = $env->getString('APP_ENV', 'development');
return match(strtolower($envValue)) {
'production', 'prod' => self::PROD,
'staging', 'stage' => self::STAGING,
'development', 'dev', 'local' => self::DEV,
default => self::DEV
};
}
}

View File

@@ -1,17 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
class RequiredEnvironmentVariableException extends FrameworkException
{
/**
* @param string $key
*/
public function __construct(string $key)
{
parent::__construct("Required environment variable '$key' is not set.");
parent::__construct(
message: "Required environment variable '$key' is not set.",
context: ExceptionContext::forOperation('config_validation', 'Configuration')
->withData(['environment_variable' => $key])
);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\External;
use App\Framework\Config\Environment;
final readonly class ExternalApiConfig
{
public function __construct(
public ShopifyConfig $shopify,
public RapidMailConfig $rapidMail,
) {
}
public static function fromEnvironment(Environment $env): self
{
return new self(
shopify: ShopifyConfig::fromEnvironment($env),
rapidMail: RapidMailConfig::fromEnvironment($env),
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\External;
use App\Framework\Config\Environment;
use App\Framework\Config\EnvKey;
final readonly class RapidMailConfig
{
public function __construct(
public string $username,
public string $password,
public bool $testMode,
) {
}
public static function fromEnvironment(Environment $env): self
{
return new self(
username: $env->getRequired(EnvKey::RAPIDMAIL_USERNAME),
password: $env->getRequired(EnvKey::RAPIDMAIL_PASSWORD),
testMode: $env->getBool(EnvKey::RAPIDMAIL_TEST_MODE, true),
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\External;
use App\Framework\Config\Environment;
use App\Framework\Config\EnvKey;
final readonly class ShopifyConfig
{
public function __construct(
public string $webhookSecret,
) {
}
public static function fromEnvironment(Environment $env): self
{
return new self(
webhookSecret: $env->getRequired(EnvKey::SHOPIFY_WEBHOOK_SECRET),
);
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
use App\Framework\Config\ValueObjects\SecurityContext;
use App\Framework\Encryption\EncryptionInterface;
use App\Framework\Http\ServerEnvironment;
use App\Framework\Http\ServerKey;
use App\Framework\Random\RandomGenerator;
use App\Framework\UserAgent\UserAgent;
/**
* Secure secrets management using pluggable encryption
*/
final readonly class SecretManager
{
private const string SECRET_PREFIX = 'SECRET_';
public function __construct(
private Environment $environment,
private EncryptionInterface $encryption,
private ServerEnvironment $serverEnvironment,
private RandomGenerator $randomGenerator
) {
}
/**
* Get a secret value, automatically decrypting if necessary
*/
public function getSecret(EnvKey|string $key, mixed $default = null): mixed
{
$value = $this->environment->get($key, $default);
if ($value === null) {
return $default;
}
if (is_string($value) && $this->encryption->isEncrypted($value)) {
$this->auditSecretAccess($this->environment->keyToString($key), 'read');
return $this->encryption->decrypt($value);
}
return $value;
}
/**
* Get a required secret, throwing exception if not found
*/
public function getRequiredSecret(EnvKey|string $key): mixed
{
$value = $this->getSecret($key);
if ($value === null) {
$keyString = $this->environment->keyToString($key);
throw new RequiredEnvironmentVariableException($keyString);
}
return $value;
}
/**
* Encrypt a secret value for storage
*/
public function encryptSecret(string $value): string
{
return $this->encryption->encrypt($value);
}
/**
* Check if a value is encrypted
*/
public function isEncrypted(mixed $value): bool
{
return is_string($value) && $this->encryption->isEncrypted($value);
}
/**
* Get all secret keys (those starting with SECRET_PREFIX)
*/
public function getSecretKeys(): array
{
$allVars = $this->environment->all();
$secretKeys = [];
foreach (array_keys($allVars) as $key) {
if (str_starts_with($key, self::SECRET_PREFIX)) {
$secretKeys[] = $key;
}
}
return $secretKeys;
}
/**
* Rotate encryption for all secrets with new encryption instance
*/
public function rotateSecrets(EncryptionInterface $newEncryption): array
{
$secretKeys = $this->getSecretKeys();
$rotatedSecrets = [];
foreach ($secretKeys as $key) {
$currentValue = $this->environment->get($key);
if ($this->isEncrypted($currentValue)) {
// Decrypt with old encryption, encrypt with new encryption
$decrypted = $this->encryption->decrypt($currentValue);
$rotatedSecrets[$key] = $newEncryption->encrypt($decrypted);
$this->auditSecretAccess($key, 'rotate');
}
}
return $rotatedSecrets;
}
/**
* Generate a secure token for API keys, passwords, etc.
*/
public function generateSecureToken(int $length = 32): string
{
$bytes = $this->randomGenerator->bytes($length);
return bin2hex($bytes);
}
/**
* Generate a secure password with mixed character types
*/
public function generateSecurePassword(int $length = 16): string
{
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
$charactersLength = strlen($characters);
$password = '';
for ($i = 0; $i < $length; $i++) {
$randomIndex = $this->randomGenerator->int(0, $charactersLength - 1);
$password .= $characters[$randomIndex];
}
return $password;
}
/**
* Audit log for secret access (for compliance)
*/
public function auditSecretAccess(string $key, string $action = 'read'): void
{
$timestamp = date('Y-m-d H:i:s');
$user = $this->serverEnvironment->get(ServerKey::REMOTE_USER, 'unknown');
$clientIp = (string)$this->serverEnvironment->getClientIp();
$userAgent = $this->maskUserAgent($this->serverEnvironment->getUserAgent());
$logEntry = sprintf(
'[SECRETS] %s - Action: %s, Key: %s, User: %s, IP: %s, Method: %s, UA: %s',
$timestamp,
$action,
$key,
$user,
$clientIp,
$this->encryption->getMethod(),
$userAgent
);
// Log to system logs (in production, this should go to a secure audit system)
error_log($logEntry);
}
/**
* Mask sensitive values for logging
*/
public function maskSecret(string $secret): string
{
$length = strlen($secret);
if ($length <= 4) {
return str_repeat('*', $length);
}
return substr($secret, 0, 2) . str_repeat('*', $length - 4) . substr($secret, -2);
}
/**
* Mask user agent for privacy in logs
*/
private function maskUserAgent(UserAgent $userAgent): string
{
$userAgentString = $userAgent->value;
if (strlen($userAgentString) <= 20) {
return $userAgentString;
}
return substr($userAgentString, 0, 20) . '...';
}
/**
* Check if the current context is secure (HTTPS)
*/
public function isSecureContext(): bool
{
return $this->serverEnvironment->isHttps();
}
/**
* Get security context information
*/
public function getSecurityContext(): SecurityContext
{
return SecurityContext::create(
isHttps: $this->serverEnvironment->isHttps(),
clientIp: $this->serverEnvironment->getClientIp(),
maskedUserAgent: $this->maskUserAgent($this->serverEnvironment->getUserAgent()),
serverName: $this->serverEnvironment->getServerName(),
encryptionMethod: $this->encryption->getMethod()
);
}
/**
* Validate that secrets are properly encrypted in non-secure contexts
*/
public function validateSecretsForContext(): array
{
$issues = [];
$secretKeys = $this->getSecretKeys();
$isSecure = $this->isSecureContext();
foreach ($secretKeys as $key) {
$value = $this->environment->get($key);
if (! $this->isEncrypted($value)) {
$issues[] = [
'key' => $key,
'issue' => 'not_encrypted',
'severity' => $isSecure ? 'medium' : 'high',
'recommendation' => 'Encrypt this secret using SecretManager::encryptSecret()',
];
}
}
return $issues;
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
/**
* Security configuration that adapts to environment
*/
final readonly class SecurityConfig
{
public function __construct(
public string $appKey = '',
public bool $enableSecurityHeaders = true,
public bool $enableCsrfProtection = true,
public bool $enableRateLimiting = true,
public int $rateLimitPerMinute = 60,
public int $rateLimitBurstSize = 10,
public bool $logSecurityEvents = true,
public bool $enableStrictMode = false,
public array $allowedHosts = [],
public int $sessionLifetime = 3600,
public bool $secureSessionCookies = true,
public bool $httpOnlySessionCookies = true
) {
}
public static function forEnvironment(EnvironmentType $environment, Environment $env): self
{
return match($environment) {
EnvironmentType::PROD => self::production($env),
EnvironmentType::STAGING => self::staging($env),
EnvironmentType::DEV => self::development($env),
};
}
private static function production(Environment $env): self
{
return new self(
appKey: $env->getRequired(EnvKey::APP_KEY),
enableSecurityHeaders: true,
enableCsrfProtection: true,
enableRateLimiting: true,
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 30),
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 5),
logSecurityEvents: true,
enableStrictMode: true,
allowedHosts: $env->getString('SECURITY_ALLOWED_HOSTS', '') ?
explode(',', $env->getString('SECURITY_ALLOWED_HOSTS')) : [],
sessionLifetime: $env->getInt('SESSION_LIFETIME', 1800), // 30 minutes
secureSessionCookies: true,
httpOnlySessionCookies: true
);
}
private static function staging(Environment $env): self
{
return new self(
appKey: $env->getRequired(EnvKey::APP_KEY),
enableSecurityHeaders: true,
enableCsrfProtection: true,
enableRateLimiting: true,
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 60),
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 10),
logSecurityEvents: true,
enableStrictMode: true,
allowedHosts: $env->getString('SECURITY_ALLOWED_HOSTS', '') ?
explode(',', $env->getString('SECURITY_ALLOWED_HOSTS')) : [],
sessionLifetime: $env->getInt('SESSION_LIFETIME', 3600), // 1 hour
secureSessionCookies: true,
httpOnlySessionCookies: true
);
}
private static function development(Environment $env): self
{
return new self(
appKey: $env->getString(EnvKey::APP_KEY, 'dev-insecure-key-32-chars-long!!!'),
enableSecurityHeaders: true, // Security Headers IMMER aktiv
enableCsrfProtection: true, // CSRF IMMER aktiv für Sicherheit
enableRateLimiting: $env->getBool('SECURITY_ENABLE_RATE_LIMITING', false),
rateLimitPerMinute: $env->getInt('SECURITY_RATE_LIMIT_PER_MINUTE', 1000),
rateLimitBurstSize: $env->getInt('SECURITY_RATE_LIMIT_BURST', 100),
logSecurityEvents: $env->getBool('SECURITY_LOG_EVENTS', true),
enableStrictMode: false,
allowedHosts: [], // Allow all in development
sessionLifetime: $env->getInt('SESSION_LIFETIME', 86400), // 24 hours
secureSessionCookies: false, // Allow HTTP in development
httpOnlySessionCookies: true
);
}
}

View File

@@ -1,47 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Context\ContextType;
use App\Framework\Config\External\ExternalApiConfig;
use App\Framework\Database\Config\DatabaseConfig;
use App\Framework\Database\Config\DatabaseConfigInitializer;
use App\Framework\DateTime\Timezone;
use App\Framework\DI\Container;
use App\Framework\RateLimit\RateLimitConfig;
final readonly class TypedConfigInitializer
{
public function __construct(
private Environment $env,
){
#debug($env->getEnum('APP_ENV', EnvironmentType::class, EnvironmentType::PROD));
) {
}
public function __invoke(Container $container): TypedConfiguration
{
$databaseConfig = $this->createDatabaseConfig();
$appConfig = $this->createAppConfig();
$securityConfig = $this->createSecurityConfig($appConfig->type);
$rateLimitConfig = $this->createRateLimitConfig($securityConfig);
$externalApiConfig = ExternalApiConfig::fromEnvironment($this->env);
$discoveryConfig = DiscoveryConfig::fromEnvironment($this->env);
$container->instance(DatabaseConfig::class, $databaseConfig);
$container->instance(AppConfig::class, $appConfig);
$container->instance(SecurityConfig::class, $securityConfig);
$container->instance(RateLimitConfig::class, $rateLimitConfig);
$container->instance(ExternalApiConfig::class, $externalApiConfig);
$container->instance(DiscoveryConfig::class, $discoveryConfig);
// Validate environment configuration (non-fatal reporting)
$validator = new ConfigValidator($this->env);
$validator->validateAndReport();
return new TypedConfiguration(
database: $databaseConfig,
app: $appConfig,
security: $securityConfig,
rateLimit: $rateLimitConfig,
externalApis: $externalApiConfig,
discovery: $discoveryConfig,
);
}
private function createAppConfig(): AppConfig
{
$environmentType = EnvironmentType::fromEnvironment($this->env);
return new AppConfig(
name: $this->env->getString(EnvKey::APP_NAME, 'Framework App'),
version: $this->env->getString('APP_VERSION', '1.0.0'),
environment: $this->env->getString(
key: EnvKey::APP_ENV,
default: 'production'
),
debug: $this->env->getBool(EnvKey::APP_DEBUG),
debug: $this->env->getBool(EnvKey::APP_DEBUG, $environmentType->isDebugEnabled()),
timezone: $this->env->getEnum(
key: EnvKey::APP_TIMEZONE,
enumClass: Timezone::class,
default: Timezone::EuropeBerlin
),
locale: $this->env->getString(EnvKey::APP_LOCALE, 'de'),
type: $environmentType,
);
}
private function createSecurityConfig(EnvironmentType $environmentType): SecurityConfig
{
return SecurityConfig::forEnvironment($environmentType, $this->env);
}
private function createDatabaseConfig(): DatabaseConfig
{
$initializer = new DatabaseConfigInitializer($this->env);
return $initializer();
}
private function createRateLimitConfig(SecurityConfig $securityConfig): RateLimitConfig
{
return RateLimitConfig::fromSecurityConfig($securityConfig);
}
}

View File

@@ -1,15 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Config\External\ExternalApiConfig;
use App\Framework\Database\Config\DatabaseConfig;
use App\Framework\RateLimit\RateLimitConfig;
final readonly class TypedConfiguration
{
public function __construct(
#public DatabaseConfig $database,
public DatabaseConfig $database,
public AppConfig $app,
)
{
public SecurityConfig $security,
public RateLimitConfig $rateLimit,
public ExternalApiConfig $externalApis,
public DiscoveryConfig $discovery,
) {
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\ValueObjects;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Http\IpAddress;
/**
* Value Object representing security context information
* Used for auditing, logging, and security validation
*/
final readonly class SecurityContext
{
public function __construct(
public bool $isHttps,
public IpAddress $clientIp,
public string $maskedUserAgent,
public string $serverName,
public string $encryptionMethod,
public Timestamp $timestamp
) {
}
/**
* Create from server environment and encryption context
*/
public static function create(
bool $isHttps,
IpAddress $clientIp,
string $maskedUserAgent,
string $serverName,
string $encryptionMethod
): self {
return new self(
isHttps: $isHttps,
clientIp: $clientIp,
maskedUserAgent: $maskedUserAgent,
serverName: $serverName,
encryptionMethod: $encryptionMethod,
timestamp: Timestamp::fromFloat(microtime(true))
);
}
/**
* Check if the context is considered secure
*/
public function isSecure(): bool
{
return $this->isHttps;
}
/**
* Get security risk level based on context
*/
public function getRiskLevel(): SecurityRiskLevel
{
if (! $this->isHttps) {
return SecurityRiskLevel::HIGH;
}
if ($this->clientIp->isPrivate()) {
return SecurityRiskLevel::LOW;
}
return SecurityRiskLevel::MEDIUM;
}
/**
* Get context summary for logging
*/
public function getSummary(): string
{
$protocol = $this->isHttps ? 'HTTPS' : 'HTTP';
return sprintf(
'%s from %s via %s (%s)',
$protocol,
$this->clientIp->toString(),
$this->serverName,
$this->encryptionMethod
);
}
/**
* Convert to array for logging/serialization
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'is_https' => $this->isHttps,
'client_ip' => $this->clientIp->toString(),
'user_agent' => $this->maskedUserAgent,
'server_name' => $this->serverName,
'encryption_method' => $this->encryptionMethod,
'timestamp' => $this->timestamp->toDateTimeString(),
'risk_level' => $this->getRiskLevel()->value,
'summary' => $this->getSummary(),
];
}
/**
* Check if context is suitable for handling secrets
*/
public function isSecretHandlingSafe(): bool
{
return $this->isHttps && $this->getRiskLevel() !== SecurityRiskLevel::HIGH;
}
/**
* String representation
*/
public function __toString(): string
{
return $this->getSummary();
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config\ValueObjects;
/**
* Enum representing security risk levels
*/
enum SecurityRiskLevel: string
{
case LOW = 'low';
case MEDIUM = 'medium';
case HIGH = 'high';
case CRITICAL = 'critical';
/**
* Get human-readable name
*/
public function getDisplayName(): string
{
return match ($this) {
self::LOW => 'Low Risk',
self::MEDIUM => 'Medium Risk',
self::HIGH => 'High Risk',
self::CRITICAL => 'Critical Risk',
};
}
/**
* Check if this risk level requires immediate action
*/
public function requiresImmediateAction(): bool
{
return match ($this) {
self::LOW, self::MEDIUM => false,
self::HIGH, self::CRITICAL => true,
};
}
/**
* Get numeric priority for sorting
*/
public function getPriority(): int
{
return match ($this) {
self::LOW => 1,
self::MEDIUM => 2,
self::HIGH => 3,
self::CRITICAL => 4,
};
}
}

View File

@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace App\Framework\Config;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DDoS\DDoSConfig;
use App\Framework\Waf\ValueObjects\LayerConfig;
/**
* Web Application Firewall Configuration
*
* Central configuration for all WAF components including DDoS protection,
* bot detection, input validation, and layer management.
*/
final readonly class WafConfig
{
public Duration $globalTimeout;
public function __construct(
?Duration $globalTimeout = null,
public bool $enabled = true,
public bool $blockingMode = true,
public bool $logDetections = true,
public bool $enableDdosProtection = true,
public bool $enableBotProtection = true,
public bool $enableInputValidation = true,
public bool $enableRateLimiting = true,
public int $maxLayersPerRequest = 10,
public float $globalConfidenceThreshold = 70.0,
/** @var string[] */
public array $trustedIps = ['127.0.0.1', '::1'],
/** @var string[] */
public array $exemptPaths = ['/health', '/ping', '/metrics'],
/** @var string[] */
public array $enabledLayers = [
'input_validation',
'sql_injection',
'xss_protection',
'bot_protection',
'ddos_protection',
'rate_limiting',
'geo_blocking',
]
) {
$this->globalTimeout = $globalTimeout ?? Duration::fromMilliseconds(200);
}
/**
* Create production configuration (strict security)
*/
public static function production(): self
{
return new self(
globalTimeout : Duration::fromMilliseconds(200),
enabled : true,
blockingMode : true,
logDetections : true,
enableDdosProtection : true,
enableBotProtection : true,
enableInputValidation : true,
enableRateLimiting : true,
maxLayersPerRequest : 8, // Stricter timeout for production
globalConfidenceThreshold: 85.0,
trustedIps : [], // No trusted IPs in production
exemptPaths : ['/health'], // Minimal exempt paths
enabledLayers : [
'input_validation',
'sql_injection',
'xss_protection',
'bot_protection',
'ddos_protection',
'rate_limiting',
'geo_blocking',
]
);
}
/**
* Create development configuration (permissive)
*/
public static function development(): self
{
return new self(
globalTimeout : Duration::fromMilliseconds(1000),
enabled : true, // Log only in development
blockingMode : false,
logDetections : true, // Disable DDoS protection in dev
enableDdosProtection : false, // Allow development tools
enableBotProtection : false,
enableInputValidation : true, // No rate limiting in dev
enableRateLimiting : false,
maxLayersPerRequest : 15, // More lenient for development
globalConfidenceThreshold: 50.0,
trustedIps : ['127.0.0.1', '::1', '192.168.0.0/16', '10.0.0.0/8'],
exemptPaths : ['/health', '/ping', '/metrics', '/debug', '/api/test'],
enabledLayers : [
'input_validation',
'sql_injection',
'xss_protection',
]
);
}
/**
* Create testing configuration (minimal protection)
*/
public static function testing(): self
{
return new self(
globalTimeout : Duration::fromMilliseconds(5000), // Disable WAF in tests
enabled : false,
blockingMode : false,
logDetections : false,
enableDdosProtection : false,
enableBotProtection : false,
enableInputValidation : false,
enableRateLimiting : false,
maxLayersPerRequest : 5, // Very lenient for testing
globalConfidenceThreshold: 90.0,
trustedIps : ['127.0.0.1', '::1'],
exemptPaths : ['*'], // Exempt all paths in testing
enabledLayers : []
);
}
/**
* Get DDoS configuration environment type
*
* Returns a string indicating which DDoSConfig environment to use,
* breaking the direct dependency on DDoSConfig class
*/
public function getDdosConfigEnvironment(): string
{
if (! $this->enableDdosProtection) {
return 'testing'; // Disabled configuration
}
// Determine environment based on configuration
if ($this->blockingMode && $this->globalConfidenceThreshold >= 80.0) {
return 'production';
}
return 'development';
}
/**
* Get DDoS configuration
*
* @deprecated Use getDdosConfigEnvironment() instead to avoid cyclic dependencies
*/
public function getDdosConfig(): DDoSConfig
{
$environment = $this->getDdosConfigEnvironment();
return match($environment) {
'production' => DDoSConfig::production(),
'testing' => DDoSConfig::testing(),
default => DDoSConfig::development()
};
}
/**
* Get default layer configuration
*/
public function getDefaultLayerConfig(): LayerConfig
{
return match (true) {
$this->blockingMode && $this->globalConfidenceThreshold >= 80.0 => LayerConfig::production(),
! $this->blockingMode => LayerConfig::development(),
default => LayerConfig::default()
};
}
/**
* Check if layer is enabled
*/
public function isLayerEnabled(string $layerName): bool
{
return $this->enabled && in_array($layerName, $this->enabledLayers, true);
}
/**
* Check if IP is trusted
*/
public function isTrustedIp(string $ip): bool
{
foreach ($this->trustedIps as $trustedIp) {
if ($this->ipMatches($ip, $trustedIp)) {
return true;
}
}
return false;
}
/**
* Check if path is exempt
*/
public function isExemptPath(string $path): bool
{
foreach ($this->exemptPaths as $exemptPath) {
if ($exemptPath === '*' || str_starts_with($path, $exemptPath)) {
return true;
}
}
return false;
}
/**
* Check if IP matches pattern (supports CIDR)
*/
private function ipMatches(string $ip, string $pattern): bool
{
// Exact match
if ($ip === $pattern) {
return true;
}
// CIDR match
if (str_contains($pattern, '/')) {
[$network, $maskBits] = explode('/', $pattern, 2);
$maskBits = (int) $maskBits;
// IPv4 CIDR
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ipLong = ip2long($ip);
$networkLong = ip2long($network);
$mask = -1 << (32 - $maskBits);
return ($ipLong & $mask) === ($networkLong & $mask);
}
// IPv6 CIDR (simplified)
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// For IPv6, we'd need more complex subnet matching
// For now, just do exact match for IPv6
return $ip === $network;
}
}
return false;
}
/**
* Get configuration for environment detection
*/
public function getEnvironmentType(): string
{
return match (true) {
! $this->enabled => 'testing',
! $this->blockingMode => 'development',
$this->globalConfidenceThreshold >= 80.0 => 'production',
default => 'development'
};
}
/**
* Create a new instance with updated global timeout
*/
public function withGlobalTimeout(Duration $globalTimeout): self
{
return new self(
globalTimeout : $globalTimeout,
enabled : $this->enabled,
blockingMode : $this->blockingMode,
logDetections : $this->logDetections,
enableDdosProtection : $this->enableDdosProtection,
enableBotProtection : $this->enableBotProtection,
enableInputValidation : $this->enableInputValidation,
enableRateLimiting : $this->enableRateLimiting,
maxLayersPerRequest : $this->maxLayersPerRequest,
globalConfidenceThreshold: $this->globalConfidenceThreshold,
trustedIps : $this->trustedIps,
exemptPaths : $this->exemptPaths,
enabledLayers : $this->enabledLayers
);
}
/**
* Convert to array for serialization
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'enabled' => $this->enabled,
'blocking_mode' => $this->blockingMode,
'log_detections' => $this->logDetections,
'environment' => $this->getEnvironmentType(),
'components' => [
'ddos_protection' => $this->enableDdosProtection,
'bot_protection' => $this->enableBotProtection,
'input_validation' => $this->enableInputValidation,
'rate_limiting' => $this->enableRateLimiting,
],
'limits' => [
'max_layers_per_request' => $this->maxLayersPerRequest,
'global_confidence_threshold' => $this->globalConfidenceThreshold,
'global_timeout_ms' => $this->globalTimeout->toMilliseconds(),
],
'security' => [
'trusted_ips_count' => count($this->trustedIps),
'exempt_paths_count' => count($this->exemptPaths),
'enabled_layers_count' => count($this->enabledLayers),
],
];
}
}