Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
86.67% covered (warning)
86.67%
13 / 15
CRAP
97.63% covered (success)
97.63%
206 / 211
Dotenv
0.00% covered (danger)
0.00%
0 / 1
86.67% covered (warning)
86.67%
13 / 15
117
97.63% covered (success)
97.63%
206 / 211
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 load
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 loadEnv
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
15 / 15
 overload
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 populate
100.00% covered (success)
100.00%
1 / 1
12
100.00% covered (success)
100.00%
20 / 20
 parse
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
24 / 24
 lexVarname
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
13 / 13
 lexValue
100.00% covered (success)
100.00%
1 / 1
33
100.00% covered (success)
100.00%
56 / 56
 lexNestedExpression
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
12 / 12
 skipEmptyLines
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 resolveCommands
0.00% covered (danger)
0.00%
0 / 1
14.02
81.82% covered (warning)
81.82%
18 / 22
 resolveVariables
0.00% covered (danger)
0.00%
0 / 1
19
96.77% covered (success)
96.77%
30 / 31
 moveCursor
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 createFormatException
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 doLoad
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
5 / 5
<?php
/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Symfony\Component\Dotenv;
use Symfony\Component\Dotenv\Exception\FormatException;
use Symfony\Component\Dotenv\Exception\FormatExceptionContext;
use Symfony\Component\Dotenv\Exception\PathException;
use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;
use Symfony\Component\Process\Process;
/**
 * Manages .env files.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author K√©vin Dunglas <dunglas@gmail.com>
 */
final class Dotenv
{
    const VARNAME_REGEX = '(?i:[A-Z][A-Z0-9_]*+)';
    const STATE_VARNAME = 0;
    const STATE_VALUE = 1;
    private $path;
    private $cursor;
    private $lineno;
    private $data;
    private $end;
    private $values;
    private $usePutenv;
    /**
     * @var bool If `putenv()` should be used to define environment variables or not.
     *           Beware that `putenv()` is not thread safe, that's why this setting defaults to false
     */
    public function __construct(bool $usePutenv = false)
    {
        $this->usePutenv = $usePutenv;
    }
    /**
     * Loads one or several .env files.
     *
     * @param string    $path       A file to load
     * @param ...string $extraPaths A list of additional files to load
     *
     * @throws FormatException when a file has a syntax error
     * @throws PathException   when a file does not exist or is not readable
     */
    public function load(string $path, string ...$extraPaths): void
    {
        $this->doLoad(false, \func_get_args());
    }
    /**
     * Loads a .env file and the corresponding .env.local, .env.$env and .env.$env.local files if they exist.
     *
     * .env.local is always ignored in test env because tests should produce the same results for everyone.
     * .env.dist is loaded when it exists and .env is not found.
     *
     * @param string $path       A file to load
     * @param string $varName    The name of the env vars that defines the app env
     * @param string $defaultEnv The app env to use when none is defined
     * @param array  $testEnvs   A list of app envs for which .env.local should be ignored
     *
     * @throws FormatException when a file has a syntax error
     * @throws PathException   when a file does not exist or is not readable
     */
    public function loadEnv(string $path, string $varName = 'APP_ENV', string $defaultEnv = 'dev', array $testEnvs = ['test']): void
    {
        if (file_exists($path) || !file_exists($p = "$path.dist")) {
            $this->load($path);
        } else {
            $this->load($p);
        }
        if (null === $env = $_SERVER[$varName] ?? $_ENV[$varName] ?? null) {
            $this->populate([$varName => $env = $defaultEnv]);
        }
        if (!\in_array($env, $testEnvs, true) && file_exists($p = "$path.local")) {
            $this->load($p);
            $env = $_SERVER[$varName] ?? $_ENV[$varName] ?? $env;
        }
        if ('local' === $env) {
            return;
        }
        if (file_exists($p = "$path.$env")) {
            $this->load($p);
        }
        if (file_exists($p = "$path.$env.local")) {
            $this->load($p);
        }
    }
    /**
     * Loads one or several .env files and enables override existing vars.
     *
     * @param string    $path       A file to load
     * @param ...string $extraPaths A list of additional files to load
     *
     * @throws FormatException when a file has a syntax error
     * @throws PathException   when a file does not exist or is not readable
     */
    public function overload(string $path, string ...$extraPaths): void
    {
        $this->doLoad(true, \func_get_args());
    }
    /**
     * Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
     *
     * @param array $values               An array of env variables
     * @param bool  $overrideExistingVars true when existing environment variables must be overridden
     */
    public function populate(array $values, bool $overrideExistingVars = false): void
    {
        $updateLoadedVars = false;
        $loadedVars = array_flip(explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? ''));
        foreach ($values as $name => $value) {
            $notHttpName = 0 !== strpos($name, 'HTTP_');
            // don't check existence with getenv() because of thread safety issues
            if (!isset($loadedVars[$name]) && (!$overrideExistingVars && (isset($_ENV[$name]) || (isset($_SERVER[$name]) && $notHttpName)))) {
                continue;
            }
            if ($this->usePutenv) {
                putenv("$name=$value");
            }
            $_ENV[$name] = $value;
            if ($notHttpName) {
                $_SERVER[$name] = $value;
            }
            if (!isset($loadedVars[$name])) {
                $loadedVars[$name] = $updateLoadedVars = true;
            }
        }
        if ($updateLoadedVars) {
            unset($loadedVars['']);
            $loadedVars = implode(',', array_keys($loadedVars));
            $_ENV['SYMFONY_DOTENV_VARS'] = $_SERVER['SYMFONY_DOTENV_VARS'] = $loadedVars;
            if ($this->usePutenv) {
                putenv('SYMFONY_DOTENV_VARS='.$loadedVars);
            }
        }
    }
    /**
     * Parses the contents of an .env file.
     *
     * @param string $data The data to be parsed
     * @param string $path The original file name where data where stored (used for more meaningful error messages)
     *
     * @return array An array of env variables
     *
     * @throws FormatException when a file has a syntax error
     */
    public function parse(string $data, string $path = '.env'): array
    {
        $this->path = $path;
        $this->data = str_replace(["\r\n", "\r"], "\n", $data);
        $this->lineno = 1;
        $this->cursor = 0;
        $this->end = \strlen($this->data);
        $state = self::STATE_VARNAME;
        $this->values = [];
        $name = '';
        $this->skipEmptyLines();
        while ($this->cursor < $this->end) {
            switch ($state) {
                case self::STATE_VARNAME:
                    $name = $this->lexVarname();
                    $state = self::STATE_VALUE;
                    break;
                case self::STATE_VALUE:
                    $this->values[$name] = $this->lexValue();
                    $state = self::STATE_VARNAME;
                    break;
            }
        }
        if (self::STATE_VALUE === $state) {
            $this->values[$name] = '';
        }
        try {
            return $this->values;
        } finally {
            $this->values = [];
            $this->data = null;
            $this->path = null;
        }
    }
    private function lexVarname(): string
    {
        // var name + optional export
        if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A', $this->data, $matches, 0, $this->cursor)) {
            throw $this->createFormatException('Invalid character in variable name');
        }
        $this->moveCursor($matches[0]);
        if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) {
            if ($matches[1]) {
                throw $this->createFormatException('Unable to unset an environment variable');
            }
            throw $this->createFormatException('Missing = in the environment variable declaration');
        }
        if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
            throw $this->createFormatException('Whitespace characters are not supported after the variable name');
        }
        if ('=' !== $this->data[$this->cursor]) {
            throw $this->createFormatException('Missing = in the environment variable declaration');
        }
        ++$this->cursor;
        return $matches[2];
    }
    private function lexValue(): string
    {
        if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, 0, $this->cursor)) {
            $this->moveCursor($matches[0]);
            $this->skipEmptyLines();
            return '';
        }
        if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
            throw $this->createFormatException('Whitespace are not supported before the value');
        }
        $loadedVars = array_flip(explode(',', isset($_SERVER['SYMFONY_DOTENV_VARS']) ? $_SERVER['SYMFONY_DOTENV_VARS'] : (isset($_ENV['SYMFONY_DOTENV_VARS']) ? $_ENV['SYMFONY_DOTENV_VARS'] : '')));
        unset($loadedVars['']);
        $v = '';
        do {
            if ("'" === $this->data[$this->cursor]) {
                $len = 0;
                do {
                    if ($this->cursor + ++$len === $this->end) {
                        $this->cursor += $len;
                        throw $this->createFormatException('Missing quote to end the value');
                    }
                } while ("'" !== $this->data[$this->cursor + $len]);
                $v .= substr($this->data, 1 + $this->cursor, $len - 1);
                $this->cursor += 1 + $len;
            } elseif ('"' === $this->data[$this->cursor]) {
                $value = '';
                if (++$this->cursor === $this->end) {
                    throw $this->createFormatException('Missing quote to end the value');
                }
                while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) {
                    $value .= $this->data[$this->cursor];
                    ++$this->cursor;
                    if ($this->cursor === $this->end) {
                        throw $this->createFormatException('Missing quote to end the value');
                    }
                }
                ++$this->cursor;
                $value = str_replace(['\\"', '\r', '\n'], ['"', "\r", "\n"], $value);
                $resolvedValue = $value;
                $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
                $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
                $resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
                $v .= $resolvedValue;
            } else {
                $value = '';
                $prevChr = $this->data[$this->cursor - 1];
                while ($this->cursor < $this->end && !\in_array($this->data[$this->cursor], ["\n", '"', "'"], true) && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) {
                    if ('\\' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && ('"' === $this->data[$this->cursor + 1] || "'" === $this->data[$this->cursor + 1])) {
                        ++$this->cursor;
                    }
                    $value .= $prevChr = $this->data[$this->cursor];
                    if ('$' === $this->data[$this->cursor] && isset($this->data[$this->cursor + 1]) && '(' === $this->data[$this->cursor + 1]) {
                        ++$this->cursor;
                        $value .= '('.$this->lexNestedExpression().')';
                    }
                    ++$this->cursor;
                }
                $value = rtrim($value);
                $resolvedValue = $value;
                $resolvedValue = $this->resolveVariables($resolvedValue, $loadedVars);
                $resolvedValue = $this->resolveCommands($resolvedValue, $loadedVars);
                $resolvedValue = str_replace('\\\\', '\\', $resolvedValue);
                if ($resolvedValue === $value && preg_match('/\s+/', $value)) {
                    throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
                }
                $v .= $resolvedValue;
                if ($this->cursor < $this->end && '#' === $this->data[$this->cursor]) {
                    break;
                }
            }
        } while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor]);
        $this->skipEmptyLines();
        return $v;
    }
    private function lexNestedExpression(): string
    {
        ++$this->cursor;
        $value = '';
        while ("\n" !== $this->data[$this->cursor] && ')' !== $this->data[$this->cursor]) {
            $value .= $this->data[$this->cursor];
            if ('(' === $this->data[$this->cursor]) {
                $value .= $this->lexNestedExpression().')';
            }
            ++$this->cursor;
            if ($this->cursor === $this->end) {
                throw $this->createFormatException('Missing closing parenthesis.');
            }
        }
        if ("\n" === $this->data[$this->cursor]) {
            throw $this->createFormatException('Missing closing parenthesis.');
        }
        return $value;
    }
    private function skipEmptyLines()
    {
        if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, 0, $this->cursor)) {
            $this->moveCursor($match[0]);
        }
    }
    private function resolveCommands(string $value, array $loadedVars): string
    {
        if (false === strpos($value, '$')) {
            return $value;
        }