Skip to content

Instantly share code, notes, and snippets.

@jason-napolitano
Created December 20, 2020 20:35
Show Gist options
  • Save jason-napolitano/c1a02ac02e5a8df61e16935b140ea0ed to your computer and use it in GitHub Desktop.
Save jason-napolitano/c1a02ac02e5a8df61e16935b140ea0ed to your computer and use it in GitHub Desktop.
DotEnv.php
<?php
/**
* Environment-specific configuration. This class was extracted directly from the CodeIgniter 4
* framework and upgraded to use features from PHP >=8.0.0
*
*
* @link https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/Config/DotEnv.php
*/
class DotEnv
{
/**
* The directory where the .env file can be located.
*
* @var string
*/
protected string $path;
//--------------------------------------------------------------------
/**
* Builds the path to our file.
*
* @param string $path
* @param string $file
*/
public function __construct(string $path, string $file = '.env')
{
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;
}
//--------------------------------------------------------------------
/**
* The main entry point, will load the .env file and process it
* so that we end up with all settings in the PHP environment vars
* (i.e. getenv(), $_ENV, and $_SERVER)
*
* @return boolean
*/
public function load(): bool
{
// We don't want to enforce the presence of a .env file,
// they should be optional.
if ( ! is_file($this->path) ) {
return false;
}
// Ensure file is readable
if ( ! is_readable($this->path) ) {
throw new \InvalidArgumentException("The .env file is not readable: {$this->path}");
}
$lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ( $lines as $line ) {
// Is it a comment?
if ( str_starts_with(trim($line), '#') ) {
continue;
}
// If there is an equal sign, then we know we
// are assigning a variable.
if ( str_contains($line, '=') ) {
$this->setVariable($line);
}
}
return true; // for success
}
//--------------------------------------------------------------------
/**
* Sets the variable into the environment. Will parse the string
* first to look for {name}={value} pattern, ensure that nested
* variables are handled, and strip it of single and double quotes.
*
* @param string $name
* @param string $value
*/
protected function setVariable(string $name, string $value = ''): void
{
[$name, $value] = $this->normaliseVariable($name, $value);
if ( ! getenv($name, true) ) {
putenv("$name=$value");
}
if ( empty($_ENV[ $name ]) ) {
$_ENV[ $name ] = $value;
}
if ( empty($_SERVER[ $name ]) ) {
$_SERVER[ $name ] = $value;
}
}
//--------------------------------------------------------------------
/**
* Parses for assignment, cleans the $name and $value, and ensures
* that nested variables are handled.
*
* @param string $name
* @param string $value
*
* @return array
*/
public function normaliseVariable(string $name, string $value = ''): array
{
// Split our compound string into it's parts.
if ( str_contains($name, '=') ) {
[$name, $value] = explode('=', $name, 2);
}
$name = trim($name);
$value = trim($value);
// Sanitize the name
$name = str_replace(['export', '\'', '"'], '', $name);
// Sanitize the value
$value = $this->sanitizeValue($value);
$value = $this->resolveNestedVariables($value);
return [
$name,
$value,
];
}
//--------------------------------------------------------------------
/**
* Strips quotes from the environment variable value.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param string $value
*
* @return string
* @throws \InvalidArgumentException
*/
protected function sanitizeValue(string $value): string
{
if ( ! $value ) {
return $value;
}
// Does it begin with a quote?
if ( strpbrk($value[ 0 ], '"\'') !== false ) {
// value starts with a quote
$quote = $value[ 0 ];
$regexPattern = sprintf(
'/^
%1$s # match a quote at the start of the value
( # capturing sub-pattern used
(?: # we do not need to capture this
[^%1$s\\\\] # any character other than a quote or backslash
|\\\\\\\\ # or two backslashes together
|\\\\%1$s # or an escaped quote e.g \"
)* # as many characters that match the previous rules
) # end of the capturing sub-pattern
%1$s # and the closing quote
.*$ # and discard any string after the closing quote
/mx', $quote
);
$value = preg_replace($regexPattern, '$1', $value);
$value = str_replace(["\\$quote", '\\\\'], [$quote, '\\'], $value);
} else {
$parts = explode(' #', $value, 2);
$value = trim($parts[ 0 ]);
// Unquoted values cannot contain whitespace
if ( preg_match('/\s+/', $value) > 0 ) {
throw new \InvalidArgumentException('.env values containing spaces must be surrounded by quotes.');
}
}
return $value;
}
//--------------------------------------------------------------------
/**
* Resolve the nested variables.
*
* Look for ${varname} patterns in the variable value and replace with an existing
* environment variable.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param $value
*
* @return string
*/
protected function resolveNestedVariables(string $value): string
{
if ( str_contains($value, '$') ) {
$loader = $this;
$value = preg_replace_callback(
'/\${([a-zA-Z0-9_]+)}/',
static function ($matchedPatterns) use ($loader) {
$nestedVariable = $loader->getVariable($matchedPatterns[ 1 ]);
if ( is_null($nestedVariable) ) {
return $matchedPatterns[ 0 ];
}
return $nestedVariable;
},
$value
);
}
return $value;
}
//--------------------------------------------------------------------
/**
* Search the different places for environment variables and return first value found.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param string $name
*
* @return string|null
*/
protected function getVariable(string $name): ?string
{
switch ( true ) {
case array_key_exists($name, $_ENV):
return $_ENV[ $name ];
case array_key_exists($name, $_SERVER):
return $_SERVER[ $name ];
default:
$value = getenv($name);
// switch getenv default to null
return $value === false ? null : $value;
}
}
//--------------------------------------------------------------------
}
#--------------------------------------------------------------------
# Example Environment Configuration file
#
# This file can be used as a starting point for your own
# custom .env files, and contains most of the possible settings
# available in a default install.
#
# By default, all of the settings are commented out. If you want
# to override the setting, you must un-comment it by removing the '#'
# at the beginning of the line.
#--------------------------------------------------------------------
#--------------------------------------------------------------------
# APP CONFIG
#--------------------------------------------------------------------
# config.baseURL = 'http://change.me'
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
# database.hostname = localhost
# database.database = database
# database.username = username
# database.password = password
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment