├── LICENSE
├── README.md
├── composer.json
├── phpcs.xml
└── src
├── ArrayFile.php
├── Contracts
├── DataFileInterface.php
├── DataFileLexerInterface.php
└── DataFilePrinterInterface.php
├── DataFile.php
├── EnvFile.php
├── Exceptions
├── ConfigWriterException.php
└── EnvParserException.php
├── Parser
├── EnvLexer.php
├── PHPConstant.php
└── PHPFunction.php
└── Printer
├── ArrayPrinter.php
└── EnvPrinter.php
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Winter CMS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Config Writer
2 |
3 | [](https://github.com/wintercms/laravel-config-writer/releases)
4 | [](https://github.com/wintercms/laravel-config-writer/actions)
5 | [](https://packagist.org/packages/winter/laravel-config-writer)
6 | [](https://discord.gg/D5MFSPH6Ux)
7 |
8 | A utility to easily create and modify Laravel-style PHP configuration files and environment files whilst maintaining the formatting and comments contained within. This utility works by parsing the configuration files using the [PHP Parser library](https://github.com/nikic/php-parser) to convert the configuration into an abstract syntax tree, then carefully modifying the configuration values as required.
9 |
10 | This library was originally written as part of the [Storm library](https://github.com/wintercms/storm) in [Winter CMS](https://wintercms.com), but has since been extracted and repurposed as a standalone library.
11 |
12 | ## Installation
13 |
14 | ```
15 | composer require winter/laravel-config-writer
16 | ```
17 |
18 | ## Usage
19 |
20 | ### PHP array files
21 |
22 | You can modify Laravel-style PHP configuration files - PHP files that return a single array - by using the `Winter\LaravelConfigWriter\ArrayFile` class. Use the `open` method to open an existing file for modification, or to create a new config file.
23 |
24 | ```php
25 | use Winter\LaravelConfigWriter\ArrayFile;
26 |
27 | $config = ArrayFile::open(base_path('config/app.php'));
28 | ```
29 |
30 | You can set values using the `set` method. This method can be used fluently, or can be called with a single key and value or an array of keys and values.
31 |
32 | ```php
33 | $config->set('name', 'Winter CMS');
34 |
35 | $config
36 | ->set('locale', 'en_US')
37 | ->set('fallbackLocale', 'en');
38 |
39 | $config->set([
40 | 'trustedHosts' => true,
41 | 'trustedProxies' => '*',
42 | ]);
43 | ```
44 |
45 | You can also set deep values in an array value by specifying the key in dot notation, or as a nested array.
46 |
47 | ```php
48 | $config->set('connections.mysql.host', 'localhost');
49 |
50 | $config->set([
51 | 'connections' => [
52 | 'sqlite' => [
53 | 'database' => 'database.sqlite',
54 | 'driver' => 'sqlite',
55 | 'foreign_key_constraints' => true,
56 | 'prefix' => '',
57 | 'url' => null,
58 | ],
59 | ],
60 | ]);
61 | ```
62 |
63 | To finalise all your changes, use the `write` method to write the changes to the open file.
64 |
65 | ```php
66 | $config->write();
67 | ```
68 |
69 | If desired, you may also write the changes to another file altogether.
70 |
71 | ```php
72 | $config->write('path/to/newfile.php');
73 | ```
74 |
75 | Or you can simply render the changes as a string.
76 |
77 | ```php
78 | $config->render();
79 | ```
80 |
81 | #### Function calls as values
82 |
83 | Function calls can be added to your configuration file by using the `function` method. The first parameter of the `function` method defines the function to call, and the second parameter accepts an array of parameters to provide to the function.
84 |
85 | ```php
86 | $config->set('name', $config->function('env', ['APP_NAME', 'Winter CMS']));
87 | ```
88 |
89 | #### Constants as values
90 |
91 | Constants can be added to your configuration file by using the `constant` method. The only parameter required is the name of the constant.
92 |
93 | ```php
94 | $config->set('foo.bar', $config->constant('My\Class::CONSTANT'));
95 | ```
96 |
97 | #### Sorting the configuration file
98 |
99 | You can sort the configuration keys alphabetically by using the `sort` method. This will sort all current configuration values.
100 |
101 | ```php
102 | $config->sort();
103 | ```
104 |
105 | By default, this will sort the keys alphabetically in ascending order. To sort in the opposite direction, include the `ArrayFile::SORT_DESC` parameter.
106 |
107 | ```php
108 | $config->sort(ArrayFile::SORT_DESC);
109 | ```
110 |
111 | ### Environment files
112 |
113 | This utility library also allows manipulation of environment files, typically found as `.env` files in a project. The `Winter\LaravelConfigWriter\EnvFile::open()` method allows you to open or create an environment file for modification.
114 |
115 | ```php
116 | use Winter\LaravelConfigWriter\EnvFile;
117 |
118 | $env = EnvFile::open(base_path('.env'));
119 | ```
120 |
121 | You can set values using the `set` method. This method can be used fluently, or can be called with a single key and value or an array of keys and values.
122 |
123 | ```php
124 | $env->set('APP_NAME', 'Winter CMS');
125 |
126 | $env
127 | ->set('APP_URL', 'https://wintercms.com')
128 | ->set('APP_ENV', 'production');
129 |
130 | $env->set([
131 | 'DB_CONNECTION' => 'sqlite',
132 | 'DB_DATABASE' => 'database.sqlite',
133 | ]);
134 | ```
135 |
136 | > **Note:** Arrays are not supported in environment files.
137 |
138 | You can add an empty line into the environment file by using the `addEmptyLine` method. This allows you to separate groups of environment variables.
139 |
140 | ```php
141 | $env->set('FOO', 'bar');
142 | $env->addEmptyLine();
143 | $env->set('BAR', 'foo');
144 | ```
145 |
146 | To finalise all your changes, use the `write` method to write the changes to the open file.
147 |
148 | ```php
149 | $env->write();
150 | ```
151 |
152 | If desired, you may also write the changes to another file altogether.
153 |
154 | ```php
155 | $env->write(base_path('.env.local'));
156 | ```
157 |
158 | Or you can simply render the changes as a string.
159 |
160 | ```php
161 | $env->render();
162 | ```
163 |
164 | ## License
165 |
166 | This utility library is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
167 |
168 | ## Security vulnerabilities
169 |
170 | Please review our [security policy](https://github.com/wintercms/winter/security/policy) on how to report security vulnerabilities.
171 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "winter/laravel-config-writer",
3 | "description": "Utility to create and update Laravel config and .env files",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Jack Wilkinson",
9 | "email": "me@jaxwilko.com",
10 | "role": "Original author"
11 | },
12 | {
13 | "name": "Winter CMS Maintainers",
14 | "homepage": "https://wintercms.com",
15 | "role": "Maintainers"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.4.0 || ^8.0",
20 | "nikic/php-parser": "^4.10"
21 | },
22 | "require-dev": {
23 | "phpstan/phpstan": "^1.6",
24 | "phpunit/phpunit": "^9.5"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "Winter\\LaravelConfigWriter\\": "src/"
29 | }
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "Winter\\LaravelConfigWriter\\Tests\\": "tests/"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | The coding standard for Winter CMS.
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | */tests/*
27 |
28 |
29 |
30 |
34 | */tests/*
35 |
36 |
37 |
38 |
39 |
40 | src/
41 | tests/
42 |
43 |
44 | */vendor/*
45 |
46 | tests/fixtures/*
47 |
48 |
--------------------------------------------------------------------------------
/src/ArrayFile.php:
--------------------------------------------------------------------------------
1 | astReturnIndex = $this->getAstReturnIndex($ast);
56 |
57 | if (is_null($this->astReturnIndex)) {
58 | throw new \InvalidArgumentException('ArrayFiles must start with a return statement');
59 | }
60 |
61 | $this->ast = $ast;
62 | $this->lexer = $lexer;
63 | $this->filePath = $filePath;
64 | $this->printer = $printer ?? new ArrayPrinter();
65 | }
66 |
67 | /**
68 | * Return a new instance of `ArrayFile` ready for modification of the file.
69 | *
70 | * @throws \InvalidArgumentException if the provided path doesn't exist and $throwIfMissing is true
71 | * @throws ConfigWriterException if the provided path is unable to be parsed
72 | * @return static
73 | */
74 | public static function open(string $filePath, bool $throwIfMissing = false)
75 | {
76 | $exists = file_exists($filePath);
77 |
78 | if (!$exists && $throwIfMissing) {
79 | throw new \InvalidArgumentException('file not found');
80 | }
81 |
82 | $lexer = new Lexer\Emulative([
83 | 'usedAttributes' => [
84 | 'comments',
85 | 'startTokenPos',
86 | 'startLine',
87 | 'endTokenPos',
88 | 'endLine'
89 | ]
90 | ]);
91 | $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer);
92 |
93 | try {
94 | $ast = $parser->parse(
95 | $exists
96 | ? file_get_contents($filePath)
97 | : sprintf('set('property.key.value', 'example');
111 | * // or
112 | * $config->set([
113 | * 'property.key1.value' => 'example',
114 | * 'property.key2.value' => 'example'
115 | * ]);
116 | * ```
117 | *
118 | * @param string|array $key
119 | * @param mixed $value
120 | * @return static
121 | */
122 | public function set($key, $value = null)
123 | {
124 | if (is_array($key)) {
125 | foreach ($key as $name => $value) {
126 | $this->set($name, $value);
127 | }
128 |
129 | return $this;
130 | }
131 |
132 | // try to find a reference to ast object
133 | list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[$this->astReturnIndex]->expr);
134 |
135 | $valueType = $this->getType($value);
136 |
137 | // part of a path found
138 | if ($target && $remaining) {
139 | $target->value->items[] = $this->makeArrayItem(implode('.', $remaining), $valueType, $value);
140 | return $this;
141 | }
142 |
143 | // path to not found
144 | if (is_null($target)) {
145 | $this->ast[$this->astReturnIndex]->expr->items[] = $this->makeArrayItem($key, $valueType, $value);
146 | return $this;
147 | }
148 |
149 | if (!isset($target->value)) {
150 | return $this;
151 | }
152 |
153 | // special handling of function objects
154 | if (get_class($target->value) === FuncCall::class && $valueType !== 'function') {
155 | if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) {
156 | return $this;
157 | }
158 | /* @phpstan-ignore-next-line */
159 | if (isset($target->value->args[0]) && !isset($target->value->args[1])) {
160 | $target->value->args[1] = new Arg($this->makeAstNode($valueType, $value));
161 | }
162 | $target->value->args[1]->value = $this->makeAstNode($valueType, $value);
163 | return $this;
164 | }
165 |
166 | // default update in place
167 | $target->value = $this->makeAstNode($valueType, $value);
168 |
169 | return $this;
170 | }
171 |
172 | /**
173 | * Creates either a simple array item or a recursive array of items
174 | *
175 | * @param mixed $value
176 | */
177 | protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem
178 | {
179 | return (strpos($key, '.') !== false)
180 | ? $this->makeAstArrayRecursive($key, $valueType, $value)
181 | : new ArrayItem(
182 | $this->makeAstNode($valueType, $value),
183 | $this->makeAstNode($this->getType($key), $key)
184 | );
185 | }
186 |
187 | /**
188 | * Generate an AST node, using `PhpParser` classes, for a value
189 | *
190 | * @param mixed $value
191 | * @throws \RuntimeException If $type is not one of 'string', 'boolean', 'integer', 'function', 'const', 'null', or 'array'
192 | * @return ConstFetch|LNumber|String_|Array_|FuncCall
193 | */
194 | protected function makeAstNode(string $type, $value)
195 | {
196 | switch (strtolower($type)) {
197 | case 'string':
198 | return new String_($value);
199 | case 'boolean':
200 | return new ConstFetch(new Name($value ? 'true' : 'false'));
201 | case 'integer':
202 | return new LNumber($value);
203 | case 'function':
204 | return new FuncCall(
205 | new Name($value->getName()),
206 | array_map(function ($arg) {
207 | return new Arg($this->makeAstNode($this->getType($arg), $arg));
208 | }, $value->getArgs())
209 | );
210 | case 'const':
211 | return new ConstFetch(new Name($value->getName()));
212 | case 'null':
213 | return new ConstFetch(new Name('null'));
214 | case 'array':
215 | return $this->castArray($value);
216 | default:
217 | throw new \RuntimeException("An unimlemented replacement type ($type) was encountered");
218 | }
219 | }
220 |
221 | /**
222 | * Cast an array to AST
223 | *
224 | * @param array $array
225 | */
226 | protected function castArray(array $array): Array_
227 | {
228 | return ($caster = function ($array, $ast) use (&$caster) {
229 | $useKeys = [];
230 | foreach (array_keys($array) as $i => $key) {
231 | $useKeys[$key] = (!is_numeric($key) || $key !== $i);
232 | }
233 | foreach ($array as $key => $item) {
234 | if (is_array($item)) {
235 | $ast->items[] = new ArrayItem(
236 | $caster($item, new Array_()),
237 | ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null)
238 | );
239 | continue;
240 | }
241 | $ast->items[] = new ArrayItem(
242 | $this->makeAstNode($this->getType($item), $item),
243 | ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null)
244 | );
245 | }
246 |
247 | return $ast;
248 | })($array, new Array_());
249 | }
250 |
251 | /**
252 | * Returns type of var passed
253 | *
254 | * @param mixed $var
255 | */
256 | protected function getType($var): string
257 | {
258 | if ($var instanceof PHPFunction) {
259 | return 'function';
260 | }
261 |
262 | if ($var instanceof PHPConstant) {
263 | return 'const';
264 | }
265 |
266 | return gettype($var);
267 | }
268 |
269 | /**
270 | * Returns an ArrayItem generated from a dot notation path
271 | *
272 | * @param string $key
273 | * @param string $valueType
274 | * @param mixed $value
275 | */
276 | protected function makeAstArrayRecursive(string $key, string $valueType, $value): ArrayItem
277 | {
278 | $path = array_reverse(explode('.', $key));
279 |
280 | $arrayItem = $this->makeAstNode($valueType, $value);
281 |
282 | foreach ($path as $index => $pathKey) {
283 | if (is_numeric($pathKey)) {
284 | $pathKey = (int) $pathKey;
285 | }
286 | $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode($this->getType($pathKey), $pathKey));
287 |
288 | if ($index !== array_key_last($path)) {
289 | $arrayItem = new Array_([$arrayItem]);
290 | }
291 | }
292 |
293 | return $arrayItem;
294 | }
295 |
296 | /**
297 | * Find the return position within the ast, returns null on encountering an unsupported ast stmt.
298 | *
299 | * @param Stmt[] $ast
300 | * @return int|null
301 | */
302 | protected function getAstReturnIndex(array $ast): ?int
303 | {
304 | foreach ($ast as $index => $item) {
305 | switch (get_class($item)) {
306 | case Stmt\Use_::class:
307 | case Stmt\Expression::class:
308 | case Stmt\Declare_::class:
309 | break;
310 | case Stmt\Return_::class:
311 | return $index;
312 | default:
313 | return null;
314 | }
315 | }
316 |
317 | return null;
318 | }
319 |
320 | /**
321 | * Attempt to find the parent object of the targeted path.
322 | * If the path cannot be found completely, return the nearest parent and the remainder of the path
323 | *
324 | * @param array $path
325 | * @param mixed $pointer
326 | * @param int $depth
327 | * @return array
328 | * @throws ConfigWriterException if trying to set a position that is already occupied by a value
329 | */
330 | protected function seek(array $path, &$pointer, int $depth = 0): array
331 | {
332 | if (!$pointer) {
333 | return [null, $path];
334 | }
335 |
336 | $key = array_shift($path);
337 |
338 | if (isset($pointer->value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) {
339 | throw new ConfigWriterException(sprintf(
340 | 'Illegal offset, you are trying to set a position occupied by a value (%s)',
341 | get_class($pointer->value)
342 | ));
343 | }
344 |
345 | foreach (($pointer->items ?? $pointer->value->items) as $index => &$item) {
346 | // loose checking to allow for int keys
347 | if ($item->key->value == $key) {
348 | if (!empty($path)) {
349 | return $this->seek($path, $item, ++$depth);
350 | }
351 |
352 | return [$item, []];
353 | }
354 | }
355 |
356 | array_unshift($path, $key);
357 |
358 | return [($depth > 0) ? $pointer : null, $path];
359 | }
360 |
361 | /**
362 | * Sort the config, supports: ArrayFile::SORT_ASC, ArrayFile::SORT_DESC, callable
363 | *
364 | * @param string|callable $mode
365 | * @throws \InvalidArgumentException if the provided sort type is not a callable or one of static::SORT_ASC or static::SORT_DESC
366 | */
367 | public function sort($mode = self::SORT_ASC): ArrayFile
368 | {
369 | if (is_callable($mode)) {
370 | usort($this->ast[0]->expr->items, $mode);
371 | return $this;
372 | }
373 |
374 | switch ($mode) {
375 | case static::SORT_ASC:
376 | case static::SORT_DESC:
377 | $this->sortRecursive($this->ast[0]->expr->items, $mode);
378 | break;
379 | default:
380 | throw new \InvalidArgumentException('Requested sort type is invalid');
381 | }
382 |
383 | return $this;
384 | }
385 |
386 | /**
387 | * Recursive sort an Array_ item array
388 | *
389 | * @param array $array
390 | */
391 | protected function sortRecursive(array &$array, string $mode): void
392 | {
393 | foreach ($array as &$item) {
394 | if (isset($item->value) && $item->value instanceof Array_) {
395 | $this->sortRecursive($item->value->items, $mode);
396 | }
397 | }
398 |
399 | usort($array, function ($a, $b) use ($mode) {
400 | return $mode === static::SORT_ASC
401 | ? $a->key->value <=> $b->key->value
402 | : $b->key->value <=> $a->key->value;
403 | });
404 | }
405 |
406 | /**
407 | * Write the current config to a file
408 | */
409 | public function write(string $filePath = null): void
410 | {
411 | if (!$filePath && $this->filePath) {
412 | $filePath = $this->filePath;
413 | }
414 |
415 | file_put_contents($filePath, $this->render());
416 | }
417 |
418 | /**
419 | * Returns a new instance of PHPFunction
420 | *
421 | * @param array $args
422 | */
423 | public function function(string $name, array $args): PHPFunction
424 | {
425 | return new PHPFunction($name, $args);
426 | }
427 |
428 | /**
429 | * Returns a new instance of PHPConstant
430 | */
431 | public function constant(string $name): PHPConstant
432 | {
433 | return new PHPConstant($name);
434 | }
435 |
436 | /**
437 | * Get the printed AST as PHP code
438 | */
439 | public function render(): string
440 | {
441 | return $this->printer->render($this->ast, $this->lexer) . "\n";
442 | }
443 | }
444 |
--------------------------------------------------------------------------------
/src/Contracts/DataFileInterface.php:
--------------------------------------------------------------------------------
1 | $key
18 | * @param mixed $value
19 | * @return static
20 | */
21 | public function set($key, $value = null);
22 |
23 | /**
24 | * Write the current data to a file
25 | */
26 | public function write(?string $filePath = null): void;
27 |
28 | /**
29 | * Get the printed data
30 | */
31 | public function render(): string;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Contracts/DataFileLexerInterface.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | public function parse(string $string): array;
20 | }
21 |
--------------------------------------------------------------------------------
/src/Contracts/DataFilePrinterInterface.php:
--------------------------------------------------------------------------------
1 | $ast
10 | * @return string
11 | */
12 | public function render(array $ast): string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/DataFile.php:
--------------------------------------------------------------------------------
1 | ast;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/EnvFile.php:
--------------------------------------------------------------------------------
1 | filePath = $filePath;
44 | $this->lexer = $lexer ?? new EnvLexer();
45 | $this->printer = $printer ?? new EnvPrinter();
46 | $this->ast = $this->parse($this->filePath);
47 | }
48 |
49 | /**
50 | * Return a new instance of `EnvFile` ready for modification of the file.
51 | *
52 | * @return static
53 | */
54 | public static function open(string $filePath)
55 | {
56 | return new static($filePath);
57 | }
58 |
59 | /**
60 | * Set a property within the env. Passing an array as param 1 is also supported.
61 | *
62 | * ```php
63 | * $env->set('APP_PROPERTY', 'example');
64 | * // or
65 | * $env->set([
66 | * 'APP_PROPERTY' => 'example',
67 | * 'DIF_PROPERTY' => 'example'
68 | * ]);
69 | * ```
70 | * @param string|array $key
71 | * @param mixed $value
72 | * @return static
73 | */
74 | public function set($key, $value = null)
75 | {
76 | if (is_array($key)) {
77 | foreach ($key as $item => $value) {
78 | $this->set($item, $value);
79 | }
80 | return $this;
81 | }
82 |
83 | foreach ($this->ast as $index => $item) {
84 | // Skip all but keys
85 | if ($item['token'] !== $this->lexer::T_ENV) {
86 | continue;
87 | }
88 |
89 | if ($item['value'] === $key) {
90 | if (
91 | !isset($this->ast[$index + 1])
92 | || !in_array($this->ast[$index + 1]['token'], [$this->lexer::T_VALUE, $this->lexer::T_QUOTED_VALUE])
93 | ) {
94 | // The next token was not a value, we need to create an empty value node to allow for value setting
95 | array_splice($this->ast, $index + 1, 0, [[
96 | 'token' => $this->lexer::T_VALUE,
97 | 'value' => ''
98 | ]]);
99 | }
100 |
101 | $this->ast[$index + 1]['value'] = $this->castValue($value);
102 |
103 | // Reprocess the token type to ensure old casting rules are still applied
104 | switch ($this->ast[$index + 1]['token']) {
105 | case $this->lexer::T_VALUE:
106 | if (
107 | strpos($this->ast[$index + 1]['value'], '"') !== false
108 | || strpos($this->ast[$index + 1]['value'], '\'') !== false
109 | ) {
110 | $this->ast[$index + 1]['token'] = $this->lexer::T_QUOTED_VALUE;
111 | }
112 | break;
113 | case $this->lexer::T_QUOTED_VALUE:
114 | if (is_null($value) || $value === true || $value === false) {
115 | $this->ast[$index + 1]['token'] = $this->lexer::T_VALUE;
116 | }
117 | break;
118 | }
119 |
120 | return $this;
121 | }
122 | }
123 |
124 | // We did not find the key in the AST, therefore we must create it
125 | $this->ast[] = [
126 | 'token' => $this->lexer::T_ENV,
127 | 'value' => $key
128 | ];
129 | $this->ast[] = [
130 | 'token' => (is_numeric($value) || is_bool($value)) ? $this->lexer::T_VALUE : $this->lexer::T_QUOTED_VALUE,
131 | 'value' => $this->castValue($value)
132 | ];
133 |
134 | // Add a new line
135 | $this->addEmptyLine();
136 |
137 | return $this;
138 | }
139 |
140 | /**
141 | * Push a newline onto the end of the env file
142 | */
143 | public function addEmptyLine(): EnvFile
144 | {
145 | $this->ast[] = [
146 | 'match' => PHP_EOL,
147 | 'token' => $this->lexer::T_WHITESPACE,
148 | ];
149 |
150 | return $this;
151 | }
152 |
153 | /**
154 | * Write the current env lines to a file
155 | */
156 | public function write(string $filePath = null): void
157 | {
158 | if (!$filePath) {
159 | $filePath = $this->filePath;
160 | }
161 |
162 | file_put_contents($filePath, $this->render());
163 | }
164 |
165 | /**
166 | * Get the env lines data as a string
167 | */
168 | public function render(): string
169 | {
170 | return $this->printer->render($this->ast);
171 | }
172 |
173 | /**
174 | * Parse a .env file, returns an array of the env file data and a key => position map
175 | *
176 | * @return array>
177 | */
178 | protected function parse(string $filePath): array
179 | {
180 | if (!is_file($filePath)) {
181 | return [];
182 | }
183 |
184 | $contents = file_get_contents($filePath);
185 |
186 | return $this->lexer->parse($contents);
187 | }
188 |
189 | /**
190 | * Get the variables from the current env lines data as an associative array
191 | *
192 | * @return array
193 | */
194 | public function getVariables(): array
195 | {
196 | $env = [];
197 |
198 | foreach ($this->ast as $index => $item) {
199 | if ($item['token'] !== $this->lexer::T_ENV) {
200 | continue;
201 | }
202 |
203 | if (!(
204 | isset($this->ast[$index + 1])
205 | && in_array($this->ast[$index + 1]['token'], [$this->lexer::T_VALUE, $this->lexer::T_QUOTED_VALUE])
206 | )) {
207 | continue;
208 | }
209 |
210 | $env[$item['value']] = trim($this->ast[$index + 1]['value']);
211 | }
212 |
213 | return $env;
214 | }
215 |
216 | /**
217 | * Cast values to strings
218 | *
219 | * @param mixed $value
220 | */
221 | protected function castValue($value): string
222 | {
223 | if (is_null($value)) {
224 | return 'null';
225 | }
226 |
227 | if ($value === true) {
228 | return 'true';
229 | }
230 |
231 | if ($value === false) {
232 | return 'false';
233 | }
234 |
235 | return str_replace('"', '\"', $value);
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/Exceptions/ConfigWriterException.php:
--------------------------------------------------------------------------------
1 | self::T_WHITESPACE,
12 | '/^(#.*)/' => self::T_COMMENT,
13 | '/^(\w+)/s' => self::T_ENV,
14 | '/^="([^"\\\]*(?:\\\.[^"\\\]*)*)"/s' => self::T_QUOTED_VALUE,
15 | '/^\=(.*)/' => self::T_VALUE,
16 | ];
17 |
18 | /**
19 | * Parses an array of lines into an AST
20 | *
21 | * @param string $string
22 | * @return array|array[]
23 | * @throws EnvParserException
24 | */
25 | public function parse(string $string): array
26 | {
27 | $tokens = [];
28 | $offset = 0;
29 | do {
30 | $result = $this->match($string, $offset);
31 |
32 | if (is_null($result)) {
33 | throw new EnvParserException("Unable to parse file, failed at: " . $offset . ".");
34 | }
35 |
36 | $tokens[] = $result;
37 |
38 | $offset += strlen($result['match']);
39 | } while ($offset < strlen($string));
40 |
41 | return $tokens;
42 | }
43 |
44 | /**
45 | * Parse a string against our token map and return a node
46 | *
47 | * @param string $str
48 | * @param int $offset
49 | * @return array|null
50 | */
51 | public function match(string $str, int $offset): ?array
52 | {
53 | $source = $str;
54 | $str = substr($str, $offset);
55 |
56 | foreach ($this->tokenMap as $pattern => $name) {
57 | if (!preg_match($pattern, $str, $matches)) {
58 | continue;
59 | }
60 |
61 | switch ($name) {
62 | case static::T_ENV:
63 | case static::T_VALUE:
64 | case static::T_QUOTED_VALUE:
65 | return [
66 | 'match' => $matches[0],
67 | 'value' => $matches[1] ?? '',
68 | 'token' => $name,
69 | ];
70 | case static::T_COMMENT:
71 | return [
72 | 'match' => $matches[0],
73 | 'token' => $name,
74 | ];
75 | case static::T_WHITESPACE:
76 | return [
77 | 'match' => $matches[1],
78 | 'token' => $name,
79 | ];
80 | }
81 | }
82 |
83 | return null;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Parser/PHPConstant.php:
--------------------------------------------------------------------------------
1 | name = $name;
18 | }
19 |
20 | /**
21 | * Get the const name
22 | */
23 | public function getName(): string
24 | {
25 | return $this->name;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Parser/PHPFunction.php:
--------------------------------------------------------------------------------
1 | Function arguments
17 | */
18 | protected $args;
19 |
20 | /**
21 | * @param string $name
22 | * @param array $args
23 | */
24 | public function __construct(string $name, array $args = [])
25 | {
26 | $this->name = $name;
27 | $this->args = $args;
28 | }
29 |
30 | /**
31 | * Get the function name
32 | *
33 | * @return string
34 | */
35 | public function getName(): string
36 | {
37 | return $this->name;
38 | }
39 |
40 | /**
41 | * Get the function arguments
42 | *
43 | * @return array
44 | */
45 | public function getArgs(): array
46 | {
47 | return $this->args;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Printer/ArrayPrinter.php:
--------------------------------------------------------------------------------
1 | $options Dictionary of formatting options
26 | */
27 | public function __construct(array $options = [])
28 | {
29 | if (!isset($options['shortArraySyntax'])) {
30 | $options['shortArraySyntax'] = true;
31 | }
32 |
33 | parent::__construct($options);
34 | }
35 |
36 | /**
37 | * Proxy of `prettyPrintFile` to allow for adding lexer token checking support during render.
38 | * Pretty prints a file of statements (includes the opening lexer = $lexer;
51 |
52 | $p = "prettyPrint($stmts);
53 |
54 | if ($stmts[0] instanceof Stmt\InlineHTML) {
55 | $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p);
56 | }
57 | if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) {
58 | $p = preg_replace('/<\?php$/', '', rtrim($p));
59 | }
60 |
61 | $this->lexer = null;
62 |
63 | return $p;
64 | }
65 |
66 | /**
67 | * @param array $nodes
68 | * @param bool $trailingComma
69 | * @return string
70 | */
71 | protected function pMaybeMultiline(array $nodes, bool $trailingComma = false)
72 | {
73 | if ($this->hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof Expr\ArrayItem)) {
74 | return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl;
75 | } else {
76 | return $this->pCommaSeparated($nodes);
77 | }
78 | }
79 |
80 | /**
81 | * Pretty prints a comma-separated list of nodes in multiline style, including comments.
82 | *
83 | * The result includes a leading newline and one level of indentation (same as pStmts).
84 | *
85 | * @param array $nodes Array of Nodes to be printed
86 | * @param bool $trailingComma Whether to use a trailing comma
87 | *
88 | * @return string Comma separated pretty printed nodes in multiline style
89 | */
90 | protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string
91 | {
92 | $this->indent();
93 |
94 | $result = '';
95 | $lastIdx = count($nodes) - 1;
96 | foreach ($nodes as $idx => $node) {
97 | if ($node !== null) {
98 | $comments = $node->getComments();
99 |
100 | if ($comments) {
101 | $result .= $this->pComments($comments);
102 | }
103 |
104 | $result .= $this->nl . $this->p($node);
105 | } else {
106 | $result = trim($result) . "\n";
107 | }
108 | if ($trailingComma || $idx !== $lastIdx) {
109 | $result .= ',';
110 | }
111 | }
112 |
113 | $this->outdent();
114 | return $result;
115 | }
116 |
117 | /**
118 | * Render an array expression
119 | *
120 | * @param Expr\Array_ $node Array expression node
121 | *
122 | * @return string Comma separated pretty printed nodes in multiline style
123 | */
124 | protected function pExpr_Array(Expr\Array_ $node): string
125 | {
126 | $default = $this->options['shortArraySyntax']
127 | ? Expr\Array_::KIND_SHORT
128 | : Expr\Array_::KIND_LONG;
129 |
130 | $ops = $node->getAttribute('kind', $default) === Expr\Array_::KIND_SHORT
131 | ? ['[', ']']
132 | : ['array(', ')'];
133 |
134 | if (!count($node->items) && $comments = $this->getNodeComments($node)) {
135 | // the array has no items, we can inject whatever we want
136 | return sprintf(
137 | '%s%s%s%s%s',
138 | // opening control char
139 | $ops[0],
140 | // indent and add nl string
141 | $this->indent(),
142 | // join all comments with nl string
143 | implode($this->nl, $comments),
144 | // outdent and add nl string
145 | $this->outdent(),
146 | // closing control char
147 | $ops[1]
148 | );
149 | }
150 |
151 | if ($comments = $this->getCommentsNotInArray($node)) {
152 | // array has items, we have detected comments not included within the array, therefore we have found
153 | // trailing comments and must append them to the end of the array
154 | return sprintf(
155 | '%s%s%s%s%s%s',
156 | // opening control char
157 | $ops[0],
158 | // render the children
159 | $this->pMaybeMultiline($node->items, true),
160 | // add 1 level of indentation
161 | str_repeat(' ', 4),
162 | // join all comments with the current indentation
163 | implode($this->nl . str_repeat(' ', 4), $comments),
164 | // add a trailing nl
165 | $this->nl,
166 | // closing control char
167 | $ops[1]
168 | );
169 | }
170 |
171 | // default return
172 | return $ops[0] . $this->pMaybeMultiline($node->items, true) . $ops[1];
173 | }
174 |
175 | /**
176 | * Increase indentation level.
177 | * Proxied to allow for nl return
178 | *
179 | * @return string
180 | */
181 | protected function indent(): string
182 | {
183 | $this->indentLevel += 4;
184 | $this->nl .= ' ';
185 | return $this->nl;
186 | }
187 |
188 | /**
189 | * Decrease indentation level.
190 | * Proxied to allow for nl return
191 | *
192 | * @return string
193 | */
194 | protected function outdent(): string
195 | {
196 | assert($this->indentLevel >= 4);
197 | $this->indentLevel -= 4;
198 | $this->nl = "\n" . str_repeat(' ', $this->indentLevel);
199 | return $this->nl;
200 | }
201 |
202 | /**
203 | * Get all comments that have not been attributed to a node within a node array
204 | *
205 | * @param Expr\Array_ $nodes Array of nodes
206 | *
207 | * @return array Comments found
208 | */
209 | protected function getCommentsNotInArray(Expr\Array_ $nodes): array
210 | {
211 | if (!$comments = $this->getNodeComments($nodes)) {
212 | return [];
213 | }
214 |
215 | return array_filter($comments, function ($comment) use ($nodes) {
216 | return !$this->commentInNodeList($nodes->items, $comment);
217 | });
218 | }
219 |
220 | /**
221 | * Recursively check if a comment exists in an array of nodes
222 | *
223 | * @param Node[] $nodes Array of nodes
224 | * @param string $comment The comment to search for
225 | *
226 | * @return bool
227 | */
228 | protected function commentInNodeList(array $nodes, string $comment): bool
229 | {
230 | foreach ($nodes as $node) {
231 | if (isset($node->value) && $node->value instanceof Expr\Array_ && $this->commentInNodeList($node->value->items, $comment)) {
232 | return true;
233 | }
234 | if ($nodeComments = $node->getAttribute('comments')) {
235 | foreach ($nodeComments as $nodeComment) {
236 | if ($nodeComment->getText() === $comment) {
237 | return true;
238 | }
239 | }
240 | }
241 | }
242 |
243 | return false;
244 | }
245 |
246 | /**
247 | * Check the lexer tokens for comments within the node's start & end position
248 | *
249 | * @param Node $node Node to check
250 | *
251 | * @return array|null
252 | */
253 | protected function getNodeComments(Node $node): ?array
254 | {
255 | $tokens = $this->lexer->getTokens();
256 | $pos = $node->getAttribute('startTokenPos');
257 | $end = $node->getAttribute('endTokenPos');
258 | $endLine = $node->getAttribute('endLine');
259 | $content = [];
260 |
261 | while (++$pos < $end) {
262 | if (!isset($tokens[$pos]) || (!is_array($tokens[$pos]) && $tokens[$pos] !== ',')) {
263 | break;
264 | }
265 |
266 | if ($tokens[$pos][0] === T_WHITESPACE || $tokens[$pos] === ',') {
267 | continue;
268 | }
269 |
270 | list($type, $string, $line) = $tokens[$pos];
271 |
272 | if ($line > $endLine) {
273 | break;
274 | }
275 |
276 | if ($type === T_COMMENT || $type === T_DOC_COMMENT) {
277 | $content[] = $string;
278 | } elseif ($content) {
279 | break;
280 | }
281 | }
282 |
283 | return empty($content) ? null : $content;
284 | }
285 |
286 | /**
287 | * Prints reformatted text of the passed comments.
288 | *
289 | * @param \PhpParser\Comment[] $comments List of comments
290 | *
291 | * @return string Reformatted text of comments
292 | */
293 | protected function pComments(array $comments): string
294 | {
295 | $formattedComments = [];
296 |
297 | foreach ($comments as $comment) {
298 | $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText());
299 | }
300 |
301 | $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : '';
302 |
303 | return "\n" . $this->nl . trim($padding . implode($this->nl, $formattedComments)) . "\n";
304 | }
305 |
306 | /**
307 | * @param Expr\Include_ $node
308 | * @return string
309 | */
310 | protected function pExpr_Include(Expr\Include_ $node)
311 | {
312 | static $map = [
313 | Expr\Include_::TYPE_INCLUDE => 'include',
314 | Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once',
315 | Expr\Include_::TYPE_REQUIRE => 'require',
316 | Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once',
317 | ];
318 |
319 | return $map[$node->type] . '(' . $this->p($node->expr) . ')';
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/src/Printer/EnvPrinter.php:
--------------------------------------------------------------------------------
1 |