Dotenv.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Dotenv;
  11. use Symfony\Component\Dotenv\Exception\FormatException;
  12. use Symfony\Component\Dotenv\Exception\FormatExceptionContext;
  13. use Symfony\Component\Dotenv\Exception\PathException;
  14. use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
  15. use Symfony\Component\Process\Process;
  16. /**
  17. * Manages .env files.
  18. *
  19. * @author Fabien Potencier <fabien@symfony.com>
  20. * @author Kévin Dunglas <dunglas@gmail.com>
  21. */
  22. final class Dotenv
  23. {
  24. public const VARNAME_REGEX = '(?i:[A-Z][A-Z0-9_]*+)';
  25. public const STATE_VARNAME = 0;
  26. public const STATE_VALUE = 1;
  27. private $path;
  28. private $cursor;
  29. private $lineno;
  30. private $data;
  31. private $end;
  32. private $values;
  33. private $envKey;
  34. private $debugKey;
  35. private $prodEnvs = ['prod'];
  36. private $usePutenv = false;
  37. /**
  38. * @param string $envKey
  39. */
  40. public function __construct($envKey = 'APP_ENV', string $debugKey = 'APP_DEBUG')
  41. {
  42. if (\in_array($envKey = (string) $envKey, ['1', ''], true)) {
  43. trigger_deprecation('symfony/dotenv', '5.1', 'Passing a boolean to the constructor of "%s" is deprecated, use "Dotenv::usePutenv()".', __CLASS__);
  44. $this->usePutenv = (bool) $envKey;
  45. $envKey = 'APP_ENV';
  46. }
  47. $this->envKey = $envKey;
  48. $this->debugKey = $debugKey;
  49. }
  50. /**
  51. * @return $this
  52. */
  53. public function setProdEnvs(array $prodEnvs): self
  54. {
  55. $this->prodEnvs = $prodEnvs;
  56. return $this;
  57. }
  58. /**
  59. * @param bool $usePutenv If `putenv()` should be used to define environment variables or not.
  60. * Beware that `putenv()` is not thread safe, that's why this setting defaults to false
  61. *
  62. * @return $this
  63. */
  64. public function usePutenv($usePutenv = true): self
  65. {
  66. $this->usePutenv = $usePutenv;
  67. return $this;
  68. }
  69. /**
  70. * Loads one or several .env files.
  71. *
  72. * @param string $path A file to load
  73. * @param ...string $extraPaths A list of additional files to load
  74. *
  75. * @throws FormatException when a file has a syntax error
  76. * @throws PathException when a file does not exist or is not readable
  77. */
  78. public function load(string $path, string ...$extraPaths): void
  79. {
  80. $this->doLoad(false, \func_get_args());
  81. }
  82. /**
  83. * Loads a .env file and the corresponding .env.local, .env.$env and .env.$env.local files if they exist.
  84. *
  85. * .env.local is always ignored in test env because tests should produce the same results for everyone.
  86. * .env.dist is loaded when it exists and .env is not found.
  87. *
  88. * @param string $path A file to load
  89. * @param string $envKey|null The name of the env vars that defines the app env
  90. * @param string $defaultEnv The app env to use when none is defined
  91. * @param array $testEnvs A list of app envs for which .env.local should be ignored
  92. *
  93. * @throws FormatException when a file has a syntax error
  94. * @throws PathException when a file does not exist or is not readable
  95. */
  96. public function loadEnv(string $path, string $envKey = null, string $defaultEnv = 'dev', array $testEnvs = ['test']): void
  97. {
  98. $k = $envKey ?? $this->envKey;
  99. if (is_file($path) || !is_file($p = "$path.dist")) {
  100. $this->load($path);
  101. } else {
  102. $this->load($p);
  103. }
  104. if (null === $env = $_SERVER[$k] ?? $_ENV[$k] ?? null) {
  105. $this->populate([$k => $env = $defaultEnv]);
  106. }
  107. if (!\in_array($env, $testEnvs, true) && is_file($p = "$path.local")) {
  108. $this->load($p);
  109. $env = $_SERVER[$k] ?? $_ENV[$k] ?? $env;
  110. }
  111. if ('local' === $env) {
  112. return;
  113. }
  114. if (is_file($p = "$path.$env")) {
  115. $this->load($p);
  116. }
  117. if (is_file($p = "$path.$env.local")) {
  118. $this->load($p);
  119. }
  120. }
  121. /**
  122. * Loads env vars from .env.local.php if the file exists or from the other .env files otherwise.
  123. *
  124. * This method also configures the APP_DEBUG env var according to the current APP_ENV.
  125. *
  126. * See method loadEnv() for rules related to .env files.
  127. */
  128. public function bootEnv(string $path, string $defaultEnv = 'dev', array $testEnvs = ['test']): void
  129. {
  130. $p = $path.'.local.php';
  131. $env = is_file($p) ? include $p : null;
  132. $k = $this->envKey;
  133. if (\is_array($env) && (!isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) {
  134. $this->populate($env);
  135. } else {
  136. $this->loadEnv($path, $k, $defaultEnv, $testEnvs);
  137. }
  138. $_SERVER += $_ENV;
  139. $k = $this->debugKey;
  140. $debug = $_SERVER[$k] ?? !\in_array($_SERVER[$this->envKey], $this->prodEnvs, true);
  141. $_SERVER[$k] = $_ENV[$k] = (int) $debug || (!\is_bool($debug) && filter_var($debug, \FILTER_VALIDATE_BOOLEAN)) ? '1' : '0';
  142. }
  143. /**
  144. * Loads one or several .env files and enables override existing vars.
  145. *
  146. * @param string $path A file to load
  147. * @param ...string $extraPaths A list of additional files to load
  148. *
  149. * @throws FormatException when a file has a syntax error
  150. * @throws PathException when a file does not exist or is not readable
  151. */
  152. public function overload(string $path, string ...$extraPaths): void
  153. {
  154. $this->doLoad(true, \func_get_args());
  155. }
  156. /**
  157. * Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
  158. *
  159. * @param array $values An array of env variables
  160. * @param bool $overrideExistingVars true when existing environment variables must be overridden
  161. */
  162. public function populate(array $values, bool $overrideExistingVars = false): void
  163. {
  164. $updateLoadedVars = false;
  165. $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? ''));
  166. foreach ($values as $name => $value) {
  167. $notHttpName = 0 !== strpos($name, 'HTTP_');
  168. // don't check existence with getenv() because of thread safety issues
  169. if (!isset($loadedVars[$name]) && (!$overrideExistingVars && (isset($_ENV[$name]) || (isset($_SERVER[$name]) && $notHttpName)))) {
  170. continue;
  171. }
  172. if ($this->usePutenv) {
  173. putenv("$name=$value");
  174. }
  175. $_ENV[$name] = $value;
  176. if ($notHttpName) {
  177. $_SERVER[$name] = $value;
  178. }
  179. if (!isset($loadedVars[$name])) {
  180. $loadedVars[$name] = $updateLoadedVars = true;
  181. }
  182. }
  183. if ($updateLoadedVars) {
  184. unset($loadedVars['']);
  185. $loadedVars = implode(',', array_keys($loadedVars));
  186. $_ENV['SYMFONY_DOTENV_VARS'] = $_SERVER['SYMFONY_DOTENV_VARS'] = $loadedVars;
  187. if ($this->usePutenv) {
  188. putenv('SYMFONY_DOTENV_VARS='.$loadedVars);
  189. }
  190. }
  191. }
  192. /**
  193. * Parses the contents of an .env file.
  194. *
  195. * @param string $data The data to be parsed
  196. * @param string $path The original file name where data where stored (used for more meaningful error messages)
  197. *
  198. * @return array An array of env variables
  199. *
  200. * @throws FormatException when a file has a syntax error
  201. */
  202. public function parse(string $data, string $path = '.env'): array
  203. {
  204. $this->path = $path;
  205. $this->data = str_replace(["\r\n", "\r"], "\n", $data);
  206. $this->lineno = 1;
  207. $this->cursor = 0;
  208. $this->end = \strlen($this->data);
  209. $state = self::STATE_VARNAME;
  210. $this->values = [];
  211. $name = '';
  212. $this->skipEmptyLines();
  213. while ($this->cursor < $this->end) {
  214. switch ($state) {
  215. case self::STATE_VARNAME:
  216. $name = $this->lexVarname();
  217. $state = self::STATE_VALUE;
  218. break;
  219. case self::STATE_VALUE:
  220. $this->values[$name] = $this->lexValue();
  221. $state = self::STATE_VARNAME;
  222. break;
  223. }
  224. }
  225. if (self::STATE_VALUE === $state) {
  226. $this->values[$name] = '';
  227. }
  228. try {
  229. return $this->values;
  230. } finally {
  231. $this->values = [];
  232. $this->data = null;
  233. $this->path = null;
  234. }
  235. }
  236. private function lexVarname(): string
  237. {
  238. // var name + optional export
  239. if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A', $this->data, $matches, 0, $this->cursor)) {
  240. throw $this->createFormatException('Invalid character in variable name');
  241. }
  242. $this->moveCursor($matches[0]);
  243. if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) {
  244. if ($matches[1]) {
  245. throw $this->createFormatException('Unable to unset an environment variable');
  246. }
  247. throw $this->createFormatException('Missing = in the environment variable declaration');
  248. }
  249. if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  250. throw $this->createFormatException('Whitespace characters are not supported after the variable name');
  251. }
  252. if ('=' !== $this->data[$this->cursor]) {
  253. throw $this->createFormatException('Missing = in the environment variable declaration');
  254. }
  255. ++$this->cursor;
  256. return $matches[2];
  257. }
  258. private function lexValue(): string
  259. {
  260. if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) {
  261. $this->moveCursor($matches[0]);
  262. $this->skipEmptyLines();
  263. return '';
  264. }
  265. if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  266. throw $this->createFormatException('Whitespace are not supported before the value');
  267. }
  268. $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? ($_ENV['SYMFONY_DOTENV_VARS'] ?? '')));
  269. unset($loadedVars['']);
  270. $v = '';
  271. do {
  272. if ("'" === $this->data[$this->cursor]) {
  273. $len = 0;
  274. do {
  275. if ($this->cursor + ++$len === $this->end) {
  276. $this->cursor += $len;
  277. throw $this->createFormatException('Missing quote to end the value');
  278. }
  279. } while ("'" !== $this->data[$this->cursor + $len]);
  280. $v .= substr($this->data, 1 + $this->cursor, $len - 1);
  281. $this->cursor += 1 + $len;
  282. } elseif ('"' === $this->data[$this->cursor]) {
  283. $value = '';
  284. if (++$this->cursor === $this->end) {
  285. throw $this->createFormatException('Missing quote to end the value');
  286. }
  287. while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) {
  288. $value .= $this->data[$this->cursor];
  289. ++$this->cursor;
  290. if ($this->cursor === $this->end) {
  291. throw $this->createFormatException('Missing quote to end the value');
  292. }
  293. }
  294. ++$this->cursor;
  295. $value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value);
  296. $resolvedValue = $value;
  297. $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
  298. $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
  299. $resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
  300. $v .= $resolvedValue;
  301. } else {
  302. $value = '';
  303. $prevChr = $this->data[$this->cursor - 1];
  304. while ($this->cursor < $this->end && !\in_array($this->data[$this->cursor], ["\n", '"', "'"], true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) {
  305. if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && ('"' === $this->data[$this->cursor + 1] || "'" === $this->data[$this->cursor + 1])) {
  306. ++$this->cursor;
  307. }
  308. $value .= $prevChr = $this->data[$this->cursor];
  309. if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && '(' === $this->data[$this->cursor + 1]) {
  310. ++$this->cursor;
  311. $value .= '('.$this->lexNestedExpression().')';
  312. }
  313. ++$this->cursor;
  314. }
  315. $value = rtrim($value);
  316. $resolvedValue = $value;
  317. $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
  318. $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
  319. $resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
  320. if ($resolvedValue === $value && preg_match('/\s+/', $value)) {
  321. throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
  322. }
  323. $v .= $resolvedValue;
  324. if ($this->cursor < $this->end && '#' === $this->data[$this->cursor]) {
  325. break;
  326. }
  327. }
  328. } while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor]);
  329. $this->skipEmptyLines();
  330. return $v;
  331. }
  332. private function lexNestedExpression(): string
  333. {
  334. ++$this->cursor;
  335. $value = '';
  336. while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) {
  337. $value .= $this->data[$this->cursor];
  338. if ('(' === $this->data[$this->cursor]) {
  339. $value .= $this->lexNestedExpression().')';
  340. }
  341. ++$this->cursor;
  342. if ($this->cursor === $this->end) {
  343. throw $this->createFormatException('Missing closing parenthesis.');
  344. }
  345. }
  346. if ("\n" === $this->data[$this->cursor]) {
  347. throw $this->createFormatException('Missing closing parenthesis.');
  348. }
  349. return $value;
  350. }
  351. private function skipEmptyLines()
  352. {
  353. if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, 0, $this->cursor)) {
  354. $this->moveCursor($match[0]);
  355. }
  356. }
  357. private function resolveCommands(string $value, array $loadedVars): string
  358. {
  359. if (false === strpos($value, '$')) {
  360. return $value;
  361. }
  362. $regex = '/
  363. (\\\\)? # escaped with a backslash?
  364. \$
  365. (?<cmd>
  366. \( # require opening parenthesis
  367. ([^()]|\g<cmd>)+ # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
  368. \) # require closing paren
  369. )
  370. /x';
  371. return preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  372. if ('\\' === $matches[1]) {
  373. return substr($matches[0], 1);
  374. }
  375. if ('\\' === \DIRECTORY_SEPARATOR) {
  376. throw new \LogicException('Resolving commands is not supported on Windows.');
  377. }
  378. if (!class_exists(Process::class)) {
  379. throw new \LogicException('Resolving commands requires the Symfony Process component.');
  380. }
  381. $process = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline('echo '.$matches[0]) : new Process('echo '.$matches[0]);
  382. if (!method_exists(Process::class, 'fromShellCommandline') && method_exists(Process::class, 'inheritEnvironmentVariables')) {
  383. // Symfony 3.4 does not inherit env vars by default:
  384. $process->inheritEnvironmentVariables();
  385. }
  386. $env = [];
  387. foreach ($this->values as $name => $value) {
  388. if (isset($loadedVars[$name]) || (!isset($_ENV[$name]) && !(isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')))) {
  389. $env[$name] = $value;
  390. }
  391. }
  392. $process->setEnv($env);
  393. try {
  394. $process->mustRun();
  395. } catch (ProcessException $e) {
  396. throw $this->createFormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput()));
  397. }
  398. return preg_replace('/[\r\n]+$/', '', $process->getOutput());
  399. }, $value);
  400. }
  401. private function resolveVariables(string $value, array $loadedVars): string
  402. {
  403. if (false === strpos($value, '$')) {
  404. return $value;
  405. }
  406. $regex = '/
  407. (?<!\\\\)
  408. (?P<backslashes>\\\\*) # escaped with a backslash?
  409. \$
  410. (?!\() # no opening parenthesis
  411. (?P<opening_brace>\{)? # optional brace
  412. (?P<name>'.self::VARNAME_REGEX.')? # var name
  413. (?P<default_value>:[-=][^\}]++)? # optional default value
  414. (?P<closing_brace>\})? # optional closing brace
  415. /x';
  416. $value = preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  417. // odd number of backslashes means the $ character is escaped
  418. if (1 === \strlen($matches['backslashes']) % 2) {
  419. return substr($matches[0], 1);
  420. }
  421. // unescaped $ not followed by variable name
  422. if (!isset($matches['name'])) {
  423. return $matches[0];
  424. }
  425. if ('{' === $matches['opening_brace'] && !isset($matches['closing_brace'])) {
  426. throw $this->createFormatException('Unclosed braces on variable expansion');
  427. }
  428. $name = $matches['name'];
  429. if (isset($loadedVars[$name]) && isset($this->values[$name])) {
  430. $value = $this->values[$name];
  431. } elseif (isset($_ENV[$name])) {
  432. $value = $_ENV[$name];
  433. } elseif (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) {
  434. $value = $_SERVER[$name];
  435. } elseif (isset($this->values[$name])) {
  436. $value = $this->values[$name];
  437. } else {
  438. $value = (string) getenv($name);
  439. }
  440. if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) {
  441. $unsupportedChars = strpbrk($matches['default_value'], '\'"{$');
  442. if (false !== $unsupportedChars) {
  443. throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name));
  444. }
  445. $value = substr($matches['default_value'], 2);
  446. if ('=' === $matches['default_value'][1]) {
  447. $this->values[$name] = $value;
  448. }
  449. }
  450. if (!$matches['opening_brace'] && isset($matches['closing_brace'])) {
  451. $value .= '}';
  452. }
  453. return $matches['backslashes'].$value;
  454. }, $value);
  455. return $value;
  456. }
  457. private function moveCursor(string $text)
  458. {
  459. $this->cursor += \strlen($text);
  460. $this->lineno += substr_count($text, "\n");
  461. }
  462. private function createFormatException(string $message): FormatException
  463. {
  464. return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor));
  465. }
  466. private function doLoad(bool $overrideExistingVars, array $paths): void
  467. {
  468. foreach ($paths as $path) {
  469. if (!is_readable($path) || is_dir($path)) {
  470. throw new PathException($path);
  471. }
  472. $this->populate($this->parse(file_get_contents($path), $path), $overrideExistingVars);
  473. }
  474. }
  475. }