8000 [Env] added the component · symfony/symfony@39f790a · GitHub
[go: up one dir, main page]

Skip to content

Commit 39f790a

Browse files
committed
[Env] added the component
1 parent e66e6af commit 39f790a

13 files changed

+749
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"symfony/debug-bundle": "self.version",
4141
"symfony/doctrine-bridge": "self.version",
4242
"symfony/dom-crawler": "self.version",
43+
"symfony/env": "self.version",
4344
"symfony/event-dispatcher": "self.version",
4445
"symfony/expression-language": "self.version",
4546
"symfony/filesystem": "self.version",

src/Symfony/Component/Env/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Lines changed: 7 additions & 0 deletions
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

src/Symfony/Component/Env/Dotenv.php

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

0 commit comments

Comments
 (0)
0