vendor/symfony/dotenv/Dotenv.php line 49

Open in your IDE?
  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 it's not enabled by default
  61.      *
  62.      * @return $this
  63.      */
  64.     public function usePutenv(bool $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 $pathstring ...$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|null $envKey               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.      * @param bool        $overrideExistingVars Whether existing environment variables set by the system should be overridden
  93.      *
  94.      * @throws FormatException when a file has a syntax error
  95.      * @throws PathException   when a file does not exist or is not readable
  96.      */
  97.     public function loadEnv(string $path, ?string $envKey nullstring $defaultEnv 'dev', array $testEnvs = ['test'], bool $overrideExistingVars false): void
  98.     {
  99.         $k $envKey ?? $this->envKey;
  100.         if (is_file($path) || !is_file($p "$path.dist")) {
  101.             $this->doLoad($overrideExistingVars, [$path]);
  102.         } else {
  103.             $this->doLoad($overrideExistingVars, [$p]);
  104.         }
  105.         if (null === $env $_SERVER[$k] ?? $_ENV[$k] ?? null) {
  106.             $this->populate([$k => $env $defaultEnv], $overrideExistingVars);
  107.         }
  108.         if (!\in_array($env$testEnvstrue) && is_file($p "$path.local")) {
  109.             $this->doLoad($overrideExistingVars, [$p]);
  110.             $env $_SERVER[$k] ?? $_ENV[$k] ?? $env;
  111.         }
  112.         if ('local' === $env) {
  113.             return;
  114.         }
  115.         if (is_file($p "$path.$env")) {
  116.             $this->doLoad($overrideExistingVars, [$p]);
  117.         }
  118.         if (is_file($p "$path.$env.local")) {
  119.             $this->doLoad($overrideExistingVars, [$p]);
  120.         }
  121.     }
  122.     /**
  123.      * Loads env vars from .env.local.php if the file exists or from the other .env files otherwise.
  124.      *
  125.      * This method also configures the APP_DEBUG env var according to the current APP_ENV.
  126.      *
  127.      * See method loadEnv() for rules related to .env files.
  128.      */
  129.     public function bootEnv(string $pathstring $defaultEnv 'dev', array $testEnvs = ['test'], bool $overrideExistingVars false): void
  130.     {
  131.         $p $path.'.local.php';
  132.         $env is_file($p) ? include $p null;
  133.         $k $this->envKey;
  134.         if (\is_array($env) && ($overrideExistingVars || !isset($env[$k]) || ($_SERVER[$k] ?? $_ENV[$k] ?? $env[$k]) === $env[$k])) {
  135.             $this->populate($env$overrideExistingVars);
  136.         } else {
  137.             $this->loadEnv($path$k$defaultEnv$testEnvs$overrideExistingVars);
  138.         }
  139.         $_SERVER += $_ENV;
  140.         $k $this->debugKey;
  141.         $debug $_SERVER[$k] ?? !\in_array($_SERVER[$this->envKey], $this->prodEnvstrue);
  142.         $_SERVER[$k] = $_ENV[$k] = (int) $debug || (!\is_bool($debug) && filter_var($debug\FILTER_VALIDATE_BOOLEAN)) ? '1' '0';
  143.     }
  144.     /**
  145.      * Loads one or several .env files and enables override existing vars.
  146.      *
  147.      * @param string $path          A file to load
  148.      * @param string ...$extraPaths A list of additional files to load
  149.      *
  150.      * @throws FormatException when a file has a syntax error
  151.      * @throws PathException   when a file does not exist or is not readable
  152.      */
  153.     public function overload(string $pathstring ...$extraPaths): void
  154.     {
  155.         $this->doLoad(true\func_get_args());
  156.     }
  157.     /**
  158.      * Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
  159.      *
  160.      * @param array $values               An array of env variables
  161.      * @param bool  $overrideExistingVars Whether existing environment variables set by the system should be overridden
  162.      */
  163.     public function populate(array $valuesbool $overrideExistingVars false): void
  164.     {
  165.         $updateLoadedVars false;
  166.         $loadedVars array_flip(explode(','$_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? ''));
  167.         foreach ($values as $name => $value) {
  168.             $notHttpName !== strpos($name'HTTP_');
  169.             if (isset($_SERVER[$name]) && $notHttpName && !isset($_ENV[$name])) {
  170.                 $_ENV[$name] = $_SERVER[$name];
  171.             }
  172.             // don't check existence with getenv() because of thread safety issues
  173.             if (!isset($loadedVars[$name]) && !$overrideExistingVars && isset($_ENV[$name])) {
  174.                 continue;
  175.             }
  176.             if ($this->usePutenv) {
  177.                 putenv("$name=$value");
  178.             }
  179.             $_ENV[$name] = $value;
  180.             if ($notHttpName) {
  181.                 $_SERVER[$name] = $value;
  182.             }
  183.             if (!isset($loadedVars[$name])) {
  184.                 $loadedVars[$name] = $updateLoadedVars true;
  185.             }
  186.         }
  187.         if ($updateLoadedVars) {
  188.             unset($loadedVars['']);
  189.             $loadedVars implode(','array_keys($loadedVars));
  190.             $_ENV['SYMFONY_DOTENV_VARS'] = $_SERVER['SYMFONY_DOTENV_VARS'] = $loadedVars;
  191.             if ($this->usePutenv) {
  192.                 putenv('SYMFONY_DOTENV_VARS='.$loadedVars);
  193.             }
  194.         }
  195.     }
  196.     /**
  197.      * Parses the contents of an .env file.
  198.      *
  199.      * @param string $data The data to be parsed
  200.      * @param string $path The original file name where data where stored (used for more meaningful error messages)
  201.      *
  202.      * @throws FormatException when a file has a syntax error
  203.      */
  204.     public function parse(string $datastring $path '.env'): array
  205.     {
  206.         $this->path $path;
  207.         $this->data str_replace(["\r\n""\r"], "\n"$data);
  208.         $this->lineno 1;
  209.         $this->cursor 0;
  210.         $this->end \strlen($this->data);
  211.         $state self::STATE_VARNAME;
  212.         $this->values = [];
  213.         $name '';
  214.         $this->skipEmptyLines();
  215.         while ($this->cursor $this->end) {
  216.             switch ($state) {
  217.                 case self::STATE_VARNAME:
  218.                     $name $this->lexVarname();
  219.                     $state self::STATE_VALUE;
  220.                     break;
  221.                 case self::STATE_VALUE:
  222.                     $this->values[$name] = $this->lexValue();
  223.                     $state self::STATE_VARNAME;
  224.                     break;
  225.             }
  226.         }
  227.         if (self::STATE_VALUE === $state) {
  228.             $this->values[$name] = '';
  229.         }
  230.         try {
  231.             return $this->values;
  232.         } finally {
  233.             $this->values = [];
  234.             $this->data null;
  235.             $this->path null;
  236.         }
  237.     }
  238.     private function lexVarname(): string
  239.     {
  240.         // var name + optional export
  241.         if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A'$this->data$matches0$this->cursor)) {
  242.             throw $this->createFormatException('Invalid character in variable name');
  243.         }
  244.         $this->moveCursor($matches[0]);
  245.         if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) {
  246.             if ($matches[1]) {
  247.                 throw $this->createFormatException('Unable to unset an environment variable');
  248.             }
  249.             throw $this->createFormatException('Missing = in the environment variable declaration');
  250.         }
  251.         if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  252.             throw $this->createFormatException('Whitespace characters are not supported after the variable name');
  253.         }
  254.         if ('=' !== $this->data[$this->cursor]) {
  255.             throw $this->createFormatException('Missing = in the environment variable declaration');
  256.         }
  257.         ++$this->cursor;
  258.         return $matches[2];
  259.     }
  260.     private function lexValue(): string
  261.     {
  262.         if (preg_match('/[ \t]*+(?:#.*)?$/Am'$this->data$matches0$this->cursor)) {
  263.             $this->moveCursor($matches[0]);
  264.             $this->skipEmptyLines();
  265.             return '';
  266.         }
  267.         if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
  268.             throw $this->createFormatException('Whitespace are not supported before the value');
  269.         }
  270.         $loadedVars array_flip(explode(','$_SERVER['SYMFONY_DOTENV_VARS'] ?? ($_ENV['SYMFONY_DOTENV_VARS'] ?? '')));
  271.         unset($loadedVars['']);
  272.         $v '';
  273.         do {
  274.             if ("'" === $this->data[$this->cursor]) {
  275.                 $len 0;
  276.                 do {
  277.                     if ($this->cursor + ++$len === $this->end) {
  278.                         $this->cursor += $len;
  279.                         throw $this->createFormatException('Missing quote to end the value');
  280.                     }
  281.                 } while ("'" !== $this->data[$this->cursor $len]);
  282.                 $v .= substr($this->data$this->cursor$len 1);
  283.                 $this->cursor += $len;
  284.             } elseif ('"' === $this->data[$this->cursor]) {
  285.                 $value '';
  286.                 if (++$this->cursor === $this->end) {
  287.                     throw $this->createFormatException('Missing quote to end the value');
  288.                 }
  289.                 while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor 1] && '\\' !== $this->data[$this->cursor 2])) {
  290.                     $value .= $this->data[$this->cursor];
  291.                     ++$this->cursor;
  292.                     if ($this->cursor === $this->end) {
  293.                         throw $this->createFormatException('Missing quote to end the value');
  294.                     }
  295.                 }
  296.                 ++$this->cursor;
  297.                 $value str_replace(['\\"''\r''\n'], ['"'"\r""\n"], $value);
  298.                 $resolvedValue $value;
  299.                 $resolvedValue $this->resolveCommands($resolvedValue$loadedVars);
  300.                 $resolvedValue $this->resolveVariables($resolvedValue$loadedVars);
  301.                 $resolvedValue str_replace('\\\\''\\'$resolvedValue);
  302.                 $v .= $resolvedValue;
  303.             } else {
  304.                 $value '';
  305.                 $prevChr $this->data[$this->cursor 1];
  306.                 while ($this->cursor $this->end && !\in_array($this->data[$this->cursor], ["\n"'"'"'"], true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) {
  307.                     if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor 1]) && ('"' === $this->data[$this->cursor 1] || "'" === $this->data[$this->cursor 1])) {
  308.                         ++$this->cursor;
  309.                     }
  310.                     $value .= $prevChr $this->data[$this->cursor];
  311.                     if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor 1]) && '(' === $this->data[$this->cursor 1]) {
  312.                         ++$this->cursor;
  313.                         $value .= '('.$this->lexNestedExpression().')';
  314.                     }
  315.                     ++$this->cursor;
  316.                 }
  317.                 $value rtrim($value);
  318.                 $resolvedValue $value;
  319.                 $resolvedValue $this->resolveCommands($resolvedValue$loadedVars);
  320.                 $resolvedValue $this->resolveVariables($resolvedValue$loadedVars);
  321.                 $resolvedValue str_replace('\\\\''\\'$resolvedValue);
  322.                 if ($resolvedValue === $value && preg_match('/\s+/'$value)) {
  323.                     throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
  324.                 }
  325.                 $v .= $resolvedValue;
  326.                 if ($this->cursor $this->end && '#' === $this->data[$this->cursor]) {
  327.                     break;
  328.                 }
  329.             }
  330.         } while ($this->cursor $this->end && "\n" !== $this->data[$this->cursor]);
  331.         $this->skipEmptyLines();
  332.         return $v;
  333.     }
  334.     private function lexNestedExpression(): string
  335.     {
  336.         ++$this->cursor;
  337.         $value '';
  338.         while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) {
  339.             $value .= $this->data[$this->cursor];
  340.             if ('(' === $this->data[$this->cursor]) {
  341.                 $value .= $this->lexNestedExpression().')';
  342.             }
  343.             ++$this->cursor;
  344.             if ($this->cursor === $this->end) {
  345.                 throw $this->createFormatException('Missing closing parenthesis.');
  346.             }
  347.         }
  348.         if ("\n" === $this->data[$this->cursor]) {
  349.             throw $this->createFormatException('Missing closing parenthesis.');
  350.         }
  351.         return $value;
  352.     }
  353.     private function skipEmptyLines()
  354.     {
  355.         if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A'$this->data$match0$this->cursor)) {
  356.             $this->moveCursor($match[0]);
  357.         }
  358.     }
  359.     private function resolveCommands(string $value, array $loadedVars): string
  360.     {
  361.         if (false === strpos($value'$')) {
  362.             return $value;
  363.         }
  364.         $regex '/
  365.             (\\\\)?               # escaped with a backslash?
  366.             \$
  367.             (?<cmd>
  368.                 \(                # require opening parenthesis
  369.                 ([^()]|\g<cmd>)+  # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
  370.                 \)                # require closing paren
  371.             )
  372.         /x';
  373.         return preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  374.             if ('\\' === $matches[1]) {
  375.                 return substr($matches[0], 1);
  376.             }
  377.             if ('\\' === \DIRECTORY_SEPARATOR) {
  378.                 throw new \LogicException('Resolving commands is not supported on Windows.');
  379.             }
  380.             if (!class_exists(Process::class)) {
  381.                 throw new \LogicException('Resolving commands requires the Symfony Process component.');
  382.             }
  383.             $process method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline('echo '.$matches[0]) : new Process('echo '.$matches[0]);
  384.             if (!method_exists(Process::class, 'fromShellCommandline') && method_exists(Process::class, 'inheritEnvironmentVariables')) {
  385.                 // Symfony 3.4 does not inherit env vars by default:
  386.                 $process->inheritEnvironmentVariables();
  387.             }
  388.             $env = [];
  389.             foreach ($this->values as $name => $value) {
  390.                 if (isset($loadedVars[$name]) || (!isset($_ENV[$name]) && !(isset($_SERVER[$name]) && !== strpos($name'HTTP_')))) {
  391.                     $env[$name] = $value;
  392.                 }
  393.             }
  394.             $process->setEnv($env);
  395.             try {
  396.                 $process->mustRun();
  397.             } catch (ProcessException $e) {
  398.                 throw $this->createFormatException(sprintf('Issue expanding a command (%s)'$process->getErrorOutput()));
  399.             }
  400.             return preg_replace('/[\r\n]+$/'''$process->getOutput());
  401.         }, $value);
  402.     }
  403.     private function resolveVariables(string $value, array $loadedVars): string
  404.     {
  405.         if (false === strpos($value'$')) {
  406.             return $value;
  407.         }
  408.         $regex '/
  409.             (?<!\\\\)
  410.             (?P<backslashes>\\\\*)             # escaped with a backslash?
  411.             \$
  412.             (?!\()                             # no opening parenthesis
  413.             (?P<opening_brace>\{)?             # optional brace
  414.             (?P<name>'.self::VARNAME_REGEX.')? # var name
  415.             (?P<default_value>:[-=][^\}]*+)?   # optional default value
  416.             (?P<closing_brace>\})?             # optional closing brace
  417.         /x';
  418.         $value preg_replace_callback($regex, function ($matches) use ($loadedVars) {
  419.             // odd number of backslashes means the $ character is escaped
  420.             if (=== \strlen($matches['backslashes']) % 2) {
  421.                 return substr($matches[0], 1);
  422.             }
  423.             // unescaped $ not followed by variable name
  424.             if (!isset($matches['name'])) {
  425.                 return $matches[0];
  426.             }
  427.             if ('{' === $matches['opening_brace'] && !isset($matches['closing_brace'])) {
  428.                 throw $this->createFormatException('Unclosed braces on variable expansion');
  429.             }
  430.             $name $matches['name'];
  431.             if (isset($loadedVars[$name]) && isset($this->values[$name])) {
  432.                 $value $this->values[$name];
  433.             } elseif (isset($_ENV[$name])) {
  434.                 $value $_ENV[$name];
  435.             } elseif (isset($_SERVER[$name]) && !== strpos($name'HTTP_')) {
  436.                 $value $_SERVER[$name];
  437.             } elseif (isset($this->values[$name])) {
  438.                 $value $this->values[$name];
  439.             } else {
  440.                 $value = (string) getenv($name);
  441.             }
  442.             if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) {
  443.                 $unsupportedChars strpbrk($matches['default_value'], '\'"{$');
  444.                 if (false !== $unsupportedChars) {
  445.                     throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".'$unsupportedChars[0], $name));
  446.                 }
  447.                 $value substr($matches['default_value'], 2);
  448.                 if ('=' === $matches['default_value'][1]) {
  449.                     $this->values[$name] = $value;
  450.                 }
  451.             }
  452.             if (!$matches['opening_brace'] && isset($matches['closing_brace'])) {
  453.                 $value .= '}';
  454.             }
  455.             return $matches['backslashes'].$value;
  456.         }, $value);
  457.         return $value;
  458.     }
  459.     private function moveCursor(string $text)
  460.     {
  461.         $this->cursor += \strlen($text);
  462.         $this->lineno += substr_count($text"\n");
  463.     }
  464.     private function createFormatException(string $message): FormatException
  465.     {
  466.         return new FormatException($message, new FormatExceptionContext($this->data$this->path$this->lineno$this->cursor));
  467.     }
  468.     private function doLoad(bool $overrideExistingVars, array $paths): void
  469.     {
  470.         foreach ($paths as $path) {
  471.             if (!is_readable($path) || is_dir($path)) {
  472.                 throw new PathException($path);
  473.             }
  474.             $data file_get_contents($path);
  475.             if ("\xEF\xBB\xBF" === substr($data03)) {
  476.                 throw new FormatException('Loading files starting with a byte-order-mark (BOM) is not supported.', new FormatExceptionContext($data$path10));
  477.             }
  478.             $this->populate($this->parse($data$path), $overrideExistingVars);
  479.         }
  480.     }
  481. }