Skip to content

Commit 97e1c1f

Browse files
committed
[Dotenv] added the component
0 parents  commit 97e1c1f

12 files changed

+721
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
3.3.0
5+
-----
6+
7+
* added the component

Dotenv.php

+338
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
24+
*/
25+
final class Dotenv
26+
{
27+
const VARNAME_REGEX = '(?i:[A-Z][A-Z0-9_]*+)';
28+
const STATE_VARNAME = 0;
29+
const STATE_VALUE = 1;
30+
31+
private $path;
32+
private $cursor;
33+
private $lineno;
34+
private $data;
35+
private $end;
36+
private $state;
37+
private $values;
38+
39+
/**
40+
* Loads one or several .env files.
41+
*
42+
* @param ...string A list of files to load
43+
*
44+
* @throws FormatException when a file has a syntax error
45+
* @throws PathException when a file does not exist or is not readable
46+
*/
47+
public function load(/*...$paths*/)
48+
{
49+
// func_get_args() to be replaced by a variadic argument for Symfony 4.0
50+
foreach (func_get_args() as $path) {
51+
if (!is_readable($path)) {
52+
throw new PathException($path);
53+
}
54+
55+
$this->populate($this->parse(file_get_contents($path), $path));
56+
}
57+
}
58+
59+
/**
60+
* Sets values as environment variables (via putenv, $_ENV, and $_SERVER).
61+
*
62+
* Note that existing environment variables are never overridden.
63+
*
64+
* @param array An array of env variables
65+
*/
66+
public function populate($values)
67+
{
68+
foreach ($values as $name => $value) {
69+
if (isset($_ENV[$name]) || false !== getenv($name)) {
70+
continue;
71+
}
72+
73+
putenv("$name=$value");
74+
$_ENV[$name] = $value;
75+
$_SERVER[$name] = $value;
76+
}
77+
}
78+
79+
/**
80+
* Parses the contents of an .env file.
81+
*
82+
* @param string $data The data to be parsed
83+
* @param string $path The original file name where data where stored (used for more meaningful error messages)
84+
*
85+
* @return array An array of env variables
86+
*
87+
* @throws FormatException when a file has a syntax error
88+
*/
89+
public function parse($data, $path = '.env')
90+
{
91+
$this->path = $path;
92+
$this->data = str_replace(array("\r\n", "\r"), "\n", $data);
93+
$this->lineno = 1;
94+
$this->cursor = 0;
95+
$this->end = strlen($this->data);
96+
$this->state = self::STATE_VARNAME;
97+
$this->values = array();
98+
$name = $value = '';
99+
100+
$this->skipEmptyLines();
101+
102+
while ($this->cursor < $this->end) {
103+
switch ($this->state) {
104+
case self::STATE_VARNAME:
105+
$name = $this->lexVarname();
106+
$this->state = self::STATE_VALUE;
107+
break;
108+
109+
case self::STATE_VALUE:
110+
$this->values[$name] = $this->lexValue();
111+
$this->state = self::STATE_VARNAME;
112+
break;
113+
}
114+
}
115+
116+
if (self::STATE_VALUE === $this->state) {
117+
$this->values[$name] = '';
118+
}
119+
120+
try {
121+
return $this->values;
122+
} finally {
123+
$this->values = array();
124+
}
125+
}
126+
127+
private function lexVarname()
128+
{
129+
// var name + optional export
130+
if (!preg_match('/(export[ \t]++)?('.self::VARNAME_REGEX.')/A', $this->data, $matches, 0, $this->cursor)) {
131+
throw $this->createFormatException('Invalid character in variable name');
132+
}
133+
$this->moveCursor($matches[0]);
134+
135+
if ($this->cursor === $this->end || "\n" === $this->data[$this->cursor] || '#' === $this->data[$this->cursor]) {
136+
if ($matches[1]) {
137+
throw $this->createFormatException('Unable to unset an environment variable');
138+
}
139+
140+
throw $this->createFormatException('Missing = in the environment variable declaration');
141+
}
142+
143+
if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
144+
throw $this->createFormatException('Whitespace are not supported after the variable name');
145+
}
146+
147+
if ('=' !== $this->data[$this->cursor]) {
148+
throw $this->createFormatException('Missing = in the environment variable declaration');
149+
}
150+
++$this->cursor;
151+
152+
return $matches[2];
153+
}
154+
155+
private function lexValue()
156+
{
157+
if (preg_match('/[ \t]*+(?:#.*)?$/Am', $this->data, $matches, null, $this->cursor)) {
158+
$this->moveCursor($matches[0]);
159+
$this->skipEmptyLines();
160+
161+
return '';
162+
}
163+
164+
if (' ' === $this->data[$this->cursor] || "\t" === $this->data[$this->cursor]) {
165+
throw $this->createFormatException('Whitespace are not supported before the value');
166+
}
167+
168+
$value = '';
169+
$singleQuoted = false;
170+
$notQuoted = false;
171+
if ("'" === $this->data[$this->cursor]) {
172+
$singleQuoted = true;
173+
++$this->cursor;
174+
while ("\n" !== $this->data[$this->cursor]) {
175+
if ("'" === $this->data[$this->cursor]) {
176+
if ($this->cursor + 1 === $this->end) {
177+
break;
178+
}
179+
if ("'" !== $this->data[$this->cursor + 1]) {
180+
break;
181+
}
182+
183+
++$this->cursor;
184+
}
185+
$value .= $this->data[$this->cursor];
186+
++$this->cursor;
187+
188+
if ($this->cursor === $this->end) {
189+
throw $this->createFormatException('Missing quote to end the value');
190+
}
191+
}
192+
if ("\n" === $this->data[$this->cursor]) {
193+
throw $this->createFormatException('Missing quote to end the value');
194+
}
195+
++$this->cursor;
196+
} elseif ('"' === $this->data[$this->cursor]) {
197+
++$this->cursor;
198+
while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) {
199+
$value .= $this->data[$this->cursor];
200+
++$this->cursor;
201+
202+
if ($this->cursor === $this->end) {
203+
throw $this->createFormatException('Missing quote to end the value');
204+
}
205+
}
206+
if ("\n" === $this->data[$this->cursor]) {
207+
throw $this->createFormatException('Missing quote to end the value');
208+
}
209+
++$this->cursor;
210+
$value = str_replace(array('\\\\', '\\"', '\r', '\n'), array('\\', '"', "\r", "\n"), $value);
211+
} else {
212+
$notQuoted = true;
213+
$prevChr = $this->data[$this->cursor - 1];
214+
while ($this->cursor < $this->end && "\n" !== $this->data[$this->cursor] && !((' ' === $prevChr || "\t" === $prevChr) && '#' === $this->data[$this->cursor])) {
215+
$value .= $prevChr = $this->data[$this->cursor];
216+
++$this->cursor;
217+
}
218+
$value = rtrim($value);
219+
}
220+
221+
$this->skipEmptyLines();
222+
223+
$currentValue = $value;
224+
if (!$singleQuoted) {
225+
$value = $this->resolveVariables($value);
226+
$value = $this->resolveCommands($value);
227+
}
228+
229+
if ($notQuoted && $currentValue == $value && preg_match('/\s+/', $value)) {
230+
throw $this->createFormatException('A value containing spaces must be surrounded by quotes');
231+
}
232+
233+
return $value;
234+
}
235+
236+
private function skipWhitespace()
237+
{
238+
$this->cursor += strspn($this->data, " \t", $this->cursor);
239+
}
240+
241+
private function skipEmptyLines()
242+
{
243+
if (preg_match('/(?:\s*+(?:#[^\n]*+)?+)++/A', $this->data, $match, null, $this->cursor)) {
244+
$this->moveCursor($match[0]);
245+
}
246+
}
247+
248+
private function resolveCommands($value)
249+
{
250+
if (false === strpos($value, '$')) {
251+
return $value;
252+
}
253+
254+
$regex = '/
255+
(\\\\)? # escaped with a backslash?
256+
\$
257+
(?<cmd>
258+
\( # require opening parenthesis
259+
([^()]|\g<cmd>)+ # allow any number of non-parens, or balanced parens (by nesting the <cmd> expression recursively)
260+
\) # require closing paren
261+
)
262+
/x';
263+
264+
return preg_replace_callback($regex, function ($matches) {
265+
if ('\\' === $matches[1]) {
266+
return substr($matches[0], 1);
267+
}
268+
269+
if ('\\' === DIRECTORY_SEPARATOR) {
270+
throw new \LogicException('Resolving commands is not supported on Windows.');
271+
}
272+
273+
if (!class_exists(Process::class)) {
274+
throw new \LogicException('Resolving commands requires the Symfony Process component.');
275+
}
276+
277+
$process = new Process('echo '.$matches[0]);
278+
$process->inheritEnvironmentVariables(true);
279+
$process->setEnv($this->values);
280+
try {
281+
$process->mustRun();
282+
} catch (ProcessException $e) {
283+
throw $this->createFormatException(sprintf('Issue expanding a command (%s)', $process->getErrorOutput()));
284+
}
285+
286+
return preg_replace('/[\r\n]+$/', '', $process->getOutput());
287+
}, $value);
288+
}
289+
290+
private function resolveVariables($value)
291+
{
292+
if (false === strpos($value, '$')) {
293+
return $value;
294+
}
295+
296+
$regex = '/
297+
(\\\\)? # escaped with a backslash?
298+
\$
299+
(?!\() # no opening parenthesis
300+
(\{)? # optional brace
301+
('.self::VARNAME_REGEX.') # var name
302+
(\})? # optional closing brace
303+
/x';
304+
305+
$value = preg_replace_callback($regex, function ($matches) {
306+
if ('\\' === $matches[1]) {
307+
return substr($matches[0], 1);
308+
}
309+
310+
if ('{' === $matches[2] && !isset($matches[4])) {
311+
throw $this->createFormatException('Unclosed braces on variable expansion');
312+
}
313+
314+
$name = $matches[3];
315+
$value = isset($this->values[$name]) ? $this->values[$name] : (isset($_ENV[$name]) ? isset($_ENV[$name]) : (string) getenv($name));
316+
317+
if (!$matches[2] && isset($matches[4])) {
318+
$value .= '}';
319+
}
320+
321+
return $value;
322+
}, $value);
323+
324+
// unescape $
325+
return str_replace('\\$', '$', $value);
326+
}
327+
328+
private function moveCursor($text)
329+
{
330+
$this->cursor += strlen($text);
331+
$this->lineno += substr_count($text, "\n");
332+
}
333+
334+
private function createFormatException($message)
335+
{
336+
return new FormatException($message, new FormatExceptionContext($this->data, $this->path, $this->lineno, $this->cursor));
337+
}
338+
}

Exception/ExceptionInterface.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
18+
*/
19+
interface ExceptionInterface
20+
{
21+
}

0 commit comments

Comments
 (0)