path !== '' ? $this->path : base_path('.env'); } public function read(): string { return File::exists($this->path()) ? File::get($this->path()) : ''; } /** * The first line that is neither blank, nor a comment, nor KEY=value — * or null when every line is one of those three things. 1-indexed, so it * can be shown to an operator directly ("Zeile 42"). */ public function firstInvalidLine(string $content): ?int { foreach (preg_split('/\r\n|\r|\n/', $content) as $number => $line) { if (! self::isValidLine($line)) { return $number + 1; } } return null; } public function isValid(string $content): bool { return $this->firstInvalidLine($content) === null; } /** * Validate, back up, then write — in that order, and never out of it. * Content that fails validation is never backed up (nothing is about to * change) and never written. * * @return string the backup path, so the caller can tell the operator * exactly where the previous version landed * * @throws InvalidEnvContentException the content is empty, or does not parse */ public function write(string $content): string { if (trim($content) === '') { throw InvalidEnvContentException::empty(); } $line = $this->firstInvalidLine($content); if ($line !== null) { throw InvalidEnvContentException::atLine($line); } $backup = $this->backup(); File::put($this->path(), $content); return $backup; } /** * A timestamped copy of the CURRENT file, beside it — before whatever * write() is about to do to the original. Microseconds in the name so two * saves a second apart (a real thing to hit while testing this class, not * only a hypothetical operator habit) do not collide and silently * overwrite one another. * * Returns '' — never a path — when there is nothing to back up, i.e. the * file does not exist yet. read() already treats a missing file as '' * rather than an error (Codex review, P2); write() has to agree, or the * very first save on a fresh installation would fail trying to copy a * file that was never there instead of simply creating one. */ public function backup(): string { if (! File::exists($this->path())) { return ''; } $target = $this->path().'.bak-'.now()->format('Ymd-His-u'); if (! File::copy($this->path(), $target)) { throw new \RuntimeException("Could not back up [{$this->path()}] before writing it."); } return $target; } private static function isValidLine(string $line): bool { $line = ltrim(rtrim($line, "\r\n")); if ($line === '' || str_starts_with($line, '#')) { return true; } return (bool) preg_match('/^[A-Za-z_][A-Za-z0-9_]*=.*$/', $line); } }