path = $path ?: base_path('.env'); } /** * @return array */ public function read(): array { if (! is_file($this->path)) { throw new RuntimeException('Le fichier .env est introuvable.'); } $lines = preg_split('/\r\n|\r|\n/', file_get_contents($this->path)); $parsed = []; foreach ($lines as $line) { $trimmed = trim($line); if ($trimmed === '') { $parsed[] = ['type' => 'blank']; continue; } if (str_starts_with($trimmed, '#')) { $parsed[] = ['type' => 'comment', 'text' => ltrim(substr($trimmed, 1))]; continue; } if (! str_contains($trimmed, '=')) { $parsed[] = ['type' => 'raw', 'raw' => $line]; continue; } [$key, $rawValue] = explode('=', $trimmed, 2); $key = trim($key); $parsed[] = [ 'type' => 'var', 'key' => $key, 'value' => $this->unquote(trim($rawValue)), 'protected' => in_array($key, self::PROTECTED_KEYS, true), 'sensitive' => $this->isSensitive($key), ]; } return $parsed; } public function isSensitive(string $key): bool { return (bool) preg_match('/PASSWORD|SECRET|TOKEN|KEY/i', $key); } /** * Rewrites only the given keys' values, leaving every other line (order, * comments, blanks, untouched keys) exactly as-is. Protected keys are * silently ignored even if present in $updates — never trust the form. * * @param array $updates */ public function update(array $updates): void { foreach ($updates as $key => $value) { if (str_contains($value, "\n") || str_contains($value, "\r")) { throw new RuntimeException("La valeur de {$key} ne peut pas contenir de retour à la ligne."); } } if (! is_writable($this->path)) { throw new RuntimeException("Le fichier .env n'est pas accessible en écriture par le serveur web."); } $lines = preg_split('/\r\n|\r|\n/', file_get_contents($this->path)); $output = []; foreach ($lines as $line) { $trimmed = trim($line); $replaced = false; if ($trimmed !== '' && ! str_starts_with($trimmed, '#') && str_contains($trimmed, '=')) { [$key] = explode('=', $trimmed, 2); $key = trim($key); if (array_key_exists($key, $updates) && ! in_array($key, self::PROTECTED_KEYS, true)) { $output[] = $key.'='.$this->quote($updates[$key]); $replaced = true; } } if (! $replaced) { $output[] = $line; } } // Write to a temp file in the same directory first, then rename — // rename() is atomic, so a request reading .env mid-save never sees // a half-written file. $tmpPath = $this->path.'.tmp-'.bin2hex(random_bytes(4)); file_put_contents($tmpPath, implode(PHP_EOL, $output)); rename($tmpPath, $this->path); } protected function unquote(string $value): string { if (strlen($value) >= 2 && $value[0] === '"' && str_ends_with($value, '"')) { return stripcslashes(substr($value, 1, -1)); } if (strlen($value) >= 2 && $value[0] === "'" && str_ends_with($value, "'")) { return substr($value, 1, -1); } return $value; } protected function quote(string $value): string { if ($value === '' || preg_match('/[\s#"]/', $value)) { return '"'.addcslashes($value, '"\\').'"'; } return $value; } }