- Update all() to automatically resolve Docker Secrets (*_FILE pattern) for empty variables - Ensures DB_PASSWORD, REDIS_PASSWORD, etc. are resolved from their *_FILE counterparts when empty - Variables like DB_PASSWORD_FILE are still included in output, but empty values are replaced with resolved secrets - Fixes issue where DB_PASSWORD and REDIS_PASSWORD appeared empty in logs even though *_FILE variables existed
284 lines
8.9 KiB
PHP
284 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Config;
|
|
|
|
use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
|
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
|
use BackedEnum;
|
|
|
|
final readonly class Environment
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $variables
|
|
*/
|
|
public function __construct(
|
|
private array $variables = [],
|
|
private DockerSecretsResolver $secretsResolver = new DockerSecretsResolver()
|
|
) {
|
|
}
|
|
|
|
public function get(EnvKey|string $key, mixed $default = null): mixed
|
|
{
|
|
$key = $this->keyToString($key);
|
|
|
|
// 1. Check if direct env var exists in internal array and is not empty
|
|
// Empty strings are treated as "not set" to allow Docker Secrets resolution
|
|
if (isset($this->variables[$key])) {
|
|
$value = $this->variables[$key];
|
|
// If value is not empty, return it (non-empty values take precedence)
|
|
if ($value !== '' && $value !== null) {
|
|
return $value;
|
|
}
|
|
// If value is empty, continue to check Docker Secrets as fallback
|
|
}
|
|
|
|
// 2. Docker Secrets support: Check for *_FILE pattern
|
|
// This allows Docker Secrets to override empty values
|
|
$secretValue = $this->secretsResolver->resolve($key, $this->variables);
|
|
if ($secretValue !== null) {
|
|
return $secretValue;
|
|
}
|
|
|
|
// 3. Fallback: Check system environment dynamically
|
|
// This handles cases where environment variables are set after Environment initialization
|
|
// (common in PHP-FPM where vars may be set during request processing)
|
|
$systemValue = $this->getFromSystemEnvironment($key);
|
|
if ($systemValue !== null && $systemValue !== '') {
|
|
return $systemValue;
|
|
}
|
|
|
|
// 4. If internal variable was set (even if empty), return it (to distinguish between "not set" and "empty")
|
|
// This preserves the original behavior: if variable is explicitly set to empty string, return it
|
|
if (isset($this->variables[$key])) {
|
|
return $this->variables[$key];
|
|
}
|
|
|
|
// 5. Return default
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Get variable from system environment as fallback
|
|
*
|
|
* This ensures we can access environment variables that were set
|
|
* after Environment initialization (e.g., by PHP-FPM/FastCGI)
|
|
*/
|
|
private function getFromSystemEnvironment(string $key): ?string
|
|
{
|
|
// Priority: $_ENV > $_SERVER > getenv()
|
|
// $_ENV and $_SERVER may contain dynamically set vars in PHP-FPM
|
|
|
|
if (isset($_ENV[$key]) && is_string($_ENV[$key])) {
|
|
return $_ENV[$key];
|
|
}
|
|
|
|
if (isset($_SERVER[$key]) && is_string($_SERVER[$key]) && !str_starts_with($key, 'HTTP_')) {
|
|
return $_SERVER[$key];
|
|
}
|
|
|
|
$value = getenv($key);
|
|
if ($value !== false && is_string($value)) {
|
|
return $value;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function getRequired(EnvKey|string $key): mixed
|
|
{
|
|
$key = $this->keyToString($key);
|
|
$value = $this->get($key);
|
|
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);
|
|
$value = $this->get($key, $default);
|
|
if (is_string($value)) {
|
|
return match (strtolower($value)) {
|
|
'true', '1', 'yes', 'on' => true,
|
|
'false', '0', 'no', 'off' => false,
|
|
default => $default
|
|
};
|
|
}
|
|
|
|
return (bool) $value;
|
|
}
|
|
|
|
public function getString(EnvKey|string $key, string $default = ''): string
|
|
{
|
|
$key = $this->keyToString($key);
|
|
|
|
return (string) $this->get($key, $default);
|
|
}
|
|
|
|
/** @param class-string<BackedEnum> $enumClass */
|
|
public function getEnum(EnvKey|string $key, string $enumClass, BackedEnum $default): BackedEnum
|
|
{
|
|
$key = $this->keyToString($key);
|
|
|
|
if (! $default instanceof $enumClass) {
|
|
throw new \InvalidArgumentException('Default value must be an instance of the enum class');
|
|
}
|
|
|
|
$value = $this->get($key);
|
|
|
|
if ($value === null) {
|
|
return $default;
|
|
}
|
|
|
|
return $enumClass::tryFrom($value) ?? $default;
|
|
}
|
|
|
|
public function has(EnvKey|string $key): bool
|
|
{
|
|
$key = $this->keyToString($key);
|
|
|
|
return $this->get($key) !== null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function all(bool $sorted = false): array
|
|
{
|
|
// Merge internal variables with system environment variables
|
|
// This ensures all available environment variables are returned,
|
|
// including those that became available after Environment initialization
|
|
// (e.g., set by PHP-FPM/FastCGI during request processing)
|
|
$systemVariables = $this->getSystemEnvironment();
|
|
|
|
// Merge: internal variables take precedence over system variables
|
|
// This ensures variables loaded from .env files or set during initialization
|
|
// take precedence over system environment variables
|
|
$all = array_merge($systemVariables, $this->variables);
|
|
|
|
// Resolve Docker Secrets for variables that are empty or not set
|
|
// This ensures that variables like DB_PASSWORD are resolved from their *_FILE counterparts
|
|
$resolved = [];
|
|
foreach ($all as $key => $value) {
|
|
// If variable is empty or not set, check for Docker Secret
|
|
if (empty($value) || $value === '' || $value === null) {
|
|
$secretValue = $this->secretsResolver->resolve($key, $all);
|
|
if ($secretValue !== null) {
|
|
$resolved[$key] = $secretValue;
|
|
continue;
|
|
}
|
|
}
|
|
// Include non-empty values and *_FILE variables
|
|
$resolved[$key] = $value;
|
|
}
|
|
|
|
if ($sorted) {
|
|
ksort($resolved);
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
|
|
/**
|
|
* Get all system environment variables dynamically
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function getSystemEnvironment(): array
|
|
{
|
|
$variables = [];
|
|
|
|
// Load from $_ENV (contains dynamically set vars in PHP-FPM)
|
|
foreach ($_ENV as $key => $value) {
|
|
if (is_string($key) && is_string($value) && !str_starts_with($key, 'HTTP_')) {
|
|
$variables[$key] = $value;
|
|
}
|
|
}
|
|
|
|
// Load from $_SERVER (may contain additional vars from web server)
|
|
foreach ($_SERVER as $key => $value) {
|
|
if (!isset($variables[$key]) &&
|
|
is_string($key) &&
|
|
is_string($value) &&
|
|
!str_starts_with($key, 'HTTP_') &&
|
|
!in_array($key, ['GATEWAY_INTERFACE', 'SERVER_SOFTWARE', 'SERVER_NAME', 'SERVER_ADDR',
|
|
'SERVER_PORT', 'REQUEST_URI', 'REQUEST_METHOD', 'QUERY_STRING',
|
|
'CONTENT_TYPE', 'CONTENT_LENGTH', 'SCRIPT_NAME', 'SCRIPT_FILENAME',
|
|
'PATH_INFO', 'FCGI_ROLE', 'REDIRECT_STATUS', 'REQUEST_TIME',
|
|
'REQUEST_TIME_FLOAT', 'DOCUMENT_ROOT', 'DOCUMENT_URI',
|
|
'REMOTE_ADDR', 'REMOTE_PORT', 'REMOTE_USER'], true)) {
|
|
$variables[$key] = $value;
|
|
}
|
|
}
|
|
|
|
// Load from getenv() as fallback
|
|
$allEnvVars = getenv();
|
|
if ($allEnvVars !== false) {
|
|
foreach ($allEnvVars as $key => $value) {
|
|
if (!isset($variables[$key])) {
|
|
$variables[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $variables;
|
|
}
|
|
|
|
/**
|
|
* Factory method für .env file loading
|
|
*/
|
|
public static function fromFile(FilePath|string $envPath): self
|
|
{
|
|
$parser = new EnvFileParser();
|
|
$variables = $parser->parse($envPath);
|
|
|
|
return new self($variables);
|
|
}
|
|
|
|
public function keyToString(EnvKey|string $key): string
|
|
{
|
|
if (is_string($key)) {
|
|
return $key;
|
|
}
|
|
|
|
return $key->value;
|
|
}
|
|
|
|
/**
|
|
* Für Tests
|
|
*/
|
|
public function withVariable(string $key, mixed $value): self
|
|
{
|
|
$variables = $this->variables;
|
|
$variables[$key] = $value;
|
|
|
|
return new self($variables);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $variables
|
|
*/
|
|
public function withVariables(array $variables): self
|
|
{
|
|
return new self(array_merge($this->variables, $variables));
|
|
}
|
|
}
|