├── CHANGELOG.md ├── Command ├── DebugCommand.php └── DotenvDumpCommand.php ├── Dotenv.php ├── Exception ├── ExceptionInterface.php ├── FormatException.php ├── FormatExceptionContext.php └── PathException.php ├── LICENSE ├── README.md └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.1 5 | --- 6 | 7 | * Add `SYMFONY_DOTENV_PATH` variable with the path to the `.env` file loaded by `Dotenv::loadEnv()` or `Dotenv::bootEnv()` 8 | 9 | 6.2 10 | --- 11 | 12 | * Add a new `filter` argument to `debug:dotenv` command to filter variable names 13 | 14 | 5.4 15 | --- 16 | 17 | * Add `dotenv:dump` command to compile the contents of the .env files into a PHP-optimized file called `.env.local.php` 18 | * Add `debug:dotenv` command to list all dotenv files with variables and values 19 | * Add `$overrideExistingVars` on `Dotenv::bootEnv()` and `Dotenv::loadEnv()` 20 | 21 | 5.1.0 22 | ----- 23 | 24 | * added `Dotenv::bootEnv()` to check for `.env.local.php` before calling `Dotenv::loadEnv()` 25 | * added `Dotenv::setProdEnvs()` and `Dotenv::usePutenv()` 26 | * made Dotenv's constructor accept `$envKey` and `$debugKey` arguments, to define 27 | the name of the env vars that configure the env name and debug settings 28 | * deprecated passing `$usePutenv` argument to Dotenv's constructor 29 | 30 | 5.0.0 31 | ----- 32 | 33 | * using `putenv()` is disabled by default 34 | 35 | 4.3.0 36 | ----- 37 | 38 | * deprecated use of `putenv()` by default. This feature will be opted-in with a constructor argument to `Dotenv` 39 | 40 | 4.2.0 41 | ----- 42 | 43 | * added `Dotenv::overload()` and `$overrideExistingVars` as optional parameter of `Dotenv::populate()` 44 | * added `Dotenv::loadEnv()` to load a .env file and its corresponding .env.local, .env.$env and .env.$env.local files if they exist 45 | 46 | 3.3.0 47 | ----- 48 | 49 | * [BC BREAK] Since v3.3.7, the latest Dotenv files override the previous ones. Real env vars are not affected and are not overridden. 50 | * added the component 51 | -------------------------------------------------------------------------------- /Command/DebugCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Command; 13 | 14 | use Symfony\Component\Console\Attribute\AsCommand; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Completion\CompletionInput; 17 | use Symfony\Component\Console\Completion\CompletionSuggestions; 18 | use Symfony\Component\Console\Formatter\OutputFormatter; 19 | use Symfony\Component\Console\Input\InputArgument; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Output\OutputInterface; 22 | use Symfony\Component\Console\Style\SymfonyStyle; 23 | use Symfony\Component\Dotenv\Dotenv; 24 | 25 | /** 26 | * A console command to debug current dotenv files with variables and values. 27 | * 28 | * @author Christopher Hertel 29 | */ 30 | #[AsCommand(name: 'debug:dotenv', description: 'List all dotenv files with variables and values')] 31 | final class DebugCommand extends Command 32 | { 33 | /** 34 | * @deprecated since Symfony 6.1 35 | */ 36 | protected static $defaultName = 'debug:dotenv'; 37 | 38 | /** 39 | * @deprecated since Symfony 6.1 40 | */ 41 | protected static $defaultDescription = 'List all dotenv files with variables and values'; 42 | 43 | public function __construct( 44 | private string $kernelEnvironment, 45 | private string $projectDirectory, 46 | ) { 47 | parent::__construct(); 48 | } 49 | 50 | protected function configure(): void 51 | { 52 | $this 53 | ->setDefinition([ 54 | new InputArgument('filter', InputArgument::OPTIONAL, 'The name of an environment variable or a filter.', null, $this->getAvailableVars(...)), 55 | ]) 56 | ->setHelp(<<<'EOT' 57 | The %command.full_name% command displays all the environment variables configured by dotenv: 58 | 59 | php %command.full_name% 60 | 61 | To get specific variables, specify its full or partial name: 62 | 63 | php %command.full_name% FOO_BAR 64 | 65 | EOT 66 | ); 67 | } 68 | 69 | protected function execute(InputInterface $input, OutputInterface $output): int 70 | { 71 | $io = new SymfonyStyle($input, $output); 72 | $io->title('Dotenv Variables & Files'); 73 | 74 | if (!\array_key_exists('SYMFONY_DOTENV_VARS', $_SERVER)) { 75 | $io->error('Dotenv component is not initialized.'); 76 | 77 | return 1; 78 | } 79 | 80 | if (!$filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? null) { 81 | $dotenvPath = $this->projectDirectory; 82 | 83 | if (is_file($composerFile = $this->projectDirectory.'/composer.json')) { 84 | $runtimeConfig = (json_decode(file_get_contents($composerFile), true))['extra']['runtime'] ?? []; 85 | 86 | if (isset($runtimeConfig['dotenv_path'])) { 87 | $dotenvPath = $this->projectDirectory.'/'.$runtimeConfig['dotenv_path']; 88 | } 89 | } 90 | 91 | $filePath = $dotenvPath.'/.env'; 92 | } 93 | 94 | $envFiles = $this->getEnvFiles($filePath); 95 | $availableFiles = array_filter($envFiles, 'is_file'); 96 | 97 | if (\in_array(\sprintf('%s.local.php', $filePath), $availableFiles, true)) { 98 | $io->warning(\sprintf('Due to existing dump file (%s.local.php) all other dotenv files are skipped.', $this->getRelativeName($filePath))); 99 | } 100 | 101 | if (is_file($filePath) && is_file(\sprintf('%s.dist', $filePath))) { 102 | $io->warning(\sprintf('The file %s.dist gets skipped due to the existence of %1$s.', $this->getRelativeName($filePath))); 103 | } 104 | 105 | $io->section('Scanned Files (in descending priority)'); 106 | $io->listing(array_map(fn (string $envFile) => \in_array($envFile, $availableFiles, true) 107 | ? \sprintf('✓ %s', $this->getRelativeName($envFile)) 108 | : \sprintf('⨯ %s', $this->getRelativeName($envFile)), $envFiles)); 109 | 110 | $nameFilter = $input->getArgument('filter'); 111 | $variables = $this->getVariables($availableFiles, $nameFilter); 112 | 113 | $io->section('Variables'); 114 | 115 | if ($variables || null === $nameFilter) { 116 | $io->table( 117 | array_merge(['Variable', 'Value'], array_map($this->getRelativeName(...), $availableFiles)), 118 | $variables 119 | ); 120 | 121 | $io->comment('Note that values might be different between web and CLI.'); 122 | } else { 123 | $io->warning(\sprintf('No variables match the given filter "%s".', $nameFilter)); 124 | } 125 | 126 | return 0; 127 | } 128 | 129 | public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void 130 | { 131 | if ($input->mustSuggestArgumentValuesFor('filter')) { 132 | $suggestions->suggestValues($this->getAvailableVars()); 133 | } 134 | } 135 | 136 | private function getVariables(array $envFiles, ?string $nameFilter): array 137 | { 138 | $variables = []; 139 | $fileValues = []; 140 | $dotenvVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? '')); 141 | 142 | foreach ($envFiles as $envFile) { 143 | $fileValues[$envFile] = $this->loadValues($envFile); 144 | $variables += $fileValues[$envFile]; 145 | } 146 | 147 | foreach ($variables as $var => $varDetails) { 148 | if (null !== $nameFilter && 0 !== stripos($var, $nameFilter)) { 149 | unset($variables[$var]); 150 | continue; 151 | } 152 | 153 | $realValue = $_SERVER[$var] ?? ''; 154 | $varDetails = [$var, ''.OutputFormatter::escape($realValue).'']; 155 | $varSeen = !isset($dotenvVars[$var]); 156 | 157 | foreach ($envFiles as $envFile) { 158 | if (null === $value = $fileValues[$envFile][$var] ?? null) { 159 | $varDetails[] = 'n/a'; 160 | continue; 161 | } 162 | 163 | $shortenedValue = OutputFormatter::escape($this->getHelper('formatter')->truncate($value, 30)); 164 | $varDetails[] = $value === $realValue && !$varSeen ? ''.$shortenedValue.'' : $shortenedValue; 165 | $varSeen = $varSeen || $value === $realValue; 166 | } 167 | 168 | $variables[$var] = $varDetails; 169 | } 170 | 171 | ksort($variables); 172 | 173 | return $variables; 174 | } 175 | 176 | private function getAvailableVars(): array 177 | { 178 | $filePath = $_SERVER['SYMFONY_DOTENV_PATH'] ?? $this->projectDirectory.\DIRECTORY_SEPARATOR.'.env'; 179 | $envFiles = $this->getEnvFiles($filePath); 180 | 181 | return array_keys($this->getVariables(array_filter($envFiles, 'is_file'), null)); 182 | } 183 | 184 | private function getEnvFiles(string $filePath): array 185 | { 186 | $files = [ 187 | \sprintf('%s.local.php', $filePath), 188 | \sprintf('%s.%s.local', $filePath, $this->kernelEnvironment), 189 | \sprintf('%s.%s', $filePath, $this->kernelEnvironment), 190 | ]; 191 | 192 | if ('test' !== $this->kernelEnvironment) { 193 | $files[] = \sprintf('%s.local', $filePath); 194 | } 195 | 196 | if (!is_file($filePath) && is_file(\sprintf('%s.dist', $filePath))) { 197 | $files[] = \sprintf('%s.dist', $filePath); 198 | } else { 199 | $files[] = $filePath; 200 | } 201 | 202 | return $files; 203 | } 204 | 205 | private function getRelativeName(string $filePath): string 206 | { 207 | if (str_starts_with($filePath, $this->projectDirectory)) { 208 | return substr($filePath, \strlen($this->projectDirectory) + 1); 209 | } 210 | 211 | return basename($filePath); 212 | } 213 | 214 | private function loadValues(string $filePath): array 215 | { 216 | if (str_ends_with($filePath, '.php')) { 217 | return include $filePath; 218 | } 219 | 220 | return (new Dotenv())->parse(file_get_contents($filePath)); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Command/DotenvDumpCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Command; 13 | 14 | use Symfony\Component\Console\Attribute\AsCommand; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputInterface; 18 | use Symfony\Component\Console\Input\InputOption; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; 21 | use Symfony\Component\Dotenv\Dotenv; 22 | 23 | /** 24 | * A console command to compile .env files into a PHP-optimized file called .env.local.php. 25 | * 26 | * @internal 27 | */ 28 | #[Autoconfigure(bind: ['$projectDir' => '%kernel.project_dir%', '$defaultEnv' => '%kernel.environment%'])] 29 | #[AsCommand(name: 'dotenv:dump', description: 'Compile .env files to .env.local.php')] 30 | final class DotenvDumpCommand extends Command 31 | { 32 | public function __construct( 33 | private string $projectDir, 34 | private ?string $defaultEnv = null, 35 | ) { 36 | parent::__construct(); 37 | } 38 | 39 | protected function configure(): void 40 | { 41 | $this 42 | ->setDefinition([ 43 | new InputArgument('env', null === $this->defaultEnv ? InputArgument::REQUIRED : InputArgument::OPTIONAL, 'The application environment to dump .env files for - e.g. "prod".'), 44 | ]) 45 | ->addOption('empty', null, InputOption::VALUE_NONE, 'Ignore the content of .env files') 46 | ->setHelp(<<<'EOT' 47 | The %command.name% command compiles .env files into a PHP-optimized file called .env.local.php. 48 | 49 | %command.full_name% 50 | EOT 51 | ) 52 | ; 53 | } 54 | 55 | protected function execute(InputInterface $input, OutputInterface $output): int 56 | { 57 | $config = []; 58 | if (is_file($projectDir = $this->projectDir)) { 59 | $config = ['dotenv_path' => basename($projectDir)]; 60 | $projectDir = \dirname($projectDir); 61 | } 62 | 63 | $composerFile = $projectDir.'/composer.json'; 64 | $config += (is_file($composerFile) ? json_decode(file_get_contents($composerFile), true) : [])['extra']['runtime'] ?? []; 65 | $dotenvPath = $projectDir.'/'.($config['dotenv_path'] ?? '.env'); 66 | $env = $input->getArgument('env') ?? $this->defaultEnv; 67 | $envKey = $config['env_var_name'] ?? 'APP_ENV'; 68 | 69 | if ($input->getOption('empty')) { 70 | $vars = [$envKey => $env]; 71 | } else { 72 | $vars = $this->loadEnv($dotenvPath, $env, $config); 73 | $env = $vars[$envKey]; 74 | } 75 | 76 | $vars = var_export($vars, true); 77 | $vars = <<writeln(\sprintf('Successfully dumped .env files in .env.local.php for the %s environment.', $env)); 88 | 89 | return 0; 90 | } 91 | 92 | private function loadEnv(string $dotenvPath, string $env, array $config): array 93 | { 94 | $envKey = $config['env_var_name'] ?? 'APP_ENV'; 95 | $testEnvs = $config['test_envs'] ?? ['test']; 96 | 97 | $dotenv = new Dotenv($envKey); 98 | 99 | $globalsBackup = [$_SERVER, $_ENV]; 100 | unset($_SERVER[$envKey]); 101 | $_ENV = [$envKey => $env]; 102 | $_SERVER['SYMFONY_DOTENV_VARS'] = implode(',', array_keys($_SERVER)); 103 | 104 | try { 105 | $dotenv->loadEnv($dotenvPath, null, 'dev', $testEnvs); 106 | unset($_ENV['SYMFONY_DOTENV_VARS']); 107 | unset($_ENV['SYMFONY_DOTENV_PATH']); 108 | 109 | return $_ENV; 110 | } finally { 111 | [$_SERVER, $_ENV] = $globalsBackup; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Dotenv.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv; 13 | 14 | use Symfony\Component\Dotenv\Exception\FormatException; 15 | use Symfony\Component\Dotenv\Exception\FormatExceptionContext; 16 | use Symfony\Component\Dotenv\Exception\PathException; 17 | use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException; 18 | use Symfony\Component\Process\Process; 19 | 20 | /** 21 | * Manages .env files. 22 | * 23 | * @author Fabien Potencier 24 | * @author Kévin Dunglas 25 | */ 26 | final class Dotenv 27 | { 28 | public const VARNAME_REGEX = '(?i:_?[A-Z][A-Z0-9_]*+)'; 29 | public const STATE_VARNAME = 0; 30 | public const STATE_VALUE = 1; 31 | 32 | private string $path; 33 | private int $cursor; 34 | private int $lineno; 35 | private string $data; 36 | private int $end; 37 | private array $values = []; 38 | private array $prodEnvs = ['prod']; 39 | private bool $usePutenv = false; 40 | 41 | public function __construct( 42 | private string $envKey = 'APP_ENV', 43 | private string $debugKey = 'APP_DEBUG', 44 | ) { 45 | } 46 | 47 | /** 48 | * @return $this 49 | */ 50 | public function setProdEnvs(array $prodEnvs): static 51 | { 52 | $this->prodEnvs = $prodEnvs; 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param bool $usePutenv If `putenv()` should be used to define environment variables or not. 59 | * Beware that `putenv()` is not thread safe, that's why it's not enabled by default 60 | * 61 | * @return $this 62 | */ 63 | public function usePutenv(bool $usePutenv = true): static 64 | { 65 | $this->usePutenv = $usePutenv; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Loads one or several .env files. 72 | * 73 | * @param string $path A file to load 74 | * @param string ...$extraPaths A list of additional files to load 75 | * 76 | * @throws FormatException when a file has a syntax error 77 | * @throws PathException when a file does not exist or is not readable 78 | */ 79 | public function load(string $path, string ...$extraPaths): void 80 | { 81 | $this->doLoad(false, \func_get_args()); 82 | } 83 | 84 | /** 85 | * Loads a .env file and the corresponding .env.local, .env.$env and .env.$env.local files if they exist. 86 | * 87 | * .env.local is always ignored in test env because tests should produce the same results for everyone. 88 | * .env.dist is loaded when it exists and .env is not found. 89 | * 90 | * @param string $path A file to load 91 | * @param string|null $envKey The name of the env vars that defines the app env 92 | * @param string $defaultEnv The app env to use when none is defined 93 | * @param array $testEnvs A list of app envs for which .env.local should be ignored 94 | * @param bool $overrideExistingVars Whether existing environment variables set by the system should be overridden 95 | * 96 | * @throws FormatException when a file has a syntax error 97 | * @throws PathException when a file does not exist or is not readable 98 | */ 99 | public function loadEnv(string $path, ?string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test'], bool $overrideExistingVars = false): void 100 | { 101 | $this->populatePath($path); 102 | 103 | $k = $envKey ?? $this->envKey; 104 | 105 | if (is_file($path) || !is_file($p = "$path.dist")) { 106 | $this->doLoad($overrideExistingVars, [$path]); 107 | } else { 108 | $this->doLoad($overrideExistingVars, [$p]); 109 | } 110 | 111 | if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) { 112 | $this->populate([$k => $env = $defaultEnv], $overrideExistingVars); 113 | } 114 | 115 | if (!\in_array($env, $testEnvs, true) && is_file($p = "$path.local")) { 116 | $this->doLoad($overrideExistingVars, [$p]); 117 | $env = $_SERVER[$k] ?? $_ENV[$k] ?? $env; 118 | } 119 | 120 | if ('local' === $env) { 121 | return; 122 | } 123 | 124 | if (is_file($p = "$path.$env")) { 125 | $this->doLoad($overrideExistingVars, [$p]); 126 | } 127 | 128 | if (is_file($p = "$path.$env.local")) { 129 | $this->doLoad($overrideExistingVars, [$p]); 130 | } 131 | } 132 | 133 | /** 134 | * Loads env vars from .env.local.php if the file exists or from the other .env files otherwise. 135 | * 136 | * This method also configures the APP_DEBUG env var according to the current APP_ENV. 137 | * 138 | * See method loadEnv() for rules related to .env files. 139 | */ 140 | public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnvs = ['test'], bool $overrideExistingVars = false): void 141 | { 142 | $p = $path.'.local.php'; 143 | $env = is_file($p) ? include $p : null; 144 | $k = $this->envKey; 145 | 146 | if (\is_array($env) && ($overrideExistingVars || !isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) { 147 | $this->populatePath($path); 148 | $this->populate($env, $overrideExistingVars); 149 | } else { 150 | $this->loadEnv($path, $k, $defaultEnv, $testEnvs, $overrideExistingVars); 151 | } 152 | 153 | $_SERVER += $_ENV; 154 | 155 | $k = $this->debugKey; 156 | $debug = $_SERVER[$k] ?? !\in_array($_SERVER[$this->envKey], $this->prodEnvs, true); 157 | $_SERVER[$k] = $_ENV[$k] = (int) $debug || (!\is_bool($debug) && filter_var($debug, \FILTER_VALIDATE_BOOL)) ? '1' : '0'; 158 | } 159 | 160 | /** 161 | * Loads one or several .env files and enables override existing vars. 162 | * 163 | * @param string $path A file to load 164 | * @param string ...$extraPaths A list of additional files to load 165 | * 166 | * @throws FormatException when a file has a syntax error 167 | * @throws PathException when a file does not exist or is not readable 168 | */ 169 | public function overload(string $path, string ...$extraPaths): void 170 | { 171 | $this->doLoad(true, \func_get_args()); 172 | } 173 | 174 | /** 175 | * Sets values as environment variables (via putenv, $_ENV, and $_SERVER). 176 | * 177 | * @param array $values An array of env variables 178 | * @param bool $overrideExistingVars Whether existing environment variables set by the system should be overridden 179 | */ 180 | public function populate(array $values, bool $overrideExistingVars = false): void 181 | { 182 | $updateLoadedVars = false; 183 | $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '')); 184 | 185 | foreach ($values as $name => $value) { 186 | $notHttpName = !str_starts_with($name, 'HTTP_'); 187 | if (isset($_SERVER[$name]) && $notHttpName && !isset($_ENV[$name])) { 188 | $_ENV[$name] = $_SERVER[$name]; 189 | } 190 | 191 | // don't check existence with getenv() because of thread safety issues 192 | if (!isset($loadedVars[$name]) && !$overrideExistingVars && isset($_ENV[$name])) { 193 | continue; 194 | } 195 | 196 | if ($this->usePutenv) { 197 | putenv("$name=$value"); 198 | } 199 | 200 | $_ENV[$name] = $value; 201 | if ($notHttpName) { 202 | $_SERVER[$name] = $value; 203 | } 204 | 205 | if (!isset($loadedVars[$name])) { 206 | $loadedVars[$name] = $updateLoadedVars = true; 207 | } 208 | } 209 | 210 | if ($updateLoadedVars) { 211 | unset($loadedVars['']); 212 | $loadedVars = implode(',', array_keys($loadedVars)); 213 | $_ENV['SYMFONY_DOTENV_VARS'] = $_SERVER['SYMFONY_DOTENV_VARS'] = $loadedVars; 214 | 215 | if ($this->usePutenv) { 216 | putenv('SYMFONY_DOTENV_VARS='.$loadedVars); 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * Parses the contents of an .env file. 223 | * 224 | * @param string $data The data to be parsed 225 | * @param string $path The original file name where data where stored (used for more meaningful error messages) 226 | * 227 | * @throws FormatException when a file has a syntax error 228 | */ 229 | public function parse(string $data, string $path = '.env'): array 230 | { 231 | $this->path = $path; 232 | $this->data = str_replace(["\r\n", "\r"], "\n", $data); 233 | $this->lineno = 1; 234 | $this->cursor = 0; 235 | $this->end = \strlen($this->data); 236 | $state = self::STATE_VARNAME; 237 | $this->values = []; 238 | $name = ''; 239 | 240 | $this->skipEmptyLines(); 241 | 242 | while ($this->cursor < $this->end) { 243 | switch ($state) { 244 | case self::STATE_VARNAME: 245 | $name = $this->lexVarname(); 246 | $state = self::STATE_VALUE; 247 | break; 248 | 249 | case self::STATE_VALUE: 250 | $this->values[$name] = $this->lexValue(); 251 | $state = self::STATE_VARNAME; 252 | break; 253 | } 254 | } 255 | 256 | if (self::STATE_VALUE === $state) { 257 | $this->values[$name] = ''; 258 | } 259 | 260 | try { 261 | return $this->values; 262 | } finally { 263 | $this->values = []; 264 | unset($this->path, $this->cursor, $this->lineno, $this->data, $this->end); 265 | } 266 | } 267 | 268 | private function lexVarname(): string 269 | { 270 | // var name + optional export 271 | if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A', $this->data, $matches, 0, $this->cursor)) { 272 | throw $this->createFormatException('Invalid character in variable name'); 273 | } 274 | $this->moveCursor($matches[0]); 275 | 276 | if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) { 277 | if ($matches[1]) { 278 | throw $this->createFormatException('Unable to unset an environment variable'); 279 | } 280 | 281 | throw $this->createFormatException('Missing = in the environment variable declaration'); 282 | } 283 | 284 | if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) { 285 | throw $this->createFormatException('Whitespace characters are not supported after the variable name'); 286 | } 287 | 288 | if ('=' !== $this->data[$this->cursor]) { 289 | throw $this->createFormatException('Missing = in the environment variable declaration'); 290 | } 291 | ++$this->cursor; 292 | 293 | return $matches[2]; 294 | } 295 | 296 | private function lexValue(): string 297 | { 298 | if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) { 299 | $this->moveCursor($matches[0]); 300 | $this->skipEmptyLines(); 301 | 302 | return ''; 303 | } 304 | 305 | if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) { 306 | throw $this->createFormatException('Whitespace are not supported before the value'); 307 | } 308 | 309 | $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '')); 310 | unset($loadedVars['']); 311 | $v = ''; 312 | 313 | do { 314 | if ("'" === $this->data[$this->cursor]) { 315 | $len = 0; 316 | 317 | do { 318 | if ($this->cursor + ++$len === $this->end) { 319 | $this->cursor += $len; 320 | 321 | throw $this->createFormatException('Missing quote to end the value'); 322 | } 323 | } while ("'" !== $this->data[$this->cursor + $len]); 324 | 325 | $v .= substr($this->data, 1 + $this->cursor, $len - 1); 326 | $this->cursor += 1 + $len; 327 | } elseif ('"' === $this->data[$this->cursor]) { 328 | $value = ''; 329 | 330 | if (++$this->cursor === $this->end) { 331 | throw $this->createFormatException('Missing quote to end the value'); 332 | } 333 | 334 | while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) { 335 | $value .= $this->data[$this->cursor]; 336 | ++$this->cursor; 337 | 338 | if ($this->cursor === $this->end) { 339 | throw $this->createFormatException('Missing quote to end the value'); 340 | } 341 | } 342 | ++$this->cursor; 343 | $value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value); 344 | $resolvedValue = $value; 345 | $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars); 346 | $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); 347 | $resolvedValue = str_replace('\\\\', '\\', $resolvedValue); 348 | $v .= $resolvedValue; 349 | } else { 350 | $value = ''; 351 | $prevChr = $this->data[$this->cursor - 1]; 352 | while ($this->cursor < $this->end && !\in_array($this->data[$this->cursor], ["\n", '"', "'"], true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) { 353 | if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && ('"' === $this->data[$this->cursor + 1] || "'" === $this->data[$this->cursor + 1])) { 354 | ++$this->cursor; 355 | } 356 | 357 | $value .= $prevChr = $this->data[$this->cursor]; 358 | 359 | if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && '(' === $this->data[$this->cursor + 1]) { 360 | ++$this->cursor; 361 | $value .= '('.$this->lexNestedExpression().')'; 362 | } 363 | 364 | ++$this->cursor; 365 | } 366 | $value = rtrim($value); 367 | $resolvedValue = $value; 368 | $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars); 369 | $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars); 370 | $resolvedValue = str_replace('\\\\', '\\', $resolvedValue); 371 | 372 | if ($resolvedValue === $value && preg_match('/\s+/', $value)) { 373 | throw $this->createFormatException('A value containing spaces must be surrounded by quotes'); 374 | } 375 | 376 | $v .= $resolvedValue; 377 | 378 | if ($this->cursor < $this->end && '#' === $this->data[$this->cursor]) { 379 | break; 380 | } 381 | } 382 | } while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor]); 383 | 384 | $this->skipEmptyLines(); 385 | 386 | return $v; 387 | } 388 | 389 | private function lexNestedExpression(): string 390 | { 391 | ++$this->cursor; 392 | $value = ''; 393 | 394 | while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) { 395 | $value .= $this->data[$this->cursor]; 396 | 397 | if ('(' === $this->data[$this->cursor]) { 398 | $value .= $this->lexNestedExpression().')'; 399 | } 400 | 401 | ++$this->cursor; 402 | 403 | if ($this->cursor === $this->end) { 404 | throw $this->createFormatException('Missing closing parenthesis.'); 405 | } 406 | } 407 | 408 | if ("\n" === $this->data[$this->cursor]) { 409 | throw $this->createFormatException('Missing closing parenthesis.'); 410 | } 411 | 412 | return $value; 413 | } 414 | 415 | private function skipEmptyLines(): void 416 | { 417 | if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, 0, $this->cursor)) { 418 | $this->moveCursor($match[0]); 419 | } 420 | } 421 | 422 | private function resolveCommands(string $value, array $loadedVars): string 423 | { 424 | if (!str_contains($value, '$')) { 425 | return $value; 426 | } 427 | 428 | $regex = '/ 429 | (\\\\)? # escaped with a backslash? 430 | \$ 431 | (? 432 | \( # require opening parenthesis 433 | ([^()]|\g)+ # allow any number of non-parens, or balanced parens (by nesting the expression recursively) 434 | \) # require closing paren 435 | ) 436 | /x'; 437 | 438 | return preg_replace_callback($regex, function ($matches) use ($loadedVars) { 439 | if ('\\' === $matches[1]) { 440 | return substr($matches[0], 1); 441 | } 442 | 443 | if ('\\' === \DIRECTORY_SEPARATOR) { 444 | throw new \LogicException('Resolving commands is not supported on Windows.'); 445 | } 446 | 447 | if (!class_exists(Process::class)) { 448 | throw new \LogicException('Resolving commands requires the Symfony Process component. Try running "composer require symfony/process".'); 449 | } 450 | 451 | $process = Process::fromShellCommandline('echo '.$matches[0]); 452 | 453 | $env = []; 454 | foreach ($this->values as $name => $value) { 455 | if (isset($loadedVars[$name]) || (!isset($_ENV[$name]) && !(isset($_SERVER[$name]) && !str_starts_with($name, 'HTTP_')))) { 456 | $env[$name] = $value; 457 | } 458 | } 459 | $process->setEnv($env); 460 | 461 | try { 462 | $process->mustRun(); 463 | } catch (ProcessException) { 464 | throw $this->createFormatException(\sprintf('Issue expanding a command (%s)', $process->getErrorOutput())); 465 | } 466 | 467 | return rtrim($process->getOutput(), "\n\r"); 468 | }, $value); 469 | } 470 | 471 | private function resolveVariables(string $value, array $loadedVars): string 472 | { 473 | if (!str_contains($value, '$')) { 474 | return $value; 475 | } 476 | 477 | $regex = '/ 478 | (?\\\\*) # escaped with a backslash? 480 | \$ 481 | (?!\() # no opening parenthesis 482 | (?P\{)? # optional brace 483 | (?P'.self::VARNAME_REGEX.')? # var name 484 | (?P:[-=][^\}]*+)? # optional default value 485 | (?P\})? # optional closing brace 486 | /x'; 487 | 488 | return preg_replace_callback($regex, function ($matches) use ($loadedVars) { 489 | // odd number of backslashes means the $ character is escaped 490 | if (1 === \strlen($matches['backslashes']) % 2) { 491 | return substr($matches[0], 1); 492 | } 493 | 494 | // unescaped $ not followed by variable name 495 | if (!isset($matches['name'])) { 496 | return $matches[0]; 497 | } 498 | 499 | if ('{' === $matches['opening_brace'] && !isset($matches['closing_brace'])) { 500 | throw $this->createFormatException('Unclosed braces on variable expansion'); 501 | } 502 | 503 | $name = $matches['name']; 504 | if (isset($loadedVars[$name]) && isset($this->values[$name])) { 505 | $value = $this->values[$name]; 506 | } elseif (isset($_ENV[$name])) { 507 | $value = $_ENV[$name]; 508 | } elseif (isset($_SERVER[$name]) && !str_starts_with($name, 'HTTP_')) { 509 | $value = $_SERVER[$name]; 510 | } elseif (isset($this->values[$name])) { 511 | $value = $this->values[$name]; 512 | } else { 513 | $value = (string) getenv($name); 514 | } 515 | 516 | if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) { 517 | $unsupportedChars = strpbrk($matches['default_value'], '\'"{$'); 518 | if (false !== $unsupportedChars) { 519 | throw $this->createFormatException(\sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name)); 520 | } 521 | 522 | $value = substr($matches['default_value'], 2); 523 | 524 | if ('=' === $matches['default_value'][1]) { 525 | $this->values[$name] = $value; 526 | } 527 | } 528 | 529 | if (!$matches['opening_brace'] && isset($matches['closing_brace'])) { 530 | $value .= '}'; 531 | } 532 | 533 | return $matches['backslashes'].$value; 534 | }, $value); 535 | } 536 | 537 | private function moveCursor(string $text): void 538 | { 539 | $this->cursor += \strlen($text); 540 | $this->lineno += substr_count($text, "\n"); 541 | } 542 | 543 | private function createFormatException(string $message): FormatException 544 | { 545 | return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor)); 546 | } 547 | 548 | private function doLoad(bool $overrideExistingVars, array $paths): void 549 | { 550 | foreach ($paths as $path) { 551 | if (!is_readable($path) || is_dir($path)) { 552 | throw new PathException($path); 553 | } 554 | 555 | $data = file_get_contents($path); 556 | 557 | if ("\xEF\xBB\xBF" === substr($data, 0, 3)) { 558 | throw new FormatException('Loading files starting with a byte-order-mark (BOM) is not supported.', new FormatExceptionContext($data, $path, 1, 0)); 559 | } 560 | 561 | $this->populate($this->parse($data, $path), $overrideExistingVars); 562 | } 563 | } 564 | 565 | private function populatePath(string $path): void 566 | { 567 | $_ENV['SYMFONY_DOTENV_PATH'] = $_SERVER['SYMFONY_DOTENV_PATH'] = $path; 568 | 569 | if ($this->usePutenv) { 570 | putenv('SYMFONY_DOTENV_PATH='.$path); 571 | } 572 | } 573 | } 574 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Exception; 13 | 14 | /** 15 | * Interface for exceptions. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | interface ExceptionInterface extends \Throwable 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/FormatException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Exception; 13 | 14 | /** 15 | * Thrown when a file has a syntax error. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | final class FormatException extends \LogicException implements ExceptionInterface 20 | { 21 | public function __construct( 22 | string $message, 23 | private FormatExceptionContext $context, 24 | int $code = 0, 25 | ?\Throwable $previous = null, 26 | ) { 27 | parent::__construct(\sprintf("%s in \"%s\" at line %d.\n%s", $message, $context->getPath(), $context->getLineno(), $context->getDetails()), $code, $previous); 28 | } 29 | 30 | public function getContext(): FormatExceptionContext 31 | { 32 | return $this->context; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Exception/FormatExceptionContext.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Exception; 13 | 14 | /** 15 | * @author Fabien Potencier 16 | */ 17 | final class FormatExceptionContext 18 | { 19 | public function __construct( 20 | private string $data, 21 | private string $path, 22 | private int $lineno, 23 | private int $cursor, 24 | ) { 25 | } 26 | 27 | public function getPath(): string 28 | { 29 | return $this->path; 30 | } 31 | 32 | public function getLineno(): int 33 | { 34 | return $this->lineno; 35 | } 36 | 37 | public function getDetails(): string 38 | { 39 | $before = str_replace("\n", '\n', substr($this->data, max(0, $this->cursor - 20), min(20, $this->cursor))); 40 | $after = str_replace("\n", '\n', substr($this->data, $this->cursor, 20)); 41 | 42 | return '...'.$before.$after."...\n".str_repeat(' ', \strlen($before) + 2).'^ line '.$this->lineno.' offset '.$this->cursor; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Exception/PathException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Dotenv\Exception; 13 | 14 | /** 15 | * Thrown when a file does not exist or is not readable. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | final class PathException extends \RuntimeException implements ExceptionInterface 20 | { 21 | public function __construct(string $path, int $code = 0, ?\Throwable $previous = null) 22 | { 23 | parent::__construct(\sprintf('Unable to read the "%s" environment file.', $path), $code, $previous); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dotenv Component 2 | ================ 3 | 4 | Symfony Dotenv parses `.env` files to make environment variables stored in them 5 | accessible via `$_SERVER` or `$_ENV`. 6 | 7 | Getting Started 8 | --------------- 9 | 10 | ```bash 11 | composer require symfony/dotenv 12 | ``` 13 | 14 | ```php 15 | use Symfony\Component\Dotenv\Dotenv; 16 | 17 | $dotenv = new Dotenv(); 18 | $dotenv->load(__DIR__.'/.env'); 19 | 20 | // you can also load several files 21 | $dotenv->load(__DIR__.'/.env', __DIR__.'/.env.dev'); 22 | 23 | // overwrites existing env variables 24 | $dotenv->overload(__DIR__.'/.env'); 25 | 26 | // loads .env, .env.local, and .env.$APP_ENV.local or .env.$APP_ENV 27 | $dotenv->loadEnv(__DIR__.'/.env'); 28 | ``` 29 | 30 | Resources 31 | --------- 32 | 33 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 34 | * [Report issues](https://github.com/symfony/symfony/issues) and 35 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 36 | in the [main Symfony repository](https://github.com/symfony/symfony) 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/dotenv", 3 | "type": "library", 4 | "description": "Registers environment variables from a .env file", 5 | "keywords": ["environment", "env", "dotenv"], 6 | "homepage": "https://symfony.com", 7 | "license" : "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2" 20 | }, 21 | "require-dev": { 22 | "symfony/console": "^6.4|^7.0", 23 | "symfony/process": "^6.4|^7.0" 24 | }, 25 | "conflict": { 26 | "symfony/console": "<6.4", 27 | "symfony/process": "<6.4" 28 | }, 29 | "autoload": { 30 | "psr-4": { "Symfony\\Component\\Dotenv\\": "" }, 31 | "exclude-from-classmap": [ 32 | "/Tests/" 33 | ] 34 | }, 35 | "minimum-stability": "dev" 36 | } 37 | --------------------------------------------------------------------------------