From 705e58c81323007a36a599a57f657f814f4787dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Fri, 24 Jul 2026 23:38:32 +0200 Subject: [PATCH 1/8] WIP --- src/mako/env/DotenvLoader.php | 231 +++++++++++++++++++++++ src/mako/env/exceptions/EnvException.php | 18 ++ 2 files changed, 249 insertions(+) create mode 100644 src/mako/env/DotenvLoader.php create mode 100644 src/mako/env/exceptions/EnvException.php diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php new file mode 100644 index 000000000..13f90c85d --- /dev/null +++ b/src/mako/env/DotenvLoader.php @@ -0,0 +1,231 @@ + 0 && ctype_space($value[$i - 1])) { + return rtrim(substr($value, 0, $i)); + } + } + + return $value; + } + + /** + * Expand variables in value. + */ + private static function expandVariables(string $value): string + { + return preg_replace_callback( + '/(\\\\*)\$\{([A-Z_][A-Z0-9_]*)\}/i', + static function (array $matches): string { + $slashes = $matches[1]; + $key = $matches[2]; + + // Odd number of backslashes escapes the variable + + if ((strlen($slashes) % 2) === 1) { + return substr($slashes, 1) . "\${{$key}}"; + } + + // Even number of backslashes, we'll expand the variable + + return $slashes . (env($key) ?? throw new EnvException(sprintf('Unknown environment variable [ $%s ].', $key))); + }, + $value + ) ?? $value; + } + + /** + * Unescape values. + */ + private static function unescape(string $value): string + { + return str_replace( + ['\\n', '\\r', '\\t', '\\"', '\\\\'], + ["\n", "\r", "\t", '"', '\\'], + $value + ); + } + + /** + * Parse value. + */ + private static function parseValue(string $value, int $lineNumber, bool $expandVariables): string + { + // Remove inline comments + + if (strpos($value, '#') !== false) { + $value = self::stripInlineComment($value); + } + + if ($value === '') { + return ''; + } + + // Get first and last character + + $first = $value[0]; + $last = $value[strlen($value) - 1]; + + // Quoted value + + if ($first === '"' || $first === "'") { + if ($last !== $first) { + throw new EnvException(sprintf('Unterminated quoted value on line [ %s ]', $lineNumber)); + } + + $value = substr($value, 1, -1); + + if ($first === '"') { + if ($expandVariables && strpos($value, '${') !== false) { + $value = self::expandVariables($value); + } + + $value = self::unescape($value); + } + + return $value; + } + + // Unquoted value so we'll just trim it and return + + return trim($value); + } + + /** + * Loads the file into environment variables. + */ + public static function load(string $filePath, bool $overrideExisting = false, bool $expandVariables = false, bool $usePutEnv = false): void + { + $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if ($lines === false) { + throw new EnvException(sprintf('Failed to read env file [ %s ].', $filePath)); + } + + foreach ($lines as $key => $line) { + $lineNumber = $key + 1; + + // Check for NUL byte + + if (str_contains($line, "\0")) { + throw new EnvException(sprintf('NUL byte detected in [ %s ] on line [ %s ].', $filePath, $lineNumber)); + } + + // Strip UTF-8 BOM from first line + + if ($lineNumber === 1 && str_starts_with($line, "\xEF\xBB\xBF")) { + $line = substr($line, 3); + } + + $line = trim($line); + + // Skip empty lines and lines that are comments + + if ($line === '' || str_starts_with($line, '#')) { + continue; + } + + // Strip "export" line prefix + + if (str_starts_with($line, 'export ')) { + $line = trim(substr($line, 7)); + } + + // Split key and value + + $pos = strpos($line, '='); + + if ($pos === false) { + throw new EnvException(sprintf('Invalid env declaration in [ %s ] on line [ %s ].', $filePath, $lineNumber)); + } + + $key = trim(substr($line, 0, $pos)); + $value = trim(substr($line, $pos + 1)); + + if ($key === '' || !preg_match('/^(?:[A-Z][A-Z0-9_]*|_[A-Z0-9_]+)$/i', $key)) { + throw new EnvException(sprintf('Invalid key [ %s ] in [ %s ] on line [ %s ].', ($key ?: 'empty'), $filePath, $lineNumber)); + } + + // Should we skip overriding existing variables? + + if ($overrideExisting === false && (array_key_exists($key, $_ENV) || getenv($key) !== false)) { + continue; + } + + // Parse value and put it into environment + + if ($value !== '') { + $value = self::parseValue($value, $lineNumber, $expandVariables); + } + + if ($usePutEnv) { + putenv("{$key}={$value}"); + } + + $_ENV[$key] = $value; + } + } +} diff --git a/src/mako/env/exceptions/EnvException.php b/src/mako/env/exceptions/EnvException.php new file mode 100644 index 000000000..310f854bd --- /dev/null +++ b/src/mako/env/exceptions/EnvException.php @@ -0,0 +1,18 @@ + Date: Sat, 25 Jul 2026 03:58:50 +0200 Subject: [PATCH 2/8] Update DotenvLoader.php --- src/mako/env/DotenvLoader.php | 36 ++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php index 13f90c85d..bf2852f8e 100644 --- a/src/mako/env/DotenvLoader.php +++ b/src/mako/env/DotenvLoader.php @@ -32,10 +32,20 @@ */ final class DotenvLoader { + /** + * Constructor. + */ + public function __construct( + protected bool $overrideExisting = false, + protected bool $interpolateVariables = false, + protected bool $usePutEnv = false + ) { + } + /** * Strip inline comment from value. */ - private static function stripInlineComment(string $value): string + private function stripInlineComment(string $value): string { $quote = ($value[0] === '"' || $value[0] === "'") ? $value[0] : null; $escaped = false; @@ -73,9 +83,9 @@ private static function stripInlineComment(string $value): string } /** - * Expand variables in value. + * Interpolate variables in value. */ - private static function expandVariables(string $value): string + private function interpolateVariables(string $value): string { return preg_replace_callback( '/(\\\\*)\$\{([A-Z_][A-Z0-9_]*)\}/i', @@ -100,7 +110,7 @@ static function (array $matches): string { /** * Unescape values. */ - private static function unescape(string $value): string + private function unescape(string $value): string { return str_replace( ['\\n', '\\r', '\\t', '\\"', '\\\\'], @@ -112,12 +122,12 @@ private static function unescape(string $value): string /** * Parse value. */ - private static function parseValue(string $value, int $lineNumber, bool $expandVariables): string + private function parseValue(string $value, int $lineNumber): string { // Remove inline comments if (strpos($value, '#') !== false) { - $value = self::stripInlineComment($value); + $value = $this->stripInlineComment($value); } if ($value === '') { @@ -139,11 +149,11 @@ private static function parseValue(string $value, int $lineNumber, bool $expandV $value = substr($value, 1, -1); if ($first === '"') { - if ($expandVariables && strpos($value, '${') !== false) { - $value = self::expandVariables($value); + if ($this->interpolateVariables && strpos($value, '${') !== false) { + $value = $this->interpolateVariables($value); } - $value = self::unescape($value); + $value = $this->unescape($value); } return $value; @@ -157,7 +167,7 @@ private static function parseValue(string $value, int $lineNumber, bool $expandV /** * Loads the file into environment variables. */ - public static function load(string $filePath, bool $overrideExisting = false, bool $expandVariables = false, bool $usePutEnv = false): void + public function load(string $filePath): void { $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); @@ -211,17 +221,17 @@ public static function load(string $filePath, bool $overrideExisting = false, bo // Should we skip overriding existing variables? - if ($overrideExisting === false && (array_key_exists($key, $_ENV) || getenv($key) !== false)) { + if ($this->overrideExisting === false && (array_key_exists($key, $_ENV) || getenv($key) !== false)) { continue; } // Parse value and put it into environment if ($value !== '') { - $value = self::parseValue($value, $lineNumber, $expandVariables); + $value = $this->parseValue($value, $lineNumber); } - if ($usePutEnv) { + if ($this->usePutEnv) { putenv("{$key}={$value}"); } From 2f266bfb17171b34ab51e8595f98aa0eec77804b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 04:05:18 +0200 Subject: [PATCH 3/8] Update DotenvLoader.php --- src/mako/env/DotenvLoader.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php index bf2852f8e..604e47f70 100644 --- a/src/mako/env/DotenvLoader.php +++ b/src/mako/env/DotenvLoader.php @@ -36,9 +36,9 @@ final class DotenvLoader * Constructor. */ public function __construct( - protected bool $overrideExisting = false, - protected bool $interpolateVariables = false, - protected bool $usePutEnv = false + private bool $overrideExisting = false, + private bool $interpolateVariables = false, + private bool $usePutEnv = false ) { } From 2483838fc6af69c1d8ce007780df831edb4f06bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 11:35:55 +0200 Subject: [PATCH 4/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ea279b4..46ae50a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ The major version bump is due to upping the required PHP version from `8.5` to ` * Added the following methods to the `ImageInterface` interface: - `ImageInterface::toBase64()` - `ImageInterface::toDataUri()` +* Added dotenv loader. #### Changes From 84295d106452c33ae4f89497b5b3bf77d7a8fd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 21:18:34 +0200 Subject: [PATCH 5/8] Cleanup + added tests --- src/mako/env/DotenvLoader.php | 57 +++-- tests/unit/env/DotenvLoaderTest.php | 194 ++++++++++++++++++ tests/unit/env/fixtures/basic.env | 35 ++++ .../unit/env/fixtures/invalid-declaration.env | 1 + tests/unit/env/fixtures/invalid-key.env | 1 + tests/unit/env/fixtures/override.env | 2 + .../unit/env/fixtures/undefined-variable.env | 1 + .../fixtures/unterminated-double-quote.env | 1 + .../fixtures/unterminated-single-quote.env | 1 + 9 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 tests/unit/env/DotenvLoaderTest.php create mode 100644 tests/unit/env/fixtures/basic.env create mode 100644 tests/unit/env/fixtures/invalid-declaration.env create mode 100644 tests/unit/env/fixtures/invalid-key.env create mode 100644 tests/unit/env/fixtures/override.env create mode 100644 tests/unit/env/fixtures/undefined-variable.env create mode 100644 tests/unit/env/fixtures/unterminated-double-quote.env create mode 100644 tests/unit/env/fixtures/unterminated-single-quote.env diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php index 604e47f70..981bcc3ab 100644 --- a/src/mako/env/DotenvLoader.php +++ b/src/mako/env/DotenvLoader.php @@ -85,11 +85,11 @@ private function stripInlineComment(string $value): string /** * Interpolate variables in value. */ - private function interpolateVariables(string $value): string + private function interpolateVariables(string $value, string $filePath, int $lineNumber): string { return preg_replace_callback( '/(\\\\*)\$\{([A-Z_][A-Z0-9_]*)\}/i', - static function (array $matches): string { + static function (array $matches) use ($filePath, $lineNumber): string { $slashes = $matches[1]; $key = $matches[2]; @@ -101,7 +101,15 @@ static function (array $matches): string { // Even number of backslashes, we'll expand the variable - return $slashes . (env($key) ?? throw new EnvException(sprintf('Unknown environment variable [ $%s ].', $key))); + return $slashes . ( + env($key) + ?? throw new EnvException(sprintf( + 'Undefined environment variable [ $%s ] in [ %s ] on line [ %s ].', + $key, + $filePath, + $lineNumber + )) + ); }, $value ) ?? $value; @@ -122,7 +130,7 @@ private function unescape(string $value): string /** * Parse value. */ - private function parseValue(string $value, int $lineNumber): string + private function parseValue(string $value, string $filePath, int $lineNumber): string { // Remove inline comments @@ -143,14 +151,18 @@ private function parseValue(string $value, int $lineNumber): string if ($first === '"' || $first === "'") { if ($last !== $first) { - throw new EnvException(sprintf('Unterminated quoted value on line [ %s ]', $lineNumber)); + throw new EnvException(sprintf( + 'Unterminated quoted value in [ %s ] on line [ %s ].', + $filePath, + $lineNumber + )); } $value = substr($value, 1, -1); if ($first === '"') { if ($this->interpolateVariables && strpos($value, '${') !== false) { - $value = $this->interpolateVariables($value); + $value = $this->interpolateVariables($value, $filePath, $lineNumber); } $value = $this->unescape($value); @@ -167,7 +179,7 @@ private function parseValue(string $value, int $lineNumber): string /** * Loads the file into environment variables. */ - public function load(string $filePath): void + public function load(string $filePath, string $keyPrefix = ''): void { $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); @@ -181,7 +193,11 @@ public function load(string $filePath): void // Check for NUL byte if (str_contains($line, "\0")) { - throw new EnvException(sprintf('NUL byte detected in [ %s ] on line [ %s ].', $filePath, $lineNumber)); + throw new EnvException(sprintf( + 'NUL byte detected in [ %s ] on line [ %s ].', + $filePath, + $lineNumber + )); } // Strip UTF-8 BOM from first line @@ -206,19 +222,30 @@ public function load(string $filePath): void // Split key and value - $pos = strpos($line, '='); + $equalPosition = strpos($line, '='); - if ($pos === false) { - throw new EnvException(sprintf('Invalid env declaration in [ %s ] on line [ %s ].', $filePath, $lineNumber)); + if ($equalPosition === false) { + throw new EnvException(sprintf( + 'Invalid env declaration in [ %s ] on line [ %s ].', + $filePath, + $lineNumber + )); } - $key = trim(substr($line, 0, $pos)); - $value = trim(substr($line, $pos + 1)); + $key = trim(substr($line, 0, $equalPosition)); + $value = trim(substr($line, $equalPosition + 1)); if ($key === '' || !preg_match('/^(?:[A-Z][A-Z0-9_]*|_[A-Z0-9_]+)$/i', $key)) { - throw new EnvException(sprintf('Invalid key [ %s ] in [ %s ] on line [ %s ].', ($key ?: 'empty'), $filePath, $lineNumber)); + throw new EnvException(sprintf( + 'Invalid key [ %s ] in [ %s ] on line [ %s ].', + ($key ?: 'empty'), + $filePath, + $lineNumber + )); } + $key = "{$keyPrefix}{$key}"; + // Should we skip overriding existing variables? if ($this->overrideExisting === false && (array_key_exists($key, $_ENV) || getenv($key) !== false)) { @@ -228,7 +255,7 @@ public function load(string $filePath): void // Parse value and put it into environment if ($value !== '') { - $value = $this->parseValue($value, $lineNumber); + $value = $this->parseValue($value, $filePath, $lineNumber); } if ($this->usePutEnv) { diff --git a/tests/unit/env/DotenvLoaderTest.php b/tests/unit/env/DotenvLoaderTest.php new file mode 100644 index 000000000..06a8e812f --- /dev/null +++ b/tests/unit/env/DotenvLoaderTest.php @@ -0,0 +1,194 @@ +load(__DIR__ . '/fixtures/basic.env', 'MAKO_TEST_'); + + $this->assertSame('value#1', $_ENV['MAKO_TEST_KEY1']); + $this->assertSame('value#2', $_ENV['MAKO_TEST_KEY2']); + $this->assertSame('value#3', $_ENV['MAKO_TEST_KEY3']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY4']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY5']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY6']); + $this->assertSame('value#7', $_ENV['MAKO_TEST_KEY7']); + $this->assertSame('value#8', $_ENV['MAKO_TEST_KEY8']); + $this->assertSame('value#9', $_ENV['MAKO_TEST_KEY9']); + $this->assertSame('value#10', $_ENV['MAKO_TEST_KEY10']); + $this->assertSame('value#11', $_ENV['MAKO_TEST_KEY11']); + $this->assertSame('value#12', $_ENV['MAKO_TEST_KEY12']); + $this->assertSame('value#13', $_ENV['MAKO_TEST_KEY13']); + $this->assertSame('value#14', $_ENV['MAKO_TEST_KEY14']); + $this->assertSame('value#15', $_ENV['MAKO_TEST_KEY15']); + $this->assertSame('foo\nbar', $_ENV['MAKO_TEST_KEY16']); + $this->assertSame('foo\nbar', $_ENV['MAKO_TEST_KEY17']); + $this->assertSame("foo\nbar", $_ENV['MAKO_TEST_KEY18']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY19']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY20']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY21']); + $this->assertSame('var:\\\\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY22']); + $this->assertSame('var:\\\\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY23']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY24']); + $this->assertSame('value#25', $_ENV['MAKO_TEST_KEY25']); + $this->assertSame('value#26', $_ENV['MAKO_TEST_KEY26']); + $this->assertSame('value#27', $_ENV['MAKO_TEST_KEY27']); + + foreach (range(1, 27) as $num) { + unset($_ENV["MAKO_TEST_KEY{$num}"]); + } + } + + /** + * + */ + public function testLoadBasicWithInterpolatedVariables(): void + { + (new DotenvLoader(interpolateVariables: true)) + ->load(__DIR__ . '/fixtures/basic.env', 'MAKO_TEST_'); + + $this->assertSame('value#1', $_ENV['MAKO_TEST_KEY1']); + $this->assertSame('value#2', $_ENV['MAKO_TEST_KEY2']); + $this->assertSame('value#3', $_ENV['MAKO_TEST_KEY3']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY4']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY5']); + $this->assertSame('var:value#1', $_ENV['MAKO_TEST_KEY6']); + $this->assertSame('value#7', $_ENV['MAKO_TEST_KEY7']); + $this->assertSame('value#8', $_ENV['MAKO_TEST_KEY8']); + $this->assertSame('value#9', $_ENV['MAKO_TEST_KEY9']); + $this->assertSame('value#10', $_ENV['MAKO_TEST_KEY10']); + $this->assertSame('value#11', $_ENV['MAKO_TEST_KEY11']); + $this->assertSame('value#12', $_ENV['MAKO_TEST_KEY12']); + $this->assertSame('value#13', $_ENV['MAKO_TEST_KEY13']); + $this->assertSame('value#14', $_ENV['MAKO_TEST_KEY14']); + $this->assertSame('value#15', $_ENV['MAKO_TEST_KEY15']); + $this->assertSame('foo\nbar', $_ENV['MAKO_TEST_KEY16']); + $this->assertSame('foo\nbar', $_ENV['MAKO_TEST_KEY17']); + $this->assertSame("foo\nbar", $_ENV['MAKO_TEST_KEY18']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY19']); + $this->assertSame('var:\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY20']); + $this->assertSame('var:${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY21']); + $this->assertSame('var:\\\\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY22']); + $this->assertSame('var:\\\\${MAKO_TEST_KEY1}', $_ENV['MAKO_TEST_KEY23']); + $this->assertSame('var:\value#1', $_ENV['MAKO_TEST_KEY24']); + $this->assertSame('value#25', $_ENV['MAKO_TEST_KEY25']); + $this->assertSame('value#26', $_ENV['MAKO_TEST_KEY26']); + $this->assertSame('value#27', $_ENV['MAKO_TEST_KEY27']); + + foreach (range(1, 27) as $num) { + unset($_ENV["MAKO_TEST_KEY{$num}"]); + } + } + + /** + * + */ + public function testNoOverride(): void + { + (new DotenvLoader) + ->load(__DIR__ . '/fixtures/override.env', 'MAKO_TEST_'); + + $this->assertSame('foo', $_ENV['MAKO_TEST_KEY1']); + + unset($_ENV['MAKO_TEST_KEY1']); + } + + /** + * + */ + public function testWithOverride(): void + { + (new DotenvLoader(overrideExisting: true)) + ->load(__DIR__ . '/fixtures/override.env', 'MAKO_TEST_'); + + $this->assertSame('bar', $_ENV['MAKO_TEST_KEY1']); + + unset($_ENV['MAKO_TEST_KEY1']); + } + + /** + * + */ + public function testInvalidDeclaration(): void + { + $file = __DIR__ . '/fixtures/invalid-declaration.env'; + + $this->expectException(EnvException::class); + $this->expectExceptionMessageIs('Invalid env declaration in [ ' . $file . ' ] on line [ 1 ].'); + + (new DotenvLoader)->load($file, 'MAKO_TEST_'); + } + + /** + * + */ + public function testInvalidKey(): void + { + $file = __DIR__ . '/fixtures/invalid-key.env'; + + $this->expectException(EnvException::class); + $this->expectExceptionMessageIs('Invalid key [ $KEY1 ] in [ ' . $file . ' ] on line [ 1 ].'); + + (new DotenvLoader) + ->load($file, 'MAKO_TEST_'); + } + + /** + * + */ + public function testUnterminatedDoubleQuote(): void + { + $file = __DIR__ . '/fixtures/unterminated-double-quote.env'; + + $this->expectException(EnvException::class); + $this->expectExceptionMessageIs('Unterminated quoted value in [ ' . $file . ' ] on line [ 1 ].'); + + (new DotenvLoader) + ->load($file, 'MAKO_TEST_'); + } + + /** + * + */ + public function testUnterminatedSingleQuote(): void + { + $file = __DIR__ . '/fixtures/unterminated-single-quote.env'; + + $this->expectException(EnvException::class); + $this->expectExceptionMessageIs('Unterminated quoted value in [ ' . $file . ' ] on line [ 1 ].'); + + (new DotenvLoader) + ->load($file, 'MAKO_TEST_'); + } + + /** + * + */ + public function testUndefinedVariable(): void + { + $file = __DIR__ . '/fixtures/undefined-variable.env'; + + $this->expectException(EnvException::class); + $this->expectExceptionMessageIs('Undefined environment variable [ $MAKO_TEST_FOO_BAR_BAZ ] in [ ' . $file . ' ] on line [ 1 ].'); + + (new DotenvLoader(interpolateVariables: true)) + ->load($file, 'MAKO_TEST_'); + } +} diff --git a/tests/unit/env/fixtures/basic.env b/tests/unit/env/fixtures/basic.env new file mode 100644 index 000000000..9ddf86ad2 --- /dev/null +++ b/tests/unit/env/fixtures/basic.env @@ -0,0 +1,35 @@ +KEY1=value#1 #comment +KEY2='value#2' #comment +KEY3="value#3" #comment + +KEY4=var:${MAKO_TEST_KEY1} +KEY5='var:${MAKO_TEST_KEY1}' +KEY6="var:${MAKO_TEST_KEY1}" + +KEY7= value#7 +KEY8= 'value#8' +KEY9= "value#9" + +KEY10 =value#10 +KEY11 ='value#11' +KEY12 ="value#12" + +KEY13 = value#13 +KEY14 = 'value#14' +KEY15 = "value#15" + +KEY16 = foo\nbar +KEY17 = 'foo\nbar' +KEY18 = "foo\nbar" + +KEY19=var:\${MAKO_TEST_KEY1} +KEY20='var:\${MAKO_TEST_KEY1}' +KEY21="var:\${MAKO_TEST_KEY1}" + +KEY22=var:\\${MAKO_TEST_KEY1} +KEY23='var:\\${MAKO_TEST_KEY1}' +KEY24="var:\\${MAKO_TEST_KEY1}" + +export KEY25= value#25 +export KEY26= 'value#26' +export KEY27= "value#27" diff --git a/tests/unit/env/fixtures/invalid-declaration.env b/tests/unit/env/fixtures/invalid-declaration.env new file mode 100644 index 000000000..7aa1ee343 --- /dev/null +++ b/tests/unit/env/fixtures/invalid-declaration.env @@ -0,0 +1 @@ +KEY1 diff --git a/tests/unit/env/fixtures/invalid-key.env b/tests/unit/env/fixtures/invalid-key.env new file mode 100644 index 000000000..65eb09bfa --- /dev/null +++ b/tests/unit/env/fixtures/invalid-key.env @@ -0,0 +1 @@ +$KEY1=foo diff --git a/tests/unit/env/fixtures/override.env b/tests/unit/env/fixtures/override.env new file mode 100644 index 000000000..790b34cad --- /dev/null +++ b/tests/unit/env/fixtures/override.env @@ -0,0 +1,2 @@ +KEY1=foo +KEY1=bar diff --git a/tests/unit/env/fixtures/undefined-variable.env b/tests/unit/env/fixtures/undefined-variable.env new file mode 100644 index 000000000..eaf207156 --- /dev/null +++ b/tests/unit/env/fixtures/undefined-variable.env @@ -0,0 +1 @@ +KEY1="${MAKO_TEST_FOO_BAR_BAZ}" diff --git a/tests/unit/env/fixtures/unterminated-double-quote.env b/tests/unit/env/fixtures/unterminated-double-quote.env new file mode 100644 index 000000000..701130fd2 --- /dev/null +++ b/tests/unit/env/fixtures/unterminated-double-quote.env @@ -0,0 +1 @@ +KEY1="value diff --git a/tests/unit/env/fixtures/unterminated-single-quote.env b/tests/unit/env/fixtures/unterminated-single-quote.env new file mode 100644 index 000000000..88c5d7fdf --- /dev/null +++ b/tests/unit/env/fixtures/unterminated-single-quote.env @@ -0,0 +1 @@ +KEY1='value From 0d0ceddf7fb03795fde72e6632884105a9e1f3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 22:28:34 +0200 Subject: [PATCH 6/8] Update DotenvLoader.php --- src/mako/env/DotenvLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php index 981bcc3ab..edea6fd5d 100644 --- a/src/mako/env/DotenvLoader.php +++ b/src/mako/env/DotenvLoader.php @@ -181,7 +181,7 @@ private function parseValue(string $value, string $filePath, int $lineNumber): s */ public function load(string $filePath, string $keyPrefix = ''): void { - $lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $lines = file($filePath, FILE_IGNORE_NEW_LINES); if ($lines === false) { throw new EnvException(sprintf('Failed to read env file [ %s ].', $filePath)); From 9d97f8ccfbd26304a6543da47ddfc3a6f3f74984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 22:29:31 +0200 Subject: [PATCH 7/8] Update DotenvLoader.php --- src/mako/env/DotenvLoader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mako/env/DotenvLoader.php b/src/mako/env/DotenvLoader.php index edea6fd5d..3d2555c53 100644 --- a/src/mako/env/DotenvLoader.php +++ b/src/mako/env/DotenvLoader.php @@ -203,8 +203,8 @@ public function load(string $filePath, string $keyPrefix = ''): void // Strip UTF-8 BOM from first line if ($lineNumber === 1 && str_starts_with($line, "\xEF\xBB\xBF")) { - $line = substr($line, 3); - } + $line = substr($line, 3); + } $line = trim($line); From 8882d83667cc83530cbb67b6e54137f2a1e941f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederic=20G=2E=20=C3=98stby?= Date: Sat, 25 Jul 2026 22:38:58 +0200 Subject: [PATCH 8/8] Update functions.php --- src/mako/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mako/functions.php b/src/mako/functions.php index c14f0c3f3..889eb47d4 100644 --- a/src/mako/functions.php +++ b/src/mako/functions.php @@ -32,7 +32,7 @@ function f(string $_name, mixed ...$_arguments): string */ function env(string $variableName, mixed $default = null, bool $localOnly = false, ?Type $as = null): mixed { - $value = $_ENV[$variableName] ?? (getenv($variableName, $localOnly) ?: null); + $value = $_ENV[$variableName] ?? (($env = getenv($variableName, $localOnly)) !== false ? $env : null); return match ($as) { null => $value,