-
Notifications
You must be signed in to change notification settings - Fork 38
Dotenv loader #354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Dotenv loader #354
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
705e58c
WIP
freost de5e165
Update DotenvLoader.php
freost 2f266bf
Update DotenvLoader.php
freost 2483838
Update CHANGELOG.md
freost 84295d1
Cleanup + added tests
freost 0d0cedd
Update DotenvLoader.php
freost 9d97f8c
Update DotenvLoader.php
freost 8882d83
Update functions.php
freost File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,268 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Frederic G. Østby | ||
| * @license http://www.makoframework.com/license | ||
| */ | ||
|
|
||
| namespace mako\env; | ||
|
|
||
| use mako\env\exceptions\EnvException; | ||
|
|
||
| use function array_key_exists; | ||
| use function ctype_space; | ||
| use function file; | ||
| use function getenv; | ||
| use function mako\env; | ||
| use function preg_match; | ||
| use function preg_replace_callback; | ||
| use function putenv; | ||
| use function rtrim; | ||
| use function sprintf; | ||
| use function str_contains; | ||
| use function str_replace; | ||
| use function str_starts_with; | ||
| use function strlen; | ||
| use function strpos; | ||
| use function substr; | ||
| use function trim; | ||
|
|
||
| /** | ||
| * Dotenv loader. | ||
| */ | ||
| final class DotenvLoader | ||
| { | ||
| /** | ||
| * Constructor. | ||
| */ | ||
| public function __construct( | ||
| private bool $overrideExisting = false, | ||
| private bool $interpolateVariables = false, | ||
| private bool $usePutEnv = false | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * Strip inline comment from value. | ||
| */ | ||
| private function stripInlineComment(string $value): string | ||
| { | ||
| $quote = ($value[0] === '"' || $value[0] === "'") ? $value[0] : null; | ||
| $escaped = false; | ||
|
|
||
| $length = strlen($value); | ||
|
|
||
| for ($i = 0; $i < $length; $i++) { | ||
| $char = $value[$i]; | ||
|
|
||
| if ($quote !== null) { | ||
| if ($i === 0) { | ||
| continue; | ||
| } | ||
|
|
||
| if ($quote === '"' && $char === '\\' && !$escaped) { | ||
| $escaped = true; | ||
| continue; | ||
| } | ||
|
|
||
| if ($char === $quote && !$escaped) { | ||
| $quote = null; | ||
| continue; | ||
| } | ||
|
|
||
| $escaped = false; | ||
| continue; | ||
| } | ||
|
|
||
| if ($char === '#' && $i > 0 && ctype_space($value[$i - 1])) { | ||
| return rtrim(substr($value, 0, $i)); | ||
| } | ||
| } | ||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| /** | ||
| * Interpolate variables in value. | ||
| */ | ||
| private function interpolateVariables(string $value, string $filePath, int $lineNumber): string | ||
| { | ||
| return preg_replace_callback( | ||
| '/(\\\\*)\$\{([A-Z_][A-Z0-9_]*)\}/i', | ||
| static function (array $matches) use ($filePath, $lineNumber): 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( | ||
| 'Undefined environment variable [ $%s ] in [ %s ] on line [ %s ].', | ||
| $key, | ||
| $filePath, | ||
| $lineNumber | ||
| )) | ||
| ); | ||
|
freost marked this conversation as resolved.
|
||
| }, | ||
| $value | ||
| ) ?? $value; | ||
| } | ||
|
|
||
| /** | ||
| * Unescape values. | ||
| */ | ||
| private function unescape(string $value): string | ||
| { | ||
| return str_replace( | ||
| ['\\n', '\\r', '\\t', '\\"', '\\\\'], | ||
| ["\n", "\r", "\t", '"', '\\'], | ||
| $value | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Parse value. | ||
| */ | ||
| private function parseValue(string $value, string $filePath, int $lineNumber): string | ||
| { | ||
| // Remove inline comments | ||
|
|
||
| if (strpos($value, '#') !== false) { | ||
| $value = $this->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 in [ %s ] on line [ %s ].', | ||
| $filePath, | ||
| $lineNumber | ||
| )); | ||
| } | ||
|
|
||
| $value = substr($value, 1, -1); | ||
|
|
||
| if ($first === '"') { | ||
| if ($this->interpolateVariables && strpos($value, '${') !== false) { | ||
| $value = $this->interpolateVariables($value, $filePath, $lineNumber); | ||
| } | ||
|
|
||
| $value = $this->unescape($value); | ||
| } | ||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| // Unquoted value so we'll just trim it and return | ||
|
|
||
| return trim($value); | ||
| } | ||
|
|
||
| /** | ||
| * Loads the file into environment variables. | ||
| */ | ||
| public function load(string $filePath, string $keyPrefix = ''): void | ||
| { | ||
| $lines = file($filePath, FILE_IGNORE_NEW_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 | ||
|
|
||
| $equalPosition = strpos($line, '='); | ||
|
|
||
| if ($equalPosition === false) { | ||
| throw new EnvException(sprintf( | ||
| 'Invalid env declaration in [ %s ] on line [ %s ].', | ||
| $filePath, | ||
| $lineNumber | ||
| )); | ||
| } | ||
|
|
||
| $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 | ||
| )); | ||
| } | ||
|
|
||
| $key = "{$keyPrefix}{$key}"; | ||
|
|
||
| // Should we skip overriding existing variables? | ||
|
|
||
| if ($this->overrideExisting === false && (array_key_exists($key, $_ENV) || getenv($key) !== false)) { | ||
| continue; | ||
| } | ||
|
|
||
| // Parse value and put it into environment | ||
|
|
||
| if ($value !== '') { | ||
| $value = $this->parseValue($value, $filePath, $lineNumber); | ||
| } | ||
|
|
||
| if ($this->usePutEnv) { | ||
| putenv("{$key}={$value}"); | ||
| } | ||
|
|
||
| $_ENV[$key] = $value; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * @copyright Frederic G. Østby | ||
| * @license http://www.makoframework.com/license | ||
| */ | ||
|
|
||
| namespace mako\env\exceptions; | ||
|
|
||
| use RuntimeException; | ||
|
|
||
| /** | ||
| * Env exception. | ||
| */ | ||
| class EnvException extends RuntimeException | ||
| { | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.