diff --git a/composer.json b/composer.json
index b9ecb636d..42b3e2513 100644
--- a/composer.json
+++ b/composer.json
@@ -42,6 +42,7 @@
"laravel/tinker": "^2.7",
"league/csv": "~9.1",
"nesbot/carbon": "^2.0",
+ "nikic/php-parser": "^4.10",
"scssphp/scssphp": "~1.0",
"symfony/yaml": "^6.0",
"twig/twig": "~3.0",
@@ -51,7 +52,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5.8",
"mockery/mockery": "^1.4.4",
- "squizlabs/php_codesniffer": "3.*",
+ "squizlabs/php_codesniffer": "^3.2",
"php-parallel-lint/php-parallel-lint": "^1.0",
"meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0",
"dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1"
diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php
index 9761b1cba..75f40831a 100644
--- a/src/Config/ConfigWriter.php
+++ b/src/Config/ConfigWriter.php
@@ -2,217 +2,49 @@
use Exception;
+use PhpParser\Error;
+use PhpParser\Lexer\Emulative;
+use PhpParser\ParserFactory;
+use Winter\Storm\Exception\SystemException;
+use Winter\Storm\Parse\PHP\ArrayFile;
+
/**
* Configuration rewriter
*
- * https://github.com/daftspunk/laravel-config-writer
+ * @see https://wintercms.com/docs/services/parser#data-file-array
*
* This class lets you rewrite array values inside a basic configuration file
* that returns a single array definition (a Laravel config file) whilst maintaining
* the integrity of the file, leaving comments and advanced settings intact.
- *
- * The following value types are supported for writing:
- * - strings
- * - integers
- * - booleans
- * - nulls
- * - single-dimension arrays
- *
- * To do:
- * - When an entry does not exist, provide a way to create it
- *
- * Pro Regextip: Use [\s\S] instead of . for multiline support
*/
class ConfigWriter
{
- public function toFile($filePath, $newValues, $useValidation = true)
- {
- $contents = file_get_contents($filePath);
- $contents = $this->toContent($contents, $newValues, $useValidation);
- file_put_contents($filePath, $contents);
- return $contents;
- }
-
- public function toContent($contents, $newValues, $useValidation = true)
- {
- $contents = $this->parseContent($contents, $newValues);
-
- if (!$useValidation) {
- return $contents;
- }
-
- $result = eval('?>'.$contents);
-
- foreach ($newValues as $key => $expectedValue) {
- $parts = explode('.', $key);
-
- $array = $result;
- foreach ($parts as $part) {
- if (!is_array($array) || !array_key_exists($part, $array)) {
- throw new Exception(sprintf('Unable to rewrite key "%s" in config, does it exist?', $key));
- }
-
- $array = $array[$part];
- }
- $actualValue = $array;
-
- if ($actualValue != $expectedValue) {
- throw new Exception(sprintf('Unable to rewrite key "%s" in config, rewrite failed', $key));
- }
- }
-
- return $contents;
- }
-
- protected function parseContent($contents, $newValues)
- {
- $result = $contents;
-
- foreach ($newValues as $path => $value) {
- $result = $this->parseContentValue($result, $path, $value);
- }
-
- return $result;
- }
-
- protected function parseContentValue($contents, $path, $value)
- {
- $result = $contents;
- $items = explode('.', $path);
- $key = array_pop($items);
- $replaceValue = $this->writeValueToPhp($value);
-
- $count = 0;
- $patterns = [];
- $patterns[] = $this->buildStringExpression($key, $items);
- $patterns[] = $this->buildStringExpression($key, $items, '"');
- $patterns[] = $this->buildConstantExpression($key, $items);
- $patterns[] = $this->buildArrayExpression($key, $items);
-
- foreach ($patterns as $pattern) {
- $result = preg_replace($pattern, '${1}${2}'.$replaceValue, $result, 1, $count);
-
- if ($count > 0) {
- break;
- }
- }
-
- return $result;
- }
-
- protected function writeValueToPhp($value)
- {
- if (is_string($value) && strpos($value, "'") === false) {
- $replaceValue = "'".$value."'";
- }
- elseif (is_string($value) && strpos($value, '"') === false) {
- $replaceValue = '"'.$value.'"';
- }
- elseif (is_bool($value)) {
- $replaceValue = ($value ? 'true' : 'false');
- }
- elseif (is_null($value)) {
- $replaceValue = 'null';
- }
- elseif (is_array($value) && count($value) === count($value, COUNT_RECURSIVE)) {
- $replaceValue = $this->writeArrayToPhp($value);
- }
- else {
- $replaceValue = $value;
- }
-
- $replaceValue = str_replace('$', '\$', $replaceValue);
-
- return $replaceValue;
- }
-
- protected function writeArrayToPhp($array)
+ public function toFile(string $filePath, array $newValues): string
{
- $result = [];
-
- foreach ($array as $value) {
- if (!is_array($value)) {
- $result[] = $this->writeValueToPhp($value);
- }
- }
-
- return '['.implode(', ', $result).']';
+ $arrayFile = ArrayFile::open($filePath)->set($newValues);
+ $arrayFile->write();
+ return $arrayFile->render();
}
- protected function buildStringExpression($targetKey, $arrayItems = [], $quoteChar = "'")
+ public function toContent(string $contents, $newValues): string
{
- $expression = [];
-
- // Opening expression for array items ($1)
- $expression[] = $this->buildArrayOpeningExpression($arrayItems);
-
- // The target key opening
- $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)['.$quoteChar.']';
-
- // The target value to be replaced ($2)
- $expression[] = '([^'.$quoteChar.']*)';
-
- // The target key closure
- $expression[] = '['.$quoteChar.']';
-
- return '/' . implode('', $expression) . '/';
- }
+ $lexer = new Emulative([
+ 'usedAttributes' => [
+ 'comments',
+ 'startTokenPos',
+ 'startLine',
+ 'endTokenPos',
+ 'endLine'
+ ]
+ ]);
+ $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer);
- /**
- * Common constants only (true, false, null, integers)
- */
- protected function buildConstantExpression($targetKey, $arrayItems = [])
- {
- $expression = [];
-
- // Opening expression for array items ($1)
- $expression[] = $this->buildArrayOpeningExpression($arrayItems);
-
- // The target key opening ($2)
- $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)';
-
- // The target value to be replaced ($3)
- $expression[] = '([tT][rR][uU][eE]|[fF][aA][lL][sS][eE]|[nN][uU][lL]{2}|[\d]+)';
-
- return '/' . implode('', $expression) . '/';
- }
-
- /**
- * Single level arrays only
- */
- protected function buildArrayExpression($targetKey, $arrayItems = [])
- {
- $expression = [];
-
- // Opening expression for array items ($1)
- $expression[] = $this->buildArrayOpeningExpression($arrayItems);
-
- // The target key opening ($2)
- $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)';
-
- // The target value to be replaced ($3)
- $expression[] = '(?:[aA][rR]{2}[aA][yY]\(|[\[])([^\]|)]*)[\]|)]';
-
- return '/' . implode('', $expression) . '/';
- }
-
- protected function buildArrayOpeningExpression($arrayItems)
- {
- if (count($arrayItems)) {
- $itemOpen = [];
- foreach ($arrayItems as $item) {
- // The left hand array assignment
- $itemOpen[] = '[\'|"]'.$item.'[\'|"]\s*=>\s*(?:[aA][rR]{2}[aA][yY]\(|[\[])';
- }
-
- // Capture all opening array (non greedy)
- $result = '(' . implode('[\s\S]*', $itemOpen) . '[\s\S]*?)';
- }
- else {
- // Gotta capture something for $1
- $result = '()';
+ try {
+ $ast = $parser->parse($contents);
+ } catch (Error $e) {
+ throw new SystemException($e);
}
- return $result;
+ return (new ArrayFile($ast, $lexer, null))->set($newValues)->render();
}
}
diff --git a/src/Foundation/Console/KeyGenerateCommand.php b/src/Foundation/Console/KeyGenerateCommand.php
index 84ffd0845..931ed226c 100644
--- a/src/Foundation/Console/KeyGenerateCommand.php
+++ b/src/Foundation/Console/KeyGenerateCommand.php
@@ -1,82 +1,48 @@
files = $files;
+ $env = EnvFile::open($this->laravel->environmentFilePath());
+ $env->set('APP_KEY', $key);
+ $env->write();
}
/**
- * Execute the console command.
+ * Confirm before proceeding with the action.
*
- * @return void
- */
- public function handle()
- {
- $key = $this->generateRandomKey();
-
- if ($this->option('show')) {
- return $this->line(''.$key.'');
- }
-
- // Next, we will replace the application key in the config file so it is
- // automatically setup for this developer. This key gets generated using a
- // secure random byte generator and is later base64 encoded for storage.
- if (!$this->setKeyInConfigFile($key)) {
- return;
- }
-
- $this->laravel['config']['app.key'] = $key;
-
- $this->info("Application key [$key] set successfully.");
- }
-
- /**
- * Set the application key in the config file.
+ * This method only asks for confirmation in production.
*
- * @param string $key
+ * @param string $warning
+ * @param \Closure|bool|null $callback
* @return bool
*/
- protected function setKeyInConfigFile($key)
+ public function confirmToProceed($warning = 'Application In Production!', $callback = null)
{
- if (!$this->confirmToProceed()) {
- return false;
+ if ($this->hasOption('force') && $this->option('force')) {
+ return true;
}
- $currentKey = $this->laravel['config']['app.key'];
+ $this->alert('An application key is already set!');
- list($path, $contents) = $this->getKeyFile();
+ $confirmed = $this->confirm('Do you really wish to run this command?');
- $contents = str_replace($currentKey, $key, $contents);
+ if (!$confirmed) {
+ $this->comment('Command Canceled!');
- $this->files->put($path, $contents);
+ return false;
+ }
return true;
}
-
- /**
- * Get the key file and contents.
- *
- * @return array
- */
- protected function getKeyFile()
- {
- $env = $this->option('env') ? $this->option('env').'/' : '';
-
- $contents = $this->files->get($path = $this->laravel['path.config']."/{$env}app.php");
-
- return [$path, $contents];
- }
}
diff --git a/src/Parse/Contracts/DataFileInterface.php b/src/Parse/Contracts/DataFileInterface.php
new file mode 100644
index 000000000..949fc0066
--- /dev/null
+++ b/src/Parse/Contracts/DataFileInterface.php
@@ -0,0 +1,24 @@
+filePath = $filePath;
+
+ list($this->env, $this->map) = $this->parse($filePath);
+ }
+
+ /**
+ * Return a new instance of `EnvFile` ready for modification of the file.
+ */
+ public static function open(?string $filePath = null): static
+ {
+ if (!$filePath) {
+ $filePath = base_path('.env');
+ }
+
+ return new static($filePath);
+ }
+
+ /**
+ * Set a property within the env. Passing an array as param 1 is also supported.
+ *
+ * ```php
+ * $env->set('APP_PROPERTY', 'example');
+ * // or
+ * $env->set([
+ * 'APP_PROPERTY' => 'example',
+ * 'DIF_PROPERTY' => 'example'
+ * ]);
+ * ```
+ */
+ public function set(array|string $key, $value = null): static
+ {
+ if (is_array($key)) {
+ foreach ($key as $item => $value) {
+ $this->set($item, $value);
+ }
+ return $this;
+ }
+
+ if (!isset($this->map[$key])) {
+ $this->env[] = [
+ 'type' => 'var',
+ 'key' => $key,
+ 'value' => $value
+ ];
+
+ $this->map[$key] = count($this->env) - 1;
+
+ return $this;
+ }
+
+ $this->env[$this->map[$key]]['value'] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Push a newline onto the end of the env file
+ */
+ public function addEmptyLine(): EnvFile
+ {
+ $this->env[] = [
+ 'type' => 'nl'
+ ];
+
+ return $this;
+ }
+
+ /**
+ * Write the current env lines to a fileh
+ */
+ public function write(string $filePath = null): void
+ {
+ if (!$filePath) {
+ $filePath = $this->filePath;
+ }
+
+ file_put_contents($filePath, $this->render());
+ }
+
+ /**
+ * Get the env lines data as a string
+ */
+ public function render(): string
+ {
+ $out = '';
+ foreach ($this->env as $env) {
+ switch ($env['type']) {
+ case 'comment':
+ $out .= $env['value'];
+ break;
+ case 'var':
+ $out .= $env['key'] . '=' . $this->escapeValue($env['value']);
+ break;
+ }
+
+ $out .= PHP_EOL;
+ }
+
+ return $out;
+ }
+
+ /**
+ * Wrap a value in quotes if needed
+ *
+ * @param mixed $value
+ */
+ protected function escapeValue($value): string
+ {
+ if (is_numeric($value)) {
+ return $value;
+ }
+
+ if ($value === true) {
+ return 'true';
+ }
+
+ if ($value === false) {
+ return 'false';
+ }
+
+ if ($value === null) {
+ return 'null';
+ }
+
+ switch ($value) {
+ case 'true':
+ case 'false':
+ case 'null':
+ return $value;
+ default:
+ // addslashes() wont work as it'll escape single quotes and they will be read literally
+ return '"' . Str::replace('"', '\"', $value) . '"';
+ }
+ }
+
+ /**
+ * Parse a .env file, returns an array of the env file data and a key => position map
+ */
+ protected function parse(string $filePath): array
+ {
+ if (!file_exists($filePath) || !($contents = file($filePath)) || !count($contents)) {
+ return [[], []];
+ }
+
+ $env = [];
+ $map = [];
+
+ foreach ($contents as $line) {
+ $type = !($line = trim($line))
+ ? 'nl'
+ : (
+ Str::startsWith($line, '#')
+ ? 'comment'
+ : 'var'
+ );
+
+ $entry = [
+ 'type' => $type
+ ];
+
+ if ($type === 'var') {
+ if (strpos($line, '=') === false) {
+ // if we cannot split the string, handle it the same as a comment
+ // i.e. inject it back into the file as is
+ $entry['type'] = $type = 'comment';
+ } else {
+ list($key, $value) = explode('=', $line);
+ $entry['key'] = trim($key);
+ $entry['value'] = trim($value, '"');
+ }
+ }
+
+ if ($type === 'comment') {
+ $entry['value'] = $line;
+ }
+
+ $env[] = $entry;
+ }
+
+ foreach ($env as $index => $item) {
+ if ($item['type'] !== 'var') {
+ continue;
+ }
+ $map[$item['key']] = $index;
+ }
+
+ return [$env, $map];
+ }
+
+ /**
+ * Get the variables from the current env lines data as an associative array
+ */
+ public function getVariables(): array
+ {
+ $env = [];
+
+ foreach ($this->env as $item) {
+ if ($item['type'] !== 'var') {
+ continue;
+ }
+ $env[$item['key']] = $item['value'];
+ }
+
+ return $env;
+ }
+}
diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php
new file mode 100644
index 000000000..fe18024aa
--- /dev/null
+++ b/src/Parse/PHP/ArrayFile.php
@@ -0,0 +1,435 @@
+astReturnIndex = $this->getAstReturnIndex($ast);
+
+ if (is_null($this->astReturnIndex)) {
+ throw new \InvalidArgumentException('ArrayFiles must start with a return statement');
+ }
+
+ $this->ast = $ast;
+ $this->lexer = $lexer;
+ $this->filePath = $filePath;
+ $this->printer = $printer ?? new ArrayPrinter();
+ }
+
+ /**
+ * Return a new instance of `ArrayFile` ready for modification of the file.
+ *
+ * @throws \InvalidArgumentException if the provided path doesn't exist and $throwIfMissing is true
+ * @throws SystemException if the provided path is unable to be parsed
+ */
+ public static function open(string $filePath, bool $throwIfMissing = false): static
+ {
+ $exists = file_exists($filePath);
+
+ if (!$exists && $throwIfMissing) {
+ throw new \InvalidArgumentException('file not found');
+ }
+
+ $lexer = new Lexer\Emulative([
+ 'usedAttributes' => [
+ 'comments',
+ 'startTokenPos',
+ 'startLine',
+ 'endTokenPos',
+ 'endLine'
+ ]
+ ]);
+ $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer);
+
+ try {
+ $ast = $parser->parse(
+ $exists
+ ? file_get_contents($filePath)
+ : sprintf('set('property.key.value', 'example');
+ * // or
+ * $config->set([
+ * 'property.key1.value' => 'example',
+ * 'property.key2.value' => 'example'
+ * ]);
+ * ```
+ */
+ public function set(string|array $key, $value = null): static
+ {
+ if (is_array($key)) {
+ foreach ($key as $name => $value) {
+ $this->set($name, $value);
+ }
+
+ return $this;
+ }
+
+ // try to find a reference to ast object
+ list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[$this->astReturnIndex]->expr);
+
+ $valueType = $this->getType($value);
+
+ // part of a path found
+ if ($target && $remaining) {
+ $target->value->items[] = $this->makeArrayItem(implode('.', $remaining), $valueType, $value);
+ return $this;
+ }
+
+ // path to not found
+ if (is_null($target)) {
+ $this->ast[$this->astReturnIndex]->expr->items[] = $this->makeArrayItem($key, $valueType, $value);
+ return $this;
+ }
+
+ if (!isset($target->value)) {
+ return $this;
+ }
+
+ // special handling of function objects
+ if (get_class($target->value) === FuncCall::class && $valueType !== 'function') {
+ if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) {
+ return $this;
+ }
+ if (isset($target->value->args[0]) && !isset($target->value->args[1])) {
+ $target->value->args[1] = new Arg($this->makeAstNode($valueType, $value));
+ }
+ $target->value->args[1]->value = $this->makeAstNode($valueType, $value);
+ return $this;
+ }
+
+ // default update in place
+ $target->value = $this->makeAstNode($valueType, $value);
+
+ return $this;
+ }
+
+ /**
+ * Creates either a simple array item or a recursive array of items
+ */
+ protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem
+ {
+ return (str_contains($key, '.'))
+ ? $this->makeAstArrayRecursive($key, $valueType, $value)
+ : new ArrayItem(
+ $this->makeAstNode($valueType, $value),
+ $this->makeAstNode($this->getType($key), $key)
+ );
+ }
+
+ /**
+ * Generate an AST node, using `PhpParser` classes, for a value
+ *
+ * @throws \RuntimeException If $type is not one of 'string', 'boolean', 'integer', 'function', 'const', 'null', or 'array'
+ * @return ConstFetch|LNumber|String_|Array_|FuncCall
+ */
+ protected function makeAstNode(string $type, $value)
+ {
+ switch (strtolower($type)) {
+ case 'string':
+ return new String_($value);
+ case 'boolean':
+ return new ConstFetch(new Name($value ? 'true' : 'false'));
+ case 'integer':
+ return new LNumber($value);
+ case 'function':
+ return new FuncCall(
+ new Name($value->getName()),
+ array_map(function ($arg) {
+ return new Arg($this->makeAstNode($this->getType($arg), $arg));
+ }, $value->getArgs())
+ );
+ case 'const':
+ return new ConstFetch(new Name($value->getName()));
+ case 'null':
+ return new ConstFetch(new Name('null'));
+ case 'array':
+ return $this->castArray($value);
+ default:
+ throw new \RuntimeException("An unimlemented replacement type ($type) was encountered");
+ }
+ }
+
+ /**
+ * Cast an array to AST
+ */
+ protected function castArray(array $array): Array_
+ {
+ return ($caster = function ($array, $ast) use (&$caster) {
+ $useKeys = [];
+ foreach (array_keys($array) as $i => $key) {
+ $useKeys[$key] = (!is_numeric($key) || $key !== $i);
+ }
+ foreach ($array as $key => $item) {
+ if (is_array($item)) {
+ $ast->items[] = new ArrayItem(
+ $caster($item, new Array_()),
+ ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null)
+ );
+ continue;
+ }
+ $ast->items[] = new ArrayItem(
+ $this->makeAstNode($this->getType($item), $item),
+ ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null)
+ );
+ }
+
+ return $ast;
+ })($array, new Array_());
+ }
+
+ /**
+ * Returns type of var passed
+ *
+ * @param mixed $var
+ */
+ protected function getType($var): string
+ {
+ if ($var instanceof PHPFunction) {
+ return 'function';
+ }
+
+ if ($var instanceof PHPConstant) {
+ return 'const';
+ }
+
+ return gettype($var);
+ }
+
+ /**
+ * Returns an ArrayItem generated from a dot notation path
+ *
+ * @param string $key
+ * @param string $valueType
+ * @param mixed $value
+ */
+ protected function makeAstArrayRecursive(string $key, string $valueType, $value): ArrayItem
+ {
+ $path = array_reverse(explode('.', $key));
+
+ $arrayItem = $this->makeAstNode($valueType, $value);
+
+ foreach ($path as $index => $pathKey) {
+ if (is_numeric($pathKey)) {
+ $pathKey = (int) $pathKey;
+ }
+ $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode($this->getType($pathKey), $pathKey));
+
+ if ($index !== array_key_last($path)) {
+ $arrayItem = new Array_([$arrayItem]);
+ }
+ }
+
+ return $arrayItem;
+ }
+
+ /**
+ * Find the return position within the ast, returns null on encountering an unsupported ast stmt.
+ *
+ * @param array $ast
+ * @return int|null
+ */
+ protected function getAstReturnIndex(array $ast): ?int
+ {
+ foreach ($ast as $index => $item) {
+ switch (get_class($item)) {
+ case Stmt\Use_::class:
+ case Stmt\Expression::class:
+ break;
+ case Stmt\Return_::class:
+ return $index;
+ default:
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Attempt to find the parent object of the targeted path.
+ * If the path cannot be found completely, return the nearest parent and the remainder of the path
+ *
+ * @param array $path
+ * @param $pointer
+ * @param int $depth
+ * @throws SystemException if trying to set a position that is already occupied by a value
+ */
+ protected function seek(array $path, &$pointer, int $depth = 0): array
+ {
+ if (!$pointer) {
+ return [null, $path];
+ }
+
+ $key = array_shift($path);
+
+ if (isset($pointer->value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) {
+ throw new SystemException(sprintf(
+ 'Illegal offset, you are trying to set a position occupied by a value (%s)',
+ get_class($pointer->value)
+ ));
+ }
+
+ foreach (($pointer->items ?? $pointer->value->items) as $index => &$item) {
+ // loose checking to allow for int keys
+ if ($item->key->value == $key) {
+ if (!empty($path)) {
+ return $this->seek($path, $item, ++$depth);
+ }
+
+ return [$item, []];
+ }
+ }
+
+ array_unshift($path, $key);
+
+ return [($depth > 0) ? $pointer : null, $path];
+ }
+
+ /**
+ * Sort the config, supports: ArrayFile::SORT_ASC, ArrayFile::SORT_DESC, callable
+ *
+ * @param string|callable $mode
+ * @throws \InvalidArgumentException if the provided sort type is not a callable or one of static::SORT_ASC or static::SORT_DESC
+ */
+ public function sort($mode = self::SORT_ASC): ArrayFile
+ {
+ if (is_callable($mode)) {
+ usort($this->ast[0]->expr->items, $mode);
+ return $this;
+ }
+
+ switch ($mode) {
+ case static::SORT_ASC:
+ case static::SORT_DESC:
+ $this->sortRecursive($this->ast[0]->expr->items, $mode);
+ break;
+ default:
+ throw new \InvalidArgumentException('Requested sort type is invalid');
+ }
+
+ return $this;
+ }
+
+ /**
+ * Recursive sort an Array_ item array
+ */
+ protected function sortRecursive(array &$array, string $mode): void
+ {
+ foreach ($array as &$item) {
+ if (isset($item->value) && $item->value instanceof Array_) {
+ $this->sortRecursive($item->value->items, $mode);
+ }
+ }
+
+ usort($array, function ($a, $b) use ($mode) {
+ return $mode === static::SORT_ASC
+ ? $a->key->value <=> $b->key->value
+ : $b->key->value <=> $a->key->value;
+ });
+ }
+
+ /**
+ * Write the current config to a file
+ */
+ public function write(string $filePath = null): void
+ {
+ if (!$filePath && $this->filePath) {
+ $filePath = $this->filePath;
+ }
+
+ file_put_contents($filePath, $this->render());
+ }
+
+ /**
+ * Returns a new instance of PHPFunction
+ */
+ public function function(string $name, array $args): PHPFunction
+ {
+ return new PHPFunction($name, $args);
+ }
+
+ /**
+ * Returns a new instance of PHPConstant
+ */
+ public function constant(string $name): PHPConstant
+ {
+ return new PHPConstant($name);
+ }
+
+ /**
+ * Get the printed AST as PHP code
+ */
+ public function render(): string
+ {
+ return $this->printer->render($this->ast, $this->lexer) . "\n";
+ }
+
+ /**
+ * Get currently loaded AST
+ *
+ * @return Stmt[]|null
+ */
+ public function getAst()
+ {
+ return $this->ast;
+ }
+}
diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php
new file mode 100644
index 000000000..04bf38937
--- /dev/null
+++ b/src/Parse/PHP/ArrayPrinter.php
@@ -0,0 +1,316 @@
+lexer = $lexer;
+
+ $p = "prettyPrint($stmts);
+
+ if ($stmts[0] instanceof Stmt\InlineHTML) {
+ $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p);
+ }
+ if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) {
+ $p = preg_replace('/<\?php$/', '', rtrim($p));
+ }
+
+ $this->lexer = null;
+
+ return $p;
+ }
+
+ /**
+ * @param array $nodes
+ * @param bool $trailingComma
+ * @return string
+ */
+ protected function pMaybeMultiline(array $nodes, bool $trailingComma = false)
+ {
+ if ($this->hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof Expr\ArrayItem)) {
+ return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl;
+ } else {
+ return $this->pCommaSeparated($nodes);
+ }
+ }
+
+ /**
+ * Pretty prints a comma-separated list of nodes in multiline style, including comments.
+ *
+ * The result includes a leading newline and one level of indentation (same as pStmts).
+ *
+ * @param Node[] $nodes Array of Nodes to be printed
+ * @param bool $trailingComma Whether to use a trailing comma
+ *
+ * @return string Comma separated pretty printed nodes in multiline style
+ */
+ protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string
+ {
+ $this->indent();
+
+ $result = '';
+ $lastIdx = count($nodes) - 1;
+ foreach ($nodes as $idx => $node) {
+ if ($node !== null) {
+ $comments = $node->getComments();
+
+ if ($comments) {
+ $result .= $this->pComments($comments);
+ }
+
+ $result .= $this->nl . $this->p($node);
+ } else {
+ $result = trim($result) . "\n";
+ }
+ if ($trailingComma || $idx !== $lastIdx) {
+ $result .= ',';
+ }
+ }
+
+ $this->outdent();
+ return $result;
+ }
+
+ /**
+ * Render an array expression
+ *
+ * @param Expr\Array_ $node Array expression node
+ *
+ * @return string Comma separated pretty printed nodes in multiline style
+ */
+ protected function pExpr_Array(Expr\Array_ $node): string
+ {
+ $default = $this->options['shortArraySyntax']
+ ? Expr\Array_::KIND_SHORT
+ : Expr\Array_::KIND_LONG;
+
+ $ops = $node->getAttribute('kind', $default) === Expr\Array_::KIND_SHORT
+ ? ['[', ']']
+ : ['array(', ')'];
+
+ if (!count($node->items) && $comments = $this->getNodeComments($node)) {
+ // the array has no items, we can inject whatever we want
+ return sprintf(
+ '%s%s%s%s%s',
+ // opening control char
+ $ops[0],
+ // indent and add nl string
+ $this->indent(),
+ // join all comments with nl string
+ implode($this->nl, $comments),
+ // outdent and add nl string
+ $this->outdent(),
+ // closing control char
+ $ops[1]
+ );
+ }
+
+ if ($comments = $this->getCommentsNotInArray($node)) {
+ // array has items, we have detected comments not included within the array, therefore we have found
+ // trailing comments and must append them to the end of the array
+ return sprintf(
+ '%s%s%s%s%s%s',
+ // opening control char
+ $ops[0],
+ // render the children
+ $this->pMaybeMultiline($node->items, true),
+ // add 1 level of indentation
+ str_repeat(' ', 4),
+ // join all comments with the current indentation
+ implode($this->nl . str_repeat(' ', 4), $comments),
+ // add a trailing nl
+ $this->nl,
+ // closing control char
+ $ops[1]
+ );
+ }
+
+ // default return
+ return $ops[0] . $this->pMaybeMultiline($node->items, true) . $ops[1];
+ }
+
+ /**
+ * Increase indentation level.
+ * Proxied to allow for nl return
+ *
+ * @return string
+ */
+ protected function indent(): string
+ {
+ $this->indentLevel += 4;
+ $this->nl .= ' ';
+ return $this->nl;
+ }
+
+ /**
+ * Decrease indentation level.
+ * Proxied to allow for nl return
+ *
+ * @return string
+ */
+ protected function outdent(): string
+ {
+ assert($this->indentLevel >= 4);
+ $this->indentLevel -= 4;
+ $this->nl = "\n" . str_repeat(' ', $this->indentLevel);
+ return $this->nl;
+ }
+
+ /**
+ * Get all comments that have not been attributed to a node within a node array
+ *
+ * @param Expr\Array_ $nodes Array of nodes
+ *
+ * @return array Comments found
+ */
+ protected function getCommentsNotInArray(Expr\Array_ $nodes): array
+ {
+ if (!$comments = $this->getNodeComments($nodes)) {
+ return [];
+ }
+
+ return array_filter($comments, function ($comment) use ($nodes) {
+ return !$this->commentInNodeList($nodes->items, $comment);
+ });
+ }
+
+ /**
+ * Recursively check if a comment exists in an array of nodes
+ *
+ * @param Node[] $nodes Array of nodes
+ * @param string $comment The comment to search for
+ *
+ * @return bool
+ */
+ protected function commentInNodeList(array $nodes, string $comment): bool
+ {
+ foreach ($nodes as $node) {
+ if ($node->value instanceof Expr\Array_ && $this->commentInNodeList($node->value->items, $comment)) {
+ return true;
+ }
+ if ($nodeComments = $node->getAttribute('comments')) {
+ foreach ($nodeComments as $nodeComment) {
+ if ($nodeComment->getText() === $comment) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check the lexer tokens for comments within the node's start & end position
+ *
+ * @param Node $node Node to check
+ *
+ * @return ?array
+ */
+ protected function getNodeComments(Node $node): ?array
+ {
+ $tokens = $this->lexer->getTokens();
+ $pos = $node->getAttribute('startTokenPos');
+ $end = $node->getAttribute('endTokenPos');
+ $endLine = $node->getAttribute('endLine');
+ $content = [];
+
+ while (++$pos < $end) {
+ if (!isset($tokens[$pos]) || (!is_array($tokens[$pos]) && $tokens[$pos] !== ',')) {
+ break;
+ }
+
+ if ($tokens[$pos][0] === T_WHITESPACE || $tokens[$pos] === ',') {
+ continue;
+ }
+
+ list($type, $string, $line) = $tokens[$pos];
+
+ if ($line > $endLine) {
+ break;
+ }
+
+ if ($type === T_COMMENT || $type === T_DOC_COMMENT) {
+ $content[] = $string;
+ } elseif ($content) {
+ break;
+ }
+ }
+
+ return empty($content) ? null : $content;
+ }
+
+ /**
+ * Prints reformatted text of the passed comments.
+ *
+ * @param array $comments List of comments
+ *
+ * @return string Reformatted text of comments
+ */
+ protected function pComments(array $comments): string
+ {
+ $formattedComments = [];
+
+ foreach ($comments as $comment) {
+ $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText());
+ }
+
+ $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : '';
+
+ return "\n" . $this->nl . trim($padding . implode($this->nl, $formattedComments)) . "\n";
+ }
+
+ protected function pExpr_Include(Expr\Include_ $node)
+ {
+ static $map = [
+ Expr\Include_::TYPE_INCLUDE => 'include',
+ Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once',
+ Expr\Include_::TYPE_REQUIRE => 'require',
+ Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once',
+ ];
+
+ return $map[$node->type] . '(' . $this->p($node->expr) . ')';
+ }
+}
diff --git a/src/Parse/PHP/PHPConstant.php b/src/Parse/PHP/PHPConstant.php
new file mode 100644
index 000000000..887a9eac5
--- /dev/null
+++ b/src/Parse/PHP/PHPConstant.php
@@ -0,0 +1,25 @@
+name = $name;
+ }
+
+ /**
+ * Get the const name
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
diff --git a/src/Parse/PHP/PHPFunction.php b/src/Parse/PHP/PHPFunction.php
new file mode 100644
index 000000000..1874b1e26
--- /dev/null
+++ b/src/Parse/PHP/PHPFunction.php
@@ -0,0 +1,47 @@
+name = $name;
+ $this->args = $args;
+ }
+
+ /**
+ * Get the function name
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Get the function arguments
+ *
+ * @return array
+ */
+ public function getArgs(): array
+ {
+ return $this->args;
+ }
+}
diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php
new file mode 100644
index 000000000..bb7a85090
--- /dev/null
+++ b/tests/Parse/ArrayFileTest.php
@@ -0,0 +1,845 @@
+assertInstanceOf(ArrayFile::class, $arrayFile);
+
+ $ast = $arrayFile->getAst();
+
+ $this->assertTrue(isset($ast[0]->expr->items[0]->key->value));
+ $this->assertEquals('debug', $ast[0]->expr->items[0]->key->value);
+ }
+
+ public function testWriteFile()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php';
+ $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php';
+
+ $arrayFile = ArrayFile::open($filePath);
+ $arrayFile->write($tmpFile);
+
+ $result = include $tmpFile;
+ $this->assertArrayHasKey('connections', $result);
+ $this->assertArrayHasKey('sqlite', $result['connections']);
+ $this->assertArrayHasKey('driver', $result['connections']['sqlite']);
+ $this->assertEquals('sqlite', $result['connections']['sqlite']['driver']);
+
+ unlink($tmpFile);
+ }
+
+ public function testWriteFileWithUpdates()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php';
+ $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php';
+
+ $arrayFile = ArrayFile::open($filePath);
+ $arrayFile->set('connections.sqlite.driver', 'winter');
+ $arrayFile->write($tmpFile);
+
+ $result = include $tmpFile;
+ $this->assertArrayHasKey('connections', $result);
+ $this->assertArrayHasKey('sqlite', $result['connections']);
+ $this->assertArrayHasKey('driver', $result['connections']['sqlite']);
+ $this->assertEquals('winter', $result['connections']['sqlite']['driver']);
+
+ unlink($tmpFile);
+ }
+
+ public function testWriteFileWithUpdatesArray()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php';
+ $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php';
+
+ $arrayFile = ArrayFile::open($filePath);
+ $arrayFile->set([
+ 'connections.sqlite.driver' => 'winter',
+ 'connections.sqlite.prefix' => 'test',
+ ]);
+ $arrayFile->write($tmpFile);
+
+ $result = include $tmpFile;
+ $this->assertArrayHasKey('connections', $result);
+ $this->assertArrayHasKey('sqlite', $result['connections']);
+ $this->assertArrayHasKey('driver', $result['connections']['sqlite']);
+ $this->assertEquals('winter', $result['connections']['sqlite']['driver']);
+ $this->assertEquals('test', $result['connections']['sqlite']['prefix']);
+
+ unlink($tmpFile);
+ }
+
+ public function testWriteEnvUpdates()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/arrayfile/env-config.php';
+ $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php';
+
+ $arrayFile = ArrayFile::open($filePath);
+ $arrayFile->write($tmpFile);
+
+ $result = include $tmpFile;
+
+ $this->assertArrayHasKey('sample', $result);
+ $this->assertArrayHasKey('value', $result['sample']);
+ $this->assertArrayHasKey('no_default', $result['sample']);
+ $this->assertEquals('default', $result['sample']['value']);
+ $this->assertNull($result['sample']['no_default']);
+
+ $arrayFile->set([
+ 'sample.value' => 'winter',
+ 'sample.no_default' => 'test',
+ ]);
+ $arrayFile->write($tmpFile);
+
+ $result = include $tmpFile;
+
+ $this->assertArrayHasKey('sample', $result);
+ $this->assertArrayHasKey('value', $result['sample']);
+ $this->assertArrayHasKey('no_default', $result['sample']);
+ $this->assertEquals('winter', $result['sample']['value']);
+ $this->assertEquals('test', $result['sample']['no_default']);
+
+ unlink($tmpFile);
+ }
+
+ public function testCasting()
+ {
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertTrue(is_array($result));
+ $this->assertArrayHasKey('url', $result);
+ $this->assertEquals('http://localhost', $result['url']);
+
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('url', false);
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertTrue(is_array($result));
+ $this->assertArrayHasKey('url', $result);
+ $this->assertFalse($result['url']);
+
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('url', 1234);
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertTrue(is_array($result));
+ $this->assertArrayHasKey('url', $result);
+ $this->assertIsInt($result['url']);
+ }
+
+ public function testRender()
+ {
+ /*
+ * Rewrite a single level string
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('url', 'https://wintercms.com');
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertTrue(is_array($result));
+ $this->assertArrayHasKey('url', $result);
+ $this->assertEquals('https://wintercms.com', $result['url']);
+
+ /*
+ * Rewrite a second level string
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('memcached.host', '69.69.69.69');
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('memcached', $result);
+ $this->assertArrayHasKey('host', $result['memcached']);
+ $this->assertEquals('69.69.69.69', $result['memcached']['host']);
+
+ /*
+ * Rewrite a third level string
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('connections.mysql.host', '127.0.0.1');
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('connections', $result);
+ $this->assertArrayHasKey('mysql', $result['connections']);
+ $this->assertArrayHasKey('host', $result['connections']['mysql']);
+ $this->assertEquals('127.0.0.1', $result['connections']['mysql']['host']);
+
+ /*un-
+ * Test alternative quoting
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('timezone', 'The Fifth Dimension')
+ ->set('timezoneAgain', 'The "Sixth" Dimension');
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('timezone', $result);
+ $this->assertArrayHasKey('timezoneAgain', $result);
+ $this->assertEquals('The Fifth Dimension', $result['timezone']);
+ $this->assertEquals('The "Sixth" Dimension', $result['timezoneAgain']);
+
+ /*
+ * Rewrite a boolean
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('debug', false)
+ ->set('debugAgain', true)
+ ->set('bullyIan', true)
+ ->set('booLeeIan', false)
+ ->set('memcached.weight', false)
+ ->set('connections.pgsql.password', true);
+
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('debug', $result);
+ $this->assertArrayHasKey('debugAgain', $result);
+ $this->assertArrayHasKey('bullyIan', $result);
+ $this->assertArrayHasKey('booLeeIan', $result);
+ $this->assertFalse($result['debug']);
+ $this->assertTrue($result['debugAgain']);
+ $this->assertTrue($result['bullyIan']);
+ $this->assertFalse($result['booLeeIan']);
+
+ $this->assertArrayHasKey('memcached', $result);
+ $this->assertArrayHasKey('weight', $result['memcached']);
+ $this->assertFalse($result['memcached']['weight']);
+
+ $this->assertArrayHasKey('connections', $result);
+ $this->assertArrayHasKey('pgsql', $result['connections']);
+ $this->assertArrayHasKey('password', $result['connections']['pgsql']);
+ $this->assertTrue($result['connections']['pgsql']['password']);
+ $this->assertEquals('', $result['connections']['sqlsrv']['password']);
+
+ /*
+ * Rewrite an integer
+ */
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php');
+ $arrayFile->set('aNumber', 69);
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('aNumber', $result);
+ $this->assertEquals(69, $result['aNumber']);
+ }
+
+ public function testConfigInvalid()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('ArrayFiles must start with a return statement');
+
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/invalid.php');
+ }
+
+ public function testConfigImports()
+ {
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/import.php');
+
+ $expected = << Response::HTTP_OK,
+ 'bar' => Response::HTTP_I_AM_A_TEAPOT,
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testConfigImportsUpdating()
+ {
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/import.php');
+ $arrayFile->set('foo', $arrayFile->constant('Response::HTTP_CONFLICT'));
+
+ $expected = << Response::HTTP_CONFLICT,
+ 'bar' => Response::HTTP_I_AM_A_TEAPOT,
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testConfigExpression()
+ {
+ $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/expression.php');
+
+ $expected = << \$bar,
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testReadCreateFile()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+
+ $this->assertFalse(file_exists($file));
+
+ $arrayFile = ArrayFile::open($file);
+
+ $this->assertInstanceOf(ArrayFile::class, $arrayFile);
+
+ $arrayFile->write();
+
+ $this->assertTrue(file_exists($file));
+ $this->assertEquals(sprintf('set('w.i.n.t.e.r', 'cms');
+
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('w', $result);
+ $this->assertArrayHasKey('i', $result['w']);
+ $this->assertArrayHasKey('n', $result['w']['i']);
+ $this->assertArrayHasKey('t', $result['w']['i']['n']);
+ $this->assertArrayHasKey('e', $result['w']['i']['n']['t']);
+ $this->assertArrayHasKey('r', $result['w']['i']['n']['t']['e']);
+ $this->assertEquals('cms', $result['w']['i']['n']['t']['e']['r']);
+ }
+
+ public function testWriteDotNotationMixedCase()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+ $arrayFile->set('w.0.n.1.e.2', 'cms');
+
+ $result = eval('?>' . $arrayFile->render());
+
+ $this->assertArrayHasKey('w', $result);
+ $this->assertArrayHasKey(0, $result['w']);
+ $this->assertArrayHasKey('n', $result['w'][0]);
+ $this->assertArrayHasKey(1, $result['w'][0]['n']);
+ $this->assertArrayHasKey('e', $result['w'][0]['n'][1]);
+ $this->assertArrayHasKey(2, $result['w'][0]['n'][1]['e']);
+ $this->assertEquals('cms', $result['w'][0]['n'][1]['e'][2]);
+ }
+
+ public function testWriteDotNotationMultiple()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+ $arrayFile->set('w.i.n.t.e.r', 'Winter CMS');
+ $arrayFile->set('w.i.n.b', 'is');
+ $arrayFile->set('w.i.n.t.a', 'very');
+ $arrayFile->set('w.i.n.c.l', 'good');
+ $arrayFile->set('w.i.n.c.e', 'and');
+ $arrayFile->set('w.i.n.c.f', 'awesome');
+ $arrayFile->set('w.i.n.g', 'for');
+ $arrayFile->set('w.i.2.g', 'development');
+
+ $arrayFile->write();
+
+ $contents = file_get_contents($file);
+
+ $expected = << [
+ 'i' => [
+ 'n' => [
+ 't' => [
+ 'e' => [
+ 'r' => 'Winter CMS',
+ ],
+ 'a' => 'very',
+ ],
+ 'b' => 'is',
+ 'c' => [
+ 'l' => 'good',
+ 'e' => 'and',
+ 'f' => 'awesome',
+ ],
+ 'g' => 'for',
+ ],
+ 2 => [
+ 'g' => 'development',
+ ],
+ ],
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $contents);
+
+ unlink($file);
+ }
+
+ public function testWriteDotDuplicateIntKeys()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+ $arrayFile->set([
+ 'w.i.n.t.e.r' => 'Winter CMS',
+ 'w.i.2.g' => 'development',
+ ]);
+ $arrayFile->set('w.i.2.g', 'development');
+
+ $arrayFile->write();
+
+ $contents = file_get_contents($file);
+
+ $expected = << [
+ 'i' => [
+ 'n' => [
+ 't' => [
+ 'e' => [
+ 'r' => 'Winter CMS',
+ ],
+ ],
+ ],
+ 2 => [
+ 'g' => 'development',
+ ],
+ ],
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $contents);
+
+ unlink($file);
+ }
+
+ public function testWriteIllegalOffset()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $this->expectException(\Winter\Storm\Exception\SystemException::class);
+
+ $arrayFile->set([
+ 'w.i.n.t.e.r' => 'Winter CMS',
+ 'w.i.n.t.e.r.2' => 'test',
+ ]);
+ }
+
+ public function testThrowExceptionIfMissing()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/missing.php';
+
+ $this->expectException(\InvalidArgumentException::class);
+
+ $arrayFile = ArrayFile::open($file, true);
+ }
+
+ public function testSetArray()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'w' => [
+ 'i' => 'n',
+ 't' => [
+ 'e',
+ 'r'
+ ]
+ ]
+ ]);
+
+ $expected = << [
+ 'i' => 'n',
+ 't' => [
+ 'e',
+ 'r',
+ ],
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testSetNumericArray()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'winter' => [
+ 1 => 'a',
+ 2 => 'b',
+ ],
+ 'cms' => [
+ 0 => 'a',
+ 1 => 'b'
+ ]
+ ]);
+
+ $expected = << [
+ 1 => 'a',
+ 2 => 'b',
+ ],
+ 'cms' => [
+ 'a',
+ 'b',
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testWriteConstCall()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'curl_port' => $arrayFile->constant('CURLOPT_PORT')
+ ]);
+
+ $arrayFile->set([
+ 'curl_return' => new \Winter\Storm\Parse\PHP\PHPConstant('CURLOPT_RETURNTRANSFER')
+ ]);
+
+ $expected = << CURLOPT_PORT,
+ 'curl_return' => CURLOPT_RETURNTRANSFER,
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testWriteArrayFunctionsAndConstCall()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'path.to.config' => [
+ 'test' => $arrayFile->function('env', ['TEST_KEY', 'default']),
+ 'details' => [
+ 'test1',
+ 'test2',
+ 'additional' => [
+ $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC'),
+ $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC')
+ ]
+ ]
+ ]
+ ]);
+
+ $expected = << [
+ 'to' => [
+ 'config' => [
+ 'test' => env('TEST_KEY', 'default'),
+ 'details' => [
+ 'test1',
+ 'test2',
+ 'additional' => [
+ \Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC,
+ \Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC,
+ ],
+ ],
+ ],
+ ],
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testWriteFunctionCall()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'key' => $arrayFile->function('env', ['KEY_A', true])
+ ]);
+
+ $arrayFile->set([
+ 'key2' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false])
+ ]);
+
+ $expected = << env('KEY_A', true),
+ 'key2' => nl2br('KEY_B', false),
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testWriteFunctionCallOverwrite()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'key' => $arrayFile->function('env', ['KEY_A', true])
+ ]);
+
+ $arrayFile->set([
+ 'key' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false])
+ ]);
+
+ $expected = << nl2br('KEY_B', false),
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testInsertNull()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'key' => $arrayFile->function('env', ['KEY_A', null]),
+ 'key2' => null
+ ]);
+
+ $expected = << env('KEY_A', null),
+ 'key2' => null,
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testSortAsc()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'b.b' => 'b',
+ 'b.a' => 'a',
+ 'a.a.b' => 'b',
+ 'a.a.a' => 'a',
+ 'a.c' => 'c',
+ 'a.b' => 'b',
+ ]);
+
+ $arrayFile->sort();
+
+ $expected = << [
+ 'a' => [
+ 'a' => 'a',
+ 'b' => 'b',
+ ],
+ 'b' => 'b',
+ 'c' => 'c',
+ ],
+ 'b' => [
+ 'a' => 'a',
+ 'b' => 'b',
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+
+ public function testSortDesc()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'b.a' => 'a',
+ 'a.a.a' => 'a',
+ 'a.a.b' => 'b',
+ 'a.b' => 'b',
+ 'a.c' => 'c',
+ 'b.b' => 'b',
+ ]);
+
+ $arrayFile->sort(ArrayFile::SORT_DESC);
+
+ $expected = << [
+ 'b' => 'b',
+ 'a' => 'a',
+ ],
+ 'a' => [
+ 'c' => 'c',
+ 'b' => 'b',
+ 'a' => [
+ 'b' => 'b',
+ 'a' => 'a',
+ ],
+ ],
+];
+
+PHP;
+
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testSortUsort()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $arrayFile->set([
+ 'a' => 'a',
+ 'b' => 'b'
+ ]);
+
+ $arrayFile->sort(function ($a, $b) {
+ static $i;
+ if (!isset($i)) {
+ $i = 1;
+ }
+ return $i--;
+ });
+
+ $expected = << 'b',
+ 'a' => 'a',
+];
+
+PHP;
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testIncludeFormatting()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/include.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $expected = << array_merge(include(__DIR__ . '/sample-array-file.php'), [
+ 'bar' => 'foo',
+ ]),
+ 'bar' => 'foo',
+];
+
+PHP;
+ $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render());
+ }
+
+ public function testEmptyNewLines()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php';
+ $arrayFile = ArrayFile::open($file);
+
+ preg_match('/^\s+$/m', $arrayFile->render(), $matches);
+
+ $this->assertEmpty($matches);
+ }
+
+ public function testNestedComments()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/nested-comments.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $code = $arrayFile->render();
+
+ $this->assertStringContainsString(str_repeat(' ', 8) . '|', $code);
+ $this->assertStringNotContainsString(str_repeat(' ', 12) . '|', $code);
+ }
+
+ public function testSingleLineComment()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/single-line-comments.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $this->assertEquals(
+ str_replace("\r", '', file_get_contents($file)),
+ str_replace("\r", '', $arrayFile->render())
+ );
+ }
+
+ public function testSingleLineCommentSubItem()
+ {
+ $file = __DIR__ . '/../fixtures/parse/arrayfile/single-line-comments-subitem.php';
+ $arrayFile = ArrayFile::open($file);
+
+ $this->assertEquals(
+ str_replace("\r", '', file_get_contents($file)),
+ str_replace("\r", '', $arrayFile->render())
+ );
+ }
+}
diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php
new file mode 100644
index 000000000..48412ce36
--- /dev/null
+++ b/tests/Parse/EnvFileTest.php
@@ -0,0 +1,189 @@
+assertInstanceOf(EnvFile::class, $env);
+
+ $arr = $env->getVariables();
+
+ $this->assertArrayHasKey('APP_URL', $arr);
+ $this->assertArrayHasKey('APP_KEY', $arr);
+ $this->assertArrayHasKey('MAIL_HOST', $arr);
+ $this->assertArrayHasKey('MAIL_DRIVER', $arr);
+ $this->assertArrayHasKey('ROUTES_CACHE', $arr);
+ $this->assertArrayNotHasKey('KEY_WITH_NO_VALUE', $arr);
+
+ $this->assertEquals('http://localhost', $arr['APP_URL']);
+ $this->assertEquals('changeme', $arr['APP_KEY']);
+ $this->assertEquals('smtp.mailgun.org', $arr['MAIL_HOST']);
+ $this->assertEquals('smtp', $arr['MAIL_DRIVER']);
+ $this->assertEquals('false', $arr['ROUTES_CACHE']);
+ }
+
+ public function testWriteFile()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/test.env';
+ $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env';
+
+ $env = EnvFile::open($filePath);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+
+ $this->assertStringContainsString('APP_DEBUG=true', $result);
+ $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result);
+ $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result);
+ $this->assertStringContainsString('ROUTES_CACHE=false', $result);
+ $this->assertStringContainsString('ENABLE_CSRF=true', $result);
+ $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result);
+
+ unlink($tmpFile);
+ }
+
+ public function testWriteFileWithUpdates()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/test.env';
+ $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env';
+
+ $env = EnvFile::open($filePath);
+ $env->set('APP_KEY', 'winter');
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+
+ $this->assertStringContainsString('APP_DEBUG=true', $result);
+ $this->assertStringContainsString('APP_KEY="winter"', $result);
+ $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result);
+ $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result);
+ $this->assertStringContainsString('ROUTES_CACHE=false', $result);
+ $this->assertStringContainsString('ENABLE_CSRF=true', $result);
+ $this->assertStringContainsString('# HELLO WORLD', $result);
+ $this->assertStringContainsString('#ENV_TEST="wintercms"', $result);
+ $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result);
+
+ unlink($tmpFile);
+ }
+
+ public function testWriteFileWithUpdatesArray()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/test.env';
+ $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env';
+
+ $env = EnvFile::open($filePath);
+ $env->set([
+ 'APP_KEY' => 'winter',
+ 'ROUTES_CACHE' => 'winter',
+ ]);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+
+ $this->assertStringContainsString('APP_DEBUG=true', $result);
+ $this->assertStringContainsString('APP_KEY="winter"', $result);
+ $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result);
+ $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result);
+ $this->assertStringContainsString('ROUTES_CACHE="winter"', $result);
+ $this->assertStringContainsString('ENABLE_CSRF=true', $result);
+ $this->assertStringContainsString('# HELLO WORLD', $result);
+ $this->assertStringContainsString('#ENV_TEST="wintercms"', $result);
+ $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result);
+
+ unlink($tmpFile);
+ }
+
+ public function testValueFormats()
+ {
+ $envFile = new EnvFile('');
+ $cases = [
+ 'APP_DEBUG=true' => [
+ 'variable' => 'APP_DEBUG',
+ 'value' => true,
+ ],
+ 'APP_URL="https://localhost"' => [
+ 'variable' => 'APP_URL',
+ 'value' => "https://localhost",
+ ],
+ 'DB_CONNECTION="mysql"' => [
+ 'variable' => 'DB_CONNECTION',
+ 'value' => "mysql",
+ ],
+ 'DB_DATABASE="data#base"' => [
+ 'variable' => 'DB_DATABASE',
+ 'value' => "data#base",
+ ],
+ 'DB_USERNAME="teal\\\'c"' => [
+ 'variable' => 'DB_USERNAME',
+ 'value' => "teal\'c",
+ ],
+ 'DB_PASSWORD="test\\"quotes\\\'test"' => [
+ 'variable' => 'DB_PASSWORD',
+ 'value' => "test\"quotes\'test",
+ ],
+ 'DB_PORT=3306' => [
+ 'variable' => 'DB_PORT',
+ 'value' => 3306,
+ ],
+ ];
+
+ foreach ($cases as $output => $config) {
+ $envFile->set($config['variable'], $config['value']);
+ $this->assertStringContainsString($output, $envFile->render());
+ }
+ }
+
+ public function testCasting()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/test.env';
+ $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env';
+
+ $env = EnvFile::open($filePath);
+ $env->set(['APP_KEY' => 'winter']);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+ $this->assertStringContainsString('APP_KEY="winter"', $result);
+
+ $env->set(['APP_KEY' => '123']);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+ $this->assertStringContainsString('APP_KEY=123', $result);
+
+ $env->set(['APP_KEY' => true]);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+ $this->assertStringContainsString('APP_KEY=true', $result);
+
+ $env->set(['APP_KEY' => false]);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+ $this->assertStringContainsString('APP_KEY=false', $result);
+
+ $env->set(['APP_KEY' => null]);
+ $env->write($tmpFile);
+
+ $result = file_get_contents($tmpFile);
+ $this->assertStringContainsString('APP_KEY=null', $result);
+
+ unlink($tmpFile);
+ }
+
+ public function testRender()
+ {
+ $filePath = __DIR__ . '/../fixtures/parse/test.env';
+
+ $env = EnvFile::open($filePath);
+
+ $this->assertEquals(file_get_contents($filePath), $env->render());
+ }
+}
diff --git a/tests/fixtures/parse/arrayfile/env-config.php b/tests/fixtures/parse/arrayfile/env-config.php
new file mode 100644
index 000000000..952e415aa
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/env-config.php
@@ -0,0 +1,8 @@
+ [
+ 'value' => env('TEST_ENV', 'default'),
+ 'no_default' => env('TEST_NO_DEFAULT')
+ ]
+];
diff --git a/tests/fixtures/parse/arrayfile/expression.php b/tests/fixtures/parse/arrayfile/expression.php
new file mode 100644
index 000000000..4a9b08d6a
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/expression.php
@@ -0,0 +1,7 @@
+ $bar
+];
diff --git a/tests/fixtures/parse/arrayfile/import.php b/tests/fixtures/parse/arrayfile/import.php
new file mode 100644
index 000000000..9cc2108f2
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/import.php
@@ -0,0 +1,8 @@
+ Response::HTTP_OK,
+ 'bar' => Response::HTTP_I_AM_A_TEAPOT
+];
diff --git a/tests/fixtures/parse/arrayfile/include.php b/tests/fixtures/parse/arrayfile/include.php
new file mode 100644
index 000000000..267bf9bb2
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/include.php
@@ -0,0 +1,13 @@
+ array_merge(include(__DIR__ . '/sample-array-file.php'), [
+ 'bar' => 'foo'
+ ]),
+ 'bar' => 'foo'
+];
diff --git a/tests/fixtures/parse/arrayfile/invalid.php b/tests/fixtures/parse/arrayfile/invalid.php
new file mode 100644
index 000000000..f734f1b9f
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/invalid.php
@@ -0,0 +1,10 @@
+ winterTest('foo')
+];
diff --git a/tests/fixtures/parse/arrayfile/nested-comments.php b/tests/fixtures/parse/arrayfile/nested-comments.php
new file mode 100644
index 000000000..846e31a59
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/nested-comments.php
@@ -0,0 +1,41 @@
+ [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Enable throttling of Backend authentication attempts
+ |--------------------------------------------------------------------------
+ |
+ | If set to true, users will be given a limited number of attempts to sign
+ | in to the Backend before being blocked for a specified number of minutes.
+ |
+ */
+
+ 'enabled' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Failed Authentication Attempt Limit
+ |--------------------------------------------------------------------------
+ |
+ | Number of failed attempts allowed while trying to authenticate a user.
+ |
+ */
+
+ 'attemptLimit' => 5,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Suspension Time
+ |--------------------------------------------------------------------------
+ |
+ | The number of minutes to suspend further attempts on authentication once
+ | the attempt limit is reached.
+ |
+ */
+
+ 'suspensionTime' => 15,
+ ],
+];
diff --git a/tests/fixtures/parse/arrayfile/sample-array-file.php b/tests/fixtures/parse/arrayfile/sample-array-file.php
new file mode 100644
index 000000000..9cdb0c712
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/sample-array-file.php
@@ -0,0 +1,149 @@
+ true,
+
+ // phpcs:ignore
+ "debugAgain" => FALSE ,
+
+ "bullyIan" => 0,
+
+ 'booLeeIan' => 1,
+
+ 'aNumber' => 55,
+
+ 'default' => 'mysql',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application URL
+ |--------------------------------------------------------------------------
+ |
+ | This URL is used by the console to properly generate URLs when using
+ | the Artisan command line tool. You should set this to the root of
+ | your application so that it is used when running Artisan tasks.
+ |
+ */
+
+ 'url' => 'http://localhost',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Timezone
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify the default timezone for your application, which
+ | will be used by the PHP date and date-time functions. We have gone
+ | ahead and set this to a sensible default for you out of the box.
+ |
+ */
+
+ 'timezone' => "Winter's time",
+
+ "timezoneAgain" => 'Something "else"' ,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Database Connections
+ |--------------------------------------------------------------------------
+ |
+ | Here are each of the database connections setup for your application.
+ | Of course, examples of configuring each database platform that is
+ | supported by Laravel is shown below to make development simple.
+ |
+ |
+ | All database work in Laravel is done through the PHP PDO facilities
+ | so make sure you have the driver for your particular database of
+ | choice installed on your machine before you begin development.
+ |
+ */
+
+ 'connections' => [
+
+ 'sqlite' => [
+ 'driver' => 'sqlite',
+ 'database' => __DIR__.'/../database/production.sqlite',
+ 'prefix' => '',
+ ],
+
+ 'mysql' => [
+ 'driver' => ['rabble' => 'mysql'],
+ 'host' => 'localhost',
+ 'database' => 'database',
+ 'username' => 'root',
+ 'password' => '',
+ 'charset' => 'utf8',
+ 'collation' => 'utf8_unicode_ci',
+ 'prefix' => '',
+ ],
+
+ 'sqlsrv' => [
+ 'driver' => 'sqlsrv',
+ 'host' => 'localhost',
+ 'database' => 'database',
+ 'username' => 'root',
+ 'password' => '',
+ 'prefix' => '',
+ ],
+
+ 'pgsql' => [
+ 'driver' => 'pgsql',
+ 'host' => 'localhost',
+ 'database' => 'database',
+ 'username' => 'root',
+ 'password' => false,
+ 'charset' => 'utf8',
+ 'prefix' => '',
+ 'schema' => 'public',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Memcached Servers
+ |--------------------------------------------------------------------------
+ |
+ | Now you may specify an array of your Memcached servers that should be
+ | used when utilizing the Memcached cache driver. All of the servers
+ | should contain a value for "host", "port", and "weight" options.
+ |
+ */
+
+ 'memcached' => ['host' => '127.0.0.1', 'port' => 11211, 'weight' => true],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Redis Databases
+ |--------------------------------------------------------------------------
+ |
+ | Redis is an open source, fast, and advanced key-value store that also
+ | provides a richer set of commands than a typical key-value systems
+ | such as APC or Memcached. Laravel makes it easy to dig right in.
+ |
+ */
+
+ 'redis' => [
+
+ 'cluster' => false,
+
+ 'default' => [
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'database' => 0,
+ ],
+
+ ],
+];
diff --git a/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php b/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php
new file mode 100644
index 000000000..948f010a1
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php
@@ -0,0 +1,60 @@
+ env('BROADCAST_DRIVER', 'null'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Broadcast Connections
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define all of the broadcast connections that will be used
+ | to broadcast events to other systems or over websockets. Samples of
+ | each available type of connection are provided inside this array.
+ |
+ */
+
+ 'connections' => [
+ 'pusher' => [
+ 'app_id' => env('PUSHER_APP_ID'),
+ 'client_options' => [
+ // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
+ ],
+ 'driver' => 'pusher',
+ 'key' => env('PUSHER_APP_KEY'),
+ 'options' => [
+ 'cluster' => env('PUSHER_APP_CLUSTER'),
+ 'useTLS' => true,
+ ],
+ 'secret' => env('PUSHER_APP_SECRET'),
+ ],
+ 'ably' => [
+ 'driver' => 'ably',
+ 'key' => env('ABLY_KEY'),
+ ],
+ 'redis' => [
+ 'connection' => 'default',
+ 'driver' => 'redis',
+ ],
+ 'log' => [
+ 'driver' => 'log',
+ ],
+ 'null' => [
+ 'driver' => 'null',
+ ],
+ ],
+];
diff --git a/tests/fixtures/parse/arrayfile/single-line-comments.php b/tests/fixtures/parse/arrayfile/single-line-comments.php
new file mode 100644
index 000000000..1076982d4
--- /dev/null
+++ b/tests/fixtures/parse/arrayfile/single-line-comments.php
@@ -0,0 +1,35 @@
+ [
+
+ // above property
+
+ 'bool' => true,
+ 'array' => [
+ // empty array comment
+ ],
+ 'multi_line' => [
+ // empty array comment
+ // with extra
+ ],
+ 'cms' => [
+ 'value',
+ // end of array comment
+ ],
+ 'multi_endings' => [
+ 'value',
+ // first line
+ // last line
+ ],
+ 'multi_comment' => [
+ 'value',
+ /*
+ * Something long
+ */
+ ],
+ 'callable' => array_merge(config('something'), [
+ // configs
+ ]),
+ ],
+];
diff --git a/tests/fixtures/parse/test.env b/tests/fixtures/parse/test.env
new file mode 100644
index 000000000..2af86fdd7
--- /dev/null
+++ b/tests/fixtures/parse/test.env
@@ -0,0 +1,26 @@
+# WINTERCMS
+
+APP_DEBUG=true
+APP_URL="http://localhost"
+APP_KEY="changeme"
+# HELLO WORLD
+
+DB_USE_CONFIG_FOR_TESTING=false
+CACHE_DRIVER="file"
+SESSION_DRIVER="file"
+QUEUE_CONNECTION="sync"
+
+MAIL_DRIVER="smtp"
+MAIL_HOST="smtp.mailgun.org"
+MAIL_PORT=587
+MAIL_ENCRYPTION="tls"
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+
+ROUTES_CACHE=false
+ASSET_CACHE=false
+DATABASE_TEMPLATES=false
+LINK_POLICY="detect"
+ENABLE_CSRF=true
+#ENV_TEST="wintercms"
+KEY_WITH_NO_VALUE