feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -6,7 +6,6 @@ namespace App\Framework\Config;
|
||||
|
||||
use App\Framework\Encryption\EncryptionFactory;
|
||||
use App\Framework\Filesystem\ValueObjects\FilePath;
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
|
||||
/**
|
||||
* Enhanced environment loader with encryption support
|
||||
@@ -14,124 +13,161 @@ use App\Framework\Random\RandomGenerator;
|
||||
final readonly class EncryptedEnvLoader
|
||||
{
|
||||
public function __construct(
|
||||
private EncryptionFactory $encryptionFactory,
|
||||
private RandomGenerator $randomGenerator
|
||||
) {
|
||||
private EncryptionFactory $encryptionFactory = new EncryptionFactory,
|
||||
private EnvFileParser $parser = new EnvFileParser()
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Load environment with automatic encryption support detection
|
||||
*
|
||||
* This method:
|
||||
* 1. Loads Docker ENV vars and .env files
|
||||
* 2. Checks for ENCRYPTION_KEY in loaded environment
|
||||
* 3. If found, reloads with encryption support for .env.secrets
|
||||
*
|
||||
* @param FilePath|string $basePath Base path to project root
|
||||
* @return Environment Loaded environment with all variables
|
||||
*/
|
||||
public function load(FilePath|string $basePath): Environment
|
||||
{
|
||||
// First pass: Load environment (Docker ENV + .env files)
|
||||
$env = $this->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
|
||||
{
|
||||
$variables = [];
|
||||
// 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);
|
||||
|
||||
// Load base .env file
|
||||
$envFile = $baseDir->join('.env');
|
||||
if ($envFile->exists()) {
|
||||
$variables = array_merge($variables, $this->parseEnvFile($envFile));
|
||||
// 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->parseEnvFile($secretsFile);
|
||||
$secretsVariables = $this->parser->parse($secretsFile);
|
||||
$variables = array_merge($variables, $secretsVariables);
|
||||
}
|
||||
|
||||
// Load environment-specific files
|
||||
// Priority: 1. Already loaded from .env file, 2. Default to development
|
||||
// Re-check APP_ENV after .env loading
|
||||
$appEnv = $variables['APP_ENV'] ?? 'development';
|
||||
|
||||
// Load environment-specific files
|
||||
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
|
||||
if ($envSpecificFile->exists()) {
|
||||
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
|
||||
$variables = array_merge($variables, $envSpecificVariables);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single .env file
|
||||
* 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 parseEnvFile(FilePath|string $filePath): array
|
||||
private function loadSystemEnvironment(): array
|
||||
{
|
||||
$variables = [];
|
||||
$file = $filePath instanceof FilePath ? $filePath : FilePath::create($filePath);
|
||||
|
||||
if (! $file->isReadable()) {
|
||||
return $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;
|
||||
}
|
||||
}
|
||||
|
||||
$lines = file($file->toString(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
return $variables;
|
||||
// 2. Load from $_ENV (may be empty in PHP-FPM)
|
||||
foreach ($_ENV as $key => $value) {
|
||||
if (!isset($variables[$key])) {
|
||||
$variables[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($lines as $lineNumber => $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip comments and empty lines
|
||||
if (str_starts_with($line, '#') || ! str_contains($line, '=')) {
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
@@ -218,7 +254,7 @@ final readonly class EncryptedEnvLoader
|
||||
$shouldEncrypt = empty($keysToEncrypt) ? str_starts_with($name, 'SECRET_') : in_array($name, $keysToEncrypt, true);
|
||||
|
||||
if ($shouldEncrypt && ! $encryption->isEncrypted($value)) {
|
||||
$value = $this->removeQuotes($value);
|
||||
$value = $this->parser->removeQuotes($value);
|
||||
$encryptedValue = $encryption->encrypt($value);
|
||||
$newLines[] = "{$name}={$encryptedValue}";
|
||||
$encryptedCount++;
|
||||
|
||||
Reference in New Issue
Block a user