loadEnvironment($basePath); // Check if we have encryption key for secrets $encryptionKey = $env->get('ENCRYPTION_KEY'); if ($encryptionKey === null) { // No encryption key, return environment as-is return $env; } try { // Second pass: Reload with encryption support for .env.secrets return $this->loadEnvironment($basePath, $encryptionKey); } catch (\Throwable $e) { // Fallback to environment without secrets if encryption fails error_log("Failed to load encrypted secrets: " . $e->getMessage()); error_log("Continuing with non-encrypted environment variables."); return $env; } } /** * Load environment from multiple files with encryption support * * Priority Strategy: * - Production: Docker ENV vars take precedence over .env files * - Development: .env files can override system environment */ public function loadEnvironment(FilePath|string $basePath, ?string $encryptionKey = null): Environment { // Start with system environment variables (Docker/OS) $systemVariables = $this->loadSystemEnvironment(); // Determine environment type from system variables first $appEnv = $systemVariables['APP_ENV'] ?? 'development'; $isProduction = $appEnv === 'production'; $baseDir = $basePath instanceof FilePath ? $basePath : FilePath::create($basePath); // For PRODUCTION: Docker ENV vars have highest priority // For DEVELOPMENT: .env files can override system env if ($isProduction) { // Production: System ENV → .env files (only if not set) $variables = $systemVariables; // Load .env files but DON'T override Docker ENV vars $envFile = $baseDir->join('.env'); if ($envFile->exists()) { $fileVariables = $this->parser->parse($envFile); // Only add variables that aren't already set by Docker foreach ($fileVariables as $key => $value) { if (!isset($variables[$key])) { $variables[$key] = $value; } } } } else { // Development: .env files → System ENV (local development workflow) $variables = $systemVariables; // Load base .env file (can override system env for development) $envFile = $baseDir->join('.env'); if ($envFile->exists()) { $variables = array_merge($variables, $this->parser->parse($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->parser->parse($secretsFile); $variables = array_merge($variables, $secretsVariables); } // Re-check APP_ENV after .env loading $appEnv = $variables['APP_ENV'] ?? 'development'; // Load environment-specific files $envSpecificFile = $baseDir->join(".env.{$appEnv}"); if ($envSpecificFile->exists()) { if ($isProduction) { // Production: Only add missing variables $envSpecificVariables = $this->parser->parse($envSpecificFile); foreach ($envSpecificVariables as $key => $value) { if (!isset($variables[$key])) { $variables[$key] = $value; } } } else { // Development: Allow override $variables = array_merge($variables, $this->parser->parse($envSpecificFile)); } } return new Environment($variables); } /** * Load variables from system environment (getenv(), $_ENV, $_SERVER) * * Priority order optimized for PHP-FPM compatibility: * 1. getenv() - Works in PHP-FPM, reads from actual process environment * 2. $_ENV - May be empty in PHP-FPM * 3. $_SERVER - May contain additional vars from web server */ private function loadSystemEnvironment(): array { $variables = []; // 1. Load from getenv() - THIS WORKS IN PHP-FPM! // In PHP-FPM, $_ENV and $_SERVER are empty by default. // getenv() reads from actual process environment, unlike $_ENV $allEnvVars = getenv(); if ($allEnvVars !== false) { foreach ($allEnvVars as $key => $value) { $variables[$key] = $value; } } // 2. Load from $_ENV (may be empty in PHP-FPM) foreach ($_ENV as $key => $value) { if (!isset($variables[$key])) { $variables[$key] = $value; } } // 3. Load from $_SERVER (may contain additional vars from web server) foreach ($_SERVER as $key => $value) { if (!isset($variables[$key]) && is_string($value)) { $variables[$key] = $value; } } return $variables; } /** * 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->parser->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; } }