From 4c71911b9c0dad732649e76b9c464397858e4ec7 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:29:41 +0100 Subject: [PATCH 001/103] Required nikic/php-parser --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9c81953a1..87a30ecc3 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "league/csv": "~9.1", "nesbot/carbon": "^2.0", "laravel/framework": "~6.0", - "laravel/tinker": "~2.0" + "laravel/tinker": "~2.0", + "nikic/php-parser": "^4.10" }, "require-dev": { "phpunit/phpunit": "^8.5.12|^9.3.3", From dd20a5621014d923a1bfe01bc51534f7a33d38af Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:30:30 +0100 Subject: [PATCH 002/103] Added custom pretty printer for winter style configs --- src/Config/WinterPrinter.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Config/WinterPrinter.php diff --git a/src/Config/WinterPrinter.php b/src/Config/WinterPrinter.php new file mode 100644 index 000000000..61a4ec7a3 --- /dev/null +++ b/src/Config/WinterPrinter.php @@ -0,0 +1,29 @@ +hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof ArrayItem)) { + return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl; + } else { + return $this->pCommaSeparated($nodes); + } + } + + protected function pComments(array $comments): string + { + $formattedComments = []; + + foreach ($comments as $comment) { + $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); + } + + return $this->nl . implode($this->nl, $formattedComments) . $this->nl; + } +} From 00020fa39c7d81e515395ebd7bfae79f9ada96d9 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:32:21 +0100 Subject: [PATCH 003/103] Added ConfigFile class for modifying configs --- src/Config/ConfigFile.php | 135 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/Config/ConfigFile.php diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php new file mode 100644 index 000000000..fb81b9721 --- /dev/null +++ b/src/Config/ConfigFile.php @@ -0,0 +1,135 @@ +ast = $ast; + $this->file = $file; + $this->printer = $printer ?? new WinterPrinter(); + } + + /** + * @param string $file + * @return ConfigFile|null + */ + public static function read(string $file): ?ConfigFile + { + if (!file_exists($file)) { + throw new \InvalidArgumentException('file not found'); + } + + $content = file_get_contents($file); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + + try { + $ast = $parser->parse($content); + } catch (Error $e) { + // should add better handling + throw new Error($e); + } + + return new static($ast, $file); + } + + /** + * @param string $key + * @param $value + * @return $this + */ + public function set(string $key, $value): ConfigFile + { + $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); + + if (!$target) { + dd($target, $key); + } + + switch (get_class($target->value)) { + case String_::class: + $target->value->value = $value; + break; + case FuncCall::class: + if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { + break; + } + if (isset($target->value->args[0]) && !isset($target->value->args[1])) { + $target->value->args[1] = clone $target->value->args[0]; + } + $target->value->args[1]->value->value = $value; + break; + } + + return $this; + } + + /** + * @param array $path + * @param $pointer + * @return mixed|null + */ + protected function seek(array $path, &$pointer) + { + $key = array_shift($path); + foreach ($pointer as $index => &$item) { + if ($item->key->value === $key) { + if (!empty($path)) { + return $this->seek($path, $item->value->items); + } + + return $item; + } + } + + return null; + } + + /** + * @param string|null $filePath + */ + public function write(string $filePath = null): void + { + if (!$filePath && $this->file) { + $filePath = $this->file; + } + + file_put_contents($filePath, $this->render()); + } + + /** + * @return string + */ + public function render(): string + { + return $this->printer->prettyPrintFile($this->ast) . PHP_EOL; + } +} From 070795133de65b5848ad2a0a1b993c251bec6986 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:34:15 +0100 Subject: [PATCH 004/103] Added class doc block --- src/Config/ConfigFile.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index fb81b9721..40b44b69e 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -7,6 +7,10 @@ use PhpParser\ParserFactory; use PhpParser\PrettyPrinterAbstract; +/** + * Class ConfigFile + * @package Winter\Storm\Config + */ class ConfigFile { /** From 45aadb73aa876d2db0f3d710cc3a317764f81798 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:35:15 +0100 Subject: [PATCH 005/103] Added doc blocks and applyed Winter code styling --- src/Config/WinterPrinter.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Config/WinterPrinter.php b/src/Config/WinterPrinter.php index 61a4ec7a3..62452ff29 100644 --- a/src/Config/WinterPrinter.php +++ b/src/Config/WinterPrinter.php @@ -1,12 +1,19 @@ -hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof ArrayItem)) { @@ -16,6 +23,10 @@ protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) } } + /** + * @param array $comments + * @return string + */ protected function pComments(array $comments): string { $formattedComments = []; From eb76cc4e7319369ea5c2dae82a3fc0fbb83be765 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:40:13 +0100 Subject: [PATCH 006/103] Removed debug code --- src/Config/ConfigFile.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 40b44b69e..12770a37b 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -74,10 +74,6 @@ public function set(string $key, $value): ConfigFile { $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); - if (!$target) { - dd($target, $key); - } - switch (get_class($target->value)) { case String_::class: $target->value->value = $value; From 0d4b2e2c45823363d72ed36cf076510914947fba Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:41:08 +0100 Subject: [PATCH 007/103] Added method to retrieve ast --- src/Config/ConfigFile.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 12770a37b..94b5cbad7 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -132,4 +132,12 @@ public function render(): string { return $this->printer->prettyPrintFile($this->ast) . PHP_EOL; } + + /** + * @return Node\Stmt[]|null + */ + public function getAst() + { + return $this->ast; + } } From 179af7c13a023a836fcb30df219edba84d9dbec0 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 30 Jun 2021 23:59:56 +0100 Subject: [PATCH 008/103] Added ConfigFile tests --- tests/Config/ConfigFileTest.php | 147 ++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/Config/ConfigFileTest.php diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php new file mode 100644 index 000000000..6b804e9d2 --- /dev/null +++ b/tests/Config/ConfigFileTest.php @@ -0,0 +1,147 @@ +assertInstanceOf(ConfigFile::class, $config); + + $ast = $config->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/config/sample-config.php'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + + $config = ConfigFile::read($filePath); + $config->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/config/sample-config.php'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + + $config = ConfigFile::read($filePath); + $config->set('connections.sqlite.driver', 'winter'); + $config->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 testRender() + { + /* + * Rewrite a single level string + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('url', 'https://wintercms.com'); + $result = eval('?>' . $config->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertEquals('https://wintercms.com', $result['url']); + + /* + * Rewrite a second level string + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('memcached.host', '69.69.69.69'); + $result = eval('?>' . $config->render()); + + $this->assertArrayHasKey('memcached', $result); + $this->assertArrayHasKey('host', $result['memcached']); + $this->assertEquals('69.69.69.69', $result['memcached']['host']); + + /* + * Rewrite a third level string + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('connections.mysql.host', '127.0.0.1'); + $result = eval('?>' . $config->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']); + + /* + * Test alternative quoting + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('timezone', 'The Fifth Dimension'); + $config->set('timezoneAgain', 'The "Sixth" Dimension'); + $result = eval('?>' . $config->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 + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('debug', false) + ->set('debugAgain', true) + ->set('bullyIan', true) + ->set('booLeeIan', false) + ->set('memcached.weight', false) + ->set('connections.pgsql.password', true); + + $result = eval('?>' . $config->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 + */ + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('aNumber', 69); + $result = eval('?>' . $config->render()); + + $this->assertArrayHasKey('aNumber', $result); + $this->assertEquals(69, $result['aNumber']); + } +} From d75b3a809852024df3279dd61e39c0c270ba232e Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 1 Jul 2021 00:30:12 +0100 Subject: [PATCH 009/103] Added additional handling for object types --- src/Config/ConfigFile.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 94b5cbad7..b429b57f8 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -3,6 +3,8 @@ use PhpParser\Error; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Scalar\String_; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Stmt; use PhpParser\ParserFactory; use PhpParser\PrettyPrinterAbstract; @@ -87,6 +89,21 @@ public function set(string $key, $value): ConfigFile } $target->value->args[1]->value->value = $value; break; + case ConstFetch::class: + if (isset($target->name->parts[0])) { + $target->name->parts[0] = $value; + } + break; + case ConstFetch::class: + if (isset($target->value->name->parts[0])) { + $target->value->name->parts[0] = $value; + } + break; + case LNumber::class: + if (isset($target->value->value)) { + $target->value->value = $value; + } + break; } return $this; From 524561f9a5d38e06c5cf0b97b338c9eb9f64e178 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 1 Jul 2021 01:26:02 +0100 Subject: [PATCH 010/103] Added fix so single line comments do not recieve nl padding --- src/Config/WinterPrinter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Config/WinterPrinter.php b/src/Config/WinterPrinter.php index 62452ff29..d4480f584 100644 --- a/src/Config/WinterPrinter.php +++ b/src/Config/WinterPrinter.php @@ -35,6 +35,8 @@ protected function pComments(array $comments): string $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); } - return $this->nl . implode($this->nl, $formattedComments) . $this->nl; + $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : ''; + + return $padding . implode($this->nl, $formattedComments) . $padding; } } From 3520e8850bcf2879fb04f78929337dccab852ade Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 11:29:51 +0100 Subject: [PATCH 011/103] Added array input and casting tests --- tests/Config/ConfigFileTest.php | 56 ++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 6b804e9d2..3de115aa1 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -53,6 +53,54 @@ public function testWriteFileWithUpdates() unlink($tmpFile); } + public function testWriteFileWithUpdatesArray() + { + $filePath = __DIR__ . '/../fixtures/config/sample-config.php'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + + $config = ConfigFile::read($filePath); + $config->set([ + 'connections.sqlite.driver' => 'winter', + 'connections.sqlite.prefix' => 'test', + ]); + $config->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 testCasting() + { + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $result = eval('?>' . $config->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertEquals('http://localhost', $result['url']); + + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('url', false); + $result = eval('?>' . $config->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertFalse($result['url']); + + $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); + $config->set('url', 1234); + $result = eval('?>' . $config->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertIsInt($result['url']); + } + public function testRender() { /* @@ -89,12 +137,12 @@ public function testRender() $this->assertArrayHasKey('host', $result['connections']['mysql']); $this->assertEquals('127.0.0.1', $result['connections']['mysql']['host']); - /* + /*un- * Test alternative quoting */ $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('timezone', 'The Fifth Dimension'); - $config->set('timezoneAgain', 'The "Sixth" Dimension'); + $config->set('timezone', 'The Fifth Dimension') + ->set('timezoneAgain', 'The "Sixth" Dimension'); $result = eval('?>' . $config->render()); $this->assertArrayHasKey('timezone', $result); @@ -114,7 +162,7 @@ public function testRender() ->set('connections.pgsql.password', true); $result = eval('?>' . $config->render()); - + $this->assertArrayHasKey('debug', $result); $this->assertArrayHasKey('debugAgain', $result); $this->assertArrayHasKey('bullyIan', $result); From 09eff7b3a23cb6d386a96e3e7d1344b3b6ab99f1 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 11:30:18 +0100 Subject: [PATCH 012/103] Added type casting functionality --- src/Config/ConfigFile.php | 62 ++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index b429b57f8..f72cbefac 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -2,6 +2,7 @@ use PhpParser\Error; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Scalar\LNumber; @@ -72,11 +73,35 @@ public static function read(string $file): ?ConfigFile * @param $value * @return $this */ - public function set(string $key, $value): ConfigFile + public function set(): ConfigFile { + $args = func_get_args(); + + if (count($args) === 1 && is_array($args[0])) { + foreach ($args[0] as $key => $value) { + $this->set($key, $value); + } + + return $this; + } + + if (count($args) !== 2 || !is_string($args[0])) { + throw new \InvalidArgumentException('invalid args passed'); + } + + list($key, $value) = $args; + $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); - switch (get_class($target->value)) { + $valueType = gettype($value); + $class = get_class($target->value); + + if ($valueType !== $this->getScalaFromNode($class)) { + $target->value = $this->makeAstNode($valueType, $value); + return $this; + } + + switch ($class) { case String_::class: $target->value->value = $value; break; @@ -94,11 +119,6 @@ public function set(string $key, $value): ConfigFile $target->name->parts[0] = $value; } break; - case ConstFetch::class: - if (isset($target->value->name->parts[0])) { - $target->value->name->parts[0] = $value; - } - break; case LNumber::class: if (isset($target->value->value)) { $target->value->value = $value; @@ -109,6 +129,34 @@ public function set(string $key, $value): ConfigFile return $this; } + protected function makeAstNode(string $type, $value) + { + switch ($type) { + case 'string': + return new String_($value); + case 'boolean': + return new ConstFetch(new Name($value ? 'true' : 'false')); + break; + case 'integer': + return new LNumber($value); + break; + case 'unknown': + default: + throw new \RuntimeException('not implemented replacement type: ' . $type); + break; + } + } + + public function getScalaFromNode(string $class): string + { + return [ + String_::class => 'string', + FuncCall::class => 'function', + ConstFetch::class => 'const|boolean', + LNumber::class => 'int' + ][$class] ?? 'unknown'; + } + /** * @param array $path * @param $pointer From 7156feccaaf59e259ee27bd0480950efb058c38b Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 11:51:18 +0100 Subject: [PATCH 013/103] Added env default update test --- tests/Config/ConfigFileTest.php | 33 ++++++++++++++++++++++++++++ tests/fixtures/config/env-config.php | 8 +++++++ 2 files changed, 41 insertions(+) create mode 100644 tests/fixtures/config/env-config.php diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 3de115aa1..71a57579a 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -75,6 +75,39 @@ public function testWriteFileWithUpdatesArray() unlink($tmpFile); } + public function testWriteEnvUpdates() + { + $filePath = __DIR__ . '/../fixtures/config/env-config.php'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + + $config = ConfigFile::read($filePath); + $config->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']); + + $config->set([ + 'sample.value' => 'winter', + 'sample.no_default' => 'test', + ]); + $config->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() { $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); diff --git a/tests/fixtures/config/env-config.php b/tests/fixtures/config/env-config.php new file mode 100644 index 000000000..952e415aa --- /dev/null +++ b/tests/fixtures/config/env-config.php @@ -0,0 +1,8 @@ + [ + 'value' => env('TEST_ENV', 'default'), + 'no_default' => env('TEST_NO_DEFAULT') + ] +]; From 4e98e46d89d97652650ad1b8ec774297119e9029 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 11:52:47 +0100 Subject: [PATCH 014/103] Switched to replace nodes rather than update in place for simplicity --- src/Config/ConfigFile.php | 47 +++++++++------------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index f72cbefac..2e2c2ef92 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -1,6 +1,7 @@ value); - if ($valueType !== $this->getScalaFromNode($class)) { - $target->value = $this->makeAstNode($valueType, $value); + if ($class === FuncCall::class) { + 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(new String_('')); + } + $target->value->args[1]->value->value = $value; return $this; } - switch ($class) { - case String_::class: - $target->value->value = $value; - break; - case FuncCall::class: - if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { - break; - } - if (isset($target->value->args[0]) && !isset($target->value->args[1])) { - $target->value->args[1] = clone $target->value->args[0]; - } - $target->value->args[1]->value->value = $value; - break; - case ConstFetch::class: - if (isset($target->name->parts[0])) { - $target->name->parts[0] = $value; - } - break; - case LNumber::class: - if (isset($target->value->value)) { - $target->value->value = $value; - } - break; - } - + $target->value = $this->makeAstNode($valueType, $value); return $this; } @@ -147,16 +130,6 @@ protected function makeAstNode(string $type, $value) } } - public function getScalaFromNode(string $class): string - { - return [ - String_::class => 'string', - FuncCall::class => 'function', - ConstFetch::class => 'const|boolean', - LNumber::class => 'int' - ][$class] ?? 'unknown'; - } - /** * @param array $path * @param $pointer From ec8d643d400b691758c91981bcfe5ab3785031b3 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 12:06:32 +0100 Subject: [PATCH 015/103] Added doc block for makeAstNode method --- src/Config/ConfigFile.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 2e2c2ef92..bc2cc82b8 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -112,6 +112,11 @@ public function set(): ConfigFile return $this; } + /** + * @param string $type + * @param $value + * @return ConstFetch|LNumber|String_ + */ protected function makeAstNode(string $type, $value) { switch ($type) { From d3f97e18dd5bc808dac3c907d34bf813bce7350a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 2 Jul 2021 12:13:10 +0100 Subject: [PATCH 016/103] Cleaned up switch statment --- src/Config/ConfigFile.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index bc2cc82b8..a550b7936 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -124,14 +124,10 @@ protected function makeAstNode(string $type, $value) return new String_($value); case 'boolean': return new ConstFetch(new Name($value ? 'true' : 'false')); - break; case 'integer': return new LNumber($value); - break; - case 'unknown': default: throw new \RuntimeException('not implemented replacement type: ' . $type); - break; } } From 31c405a5d29522b88ba953856268067272fbad5c Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:13:29 +0100 Subject: [PATCH 017/103] Update src/Config/ConfigFile.php Co-authored-by: Ben Thomson --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index a550b7936..4bddc5e05 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -63,7 +63,7 @@ public static function read(string $file): ?ConfigFile $ast = $parser->parse($content); } catch (Error $e) { // should add better handling - throw new Error($e); + throw new ApplicationException($e); } return new static($ast, $file); From 581e616c9448c3a662054a1906b81cd8447b5aa2 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:15:55 +0100 Subject: [PATCH 018/103] Update src/Config/ConfigFile.php Co-authored-by: Ben Thomson --- src/Config/ConfigFile.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 4bddc5e05..3fdc4940c 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -70,24 +70,22 @@ public static function read(string $file): ?ConfigFile } /** - * @param string $key - * @param $value + * @param string|array $key + * @param string|null $value * @return $this */ - public function set(): ConfigFile + public function set($key, string $value = null): ConfigFile { - $args = func_get_args(); - - if (count($args) === 1 && is_array($args[0])) { - foreach ($args[0] as $key => $value) { - $this->set($key, $value); + if (is_array($key)) { + foreach ($key as $name => $value) { + $this->set($name, $value); } return $this; } - if (count($args) !== 2 || !is_string($args[0])) { - throw new \InvalidArgumentException('invalid args passed'); + if ($key && is_null($value)) { + throw new ApplicationException('You must specify a value to set for the given key.'); } list($key, $value) = $args; From 9904d7c5190b39b758f12a25b6d67829a539d386 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:16:11 +0100 Subject: [PATCH 019/103] Update src/Config/ConfigFile.php Co-authored-by: Ben Thomson --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 3fdc4940c..978dcb711 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -112,7 +112,7 @@ public function set($key, string $value = null): ConfigFile /** * @param string $type - * @param $value + * @param mixed $value * @return ConstFetch|LNumber|String_ */ protected function makeAstNode(string $type, $value) From 6859072e2102c9623cdd03129bb4c64372a54ef7 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson <31214002+jaxwilko@users.noreply.github.com> Date: Tue, 6 Jul 2021 17:16:28 +0100 Subject: [PATCH 020/103] Update src/Config/ConfigFile.php Co-authored-by: Ben Thomson --- src/Config/ConfigFile.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 978dcb711..7daeb623f 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -152,6 +152,7 @@ protected function seek(array $path, &$pointer) /** * @param string|null $filePath + * @return void */ public function write(string $filePath = null): void { From c09718739344ec6544dc690fa48d11de85b52a78 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 14 Jul 2021 14:09:04 +0100 Subject: [PATCH 021/103] Added descriptive comments --- src/Config/ConfigFile.php | 40 +++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 7daeb623f..b602ee273 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -18,24 +18,26 @@ class ConfigFile { /** - * @var null + * @var Stmt[]|null Abstract syntax tree produced by `PhpParser` */ protected $ast = null; /** - * @var string|null + * @var string|null Source config file */ protected $file = null; /** - * @var PrettyPrinterAbstract|WinterPrinter|null + * @var PrettyPrinterAbstract|WinterPrinter|null Printer used to define output syntax */ protected $printer = null; /** - * Config constructor. - * @param $ast + * ConfigFile constructor. + * + * @param Stmt[]|null $ast + * @param string $file * @param PrettyPrinterAbstract|null $printer */ - public function __construct($ast, string $file = null, PrettyPrinterAbstract $printer = null) + public function __construct(array $ast, string $file = null, PrettyPrinterAbstract $printer = null) { if (!($ast[0] instanceof Stmt\Return_)) { throw new \InvalidArgumentException('configs must start with a return statement'); @@ -47,6 +49,8 @@ public function __construct($ast, string $file = null, PrettyPrinterAbstract $pr } /** + * Return a new instance of `ConfigFile` ready for modification of the file. + * * @param string $file * @return ConfigFile|null */ @@ -62,7 +66,6 @@ public static function read(string $file): ?ConfigFile try { $ast = $parser->parse($content); } catch (Error $e) { - // should add better handling throw new ApplicationException($e); } @@ -70,6 +73,17 @@ public static function read(string $file): ?ConfigFile } /** + * Set a property within the config using dot syntax. Passing an array as param 1 is also supported. + * + * ```php + * $config->set('property.key.value', 'example'); + * // or + * $config->set([ + * 'property.key1.value' => 'example', + * 'property.key2.value' => 'example' + * ]); + * ``` + * * @param string|array $key * @param string|null $value * @return $this @@ -111,6 +125,8 @@ public function set($key, string $value = null): ConfigFile } /** + * Generate an AST node, using `PhpParser` classes, for a value + * * @param string $type * @param mixed $value * @return ConstFetch|LNumber|String_ @@ -130,6 +146,8 @@ protected function makeAstNode(string $type, $value) } /** + * Get a referenced var from the `$pointer` array + * * @param array $path * @param $pointer * @return mixed|null @@ -151,6 +169,8 @@ protected function seek(array $path, &$pointer) } /** + * Write the current config to a file + * * @param string|null $filePath * @return void */ @@ -164,6 +184,8 @@ public function write(string $filePath = null): void } /** + * Get the printed AST as php code + * * @return string */ public function render(): string @@ -172,7 +194,9 @@ public function render(): string } /** - * @return Node\Stmt[]|null + * Get currently loaded AST + * + * @return Stmt[]|null */ public function getAst() { From 418d6cd75a0b9e8e9a6ff770446f6f8256a06da0 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 14 Jul 2021 14:19:31 +0100 Subject: [PATCH 022/103] Added fixes to set method to ensure types are correctly updated --- src/Config/ConfigFile.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index b602ee273..0370ddaf5 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -85,10 +85,10 @@ public static function read(string $file): ?ConfigFile * ``` * * @param string|array $key - * @param string|null $value + * @param mixed|null $value * @return $this */ - public function set($key, string $value = null): ConfigFile + public function set($key, $value = null): ConfigFile { if (is_array($key)) { foreach ($key as $name => $value) { @@ -102,8 +102,6 @@ public function set($key, string $value = null): ConfigFile throw new ApplicationException('You must specify a value to set for the given key.'); } - list($key, $value) = $args; - $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); $valueType = gettype($value); @@ -121,6 +119,7 @@ public function set($key, string $value = null): ConfigFile } $target->value = $this->makeAstNode($valueType, $value); + return $this; } From b2fac1544321bdf6d56abefc8fe765b43df48a29 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 14 Jul 2021 14:20:44 +0100 Subject: [PATCH 023/103] Updated dot notation comment --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 0370ddaf5..bc41738ca 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -73,7 +73,7 @@ public static function read(string $file): ?ConfigFile } /** - * Set a property within the config using dot syntax. Passing an array as param 1 is also supported. + * Set a property within the config using dot notation. Passing an array as param 1 is also supported. * * ```php * $config->set('property.key.value', 'example'); From 7745ad16b9bebf8d21a2ba28b1cb667ea74356c7 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sun, 25 Jul 2021 16:47:21 +0100 Subject: [PATCH 024/103] Added interface for config file modifiers --- src/Config/ConfigFileInterface.php | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Config/ConfigFileInterface.php diff --git a/src/Config/ConfigFileInterface.php b/src/Config/ConfigFileInterface.php new file mode 100644 index 000000000..9da690540 --- /dev/null +++ b/src/Config/ConfigFileInterface.php @@ -0,0 +1,39 @@ + Date: Sun, 25 Jul 2021 16:49:07 +0100 Subject: [PATCH 025/103] Added interface to ConfigFile class --- src/Config/ConfigFile.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index bc41738ca..0cde384e4 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -1,5 +1,6 @@ Date: Sun, 25 Jul 2021 16:49:33 +0100 Subject: [PATCH 026/103] Added EnvFile class and tests --- src/Config/EnvFile.php | 204 +++++++++++++++++++++++++++++++++ tests/Config/EnvFileTest.php | 142 +++++++++++++++++++++++ tests/fixtures/config/test.env | 21 ++++ 3 files changed, 367 insertions(+) create mode 100644 src/Config/EnvFile.php create mode 100644 tests/Config/EnvFileTest.php create mode 100644 tests/fixtures/config/test.env diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php new file mode 100644 index 000000000..d8b3d1064 --- /dev/null +++ b/src/Config/EnvFile.php @@ -0,0 +1,204 @@ +env = $env; + $this->file = $file; + } + + /** + * Return a new instance of `EnvFile` ready for modification of the file. + * + * @param string|null $file + * @return EnvFile|null + */ + public static function read(?string $file = null): ?EnvFile + { + if (!$file) { + $file = static::getEnvFilePath(); + } + + $loader = new Loader([$file], new DotenvFactory(), false); + + return new static($loader->load(), $file); + } + + /** + * 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' + * ]); + * ``` + * @param array|string $key + * @param mixed|null $value + * @return $this + */ + public function set($key, $value = null): EnvFile + { + if (is_array($key)) { + foreach ($key as $item => $value) { + $this->set($item, $value); + } + return $this; + } + + $this->env[$key] = $value; + + return $this; + } + + /** + * Write the current env to a file + * + * @param string|null $filePath + */ + public function write(string $filePath = null): void + { + if (!$filePath) { + $filePath = $this->file; + } + + File::put($filePath, $this->render()); + } + + /** + * Get the env as a string + * + * @return string + */ + public function render(): string + { + $out = ''; + $key = null; + // count the elements in each block + $count = 0; + + $arrayKeys = array_keys($this->env); + + foreach ($this->env as $item => $value) { + // get the prefix eg. DB_ + $prefix = explode('_', $item)[0] ?? null; + + if ($key && $key !== $prefix) { + // get the position of the prefix in the next position of $this->env + $pos = $this->strpos($arrayKeys[array_search($item, $arrayKeys) + 1] ?? '', $prefix); + if ($pos === 0 || $count > 1) { + $out .= PHP_EOL; + $count = 0; + } + } + + if ($key && $key === $prefix) { + $count++; + } + + $key = $prefix; + + $out .= $item . '=' . $this->wrapValue($value) . PHP_EOL; + } + + return $out; + } + + /** + * Allow for haystack check before execution + * + * @param string $haystack + * @param string $needle + * @param int $offset + * @return false|int + */ + public function strpos(string $haystack, string $needle, int $offset = 0) + { + if (!$haystack) { + return false; + } + + return \strpos($haystack, $needle, $offset); + } + + /** + * Wrap a value in quotes if needed + * + * @param $value + * @return string + */ + protected function wrapValue($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: + return '"' . $value . '"'; + } + } + + /** + * Get the current env array + * + * @return array + */ + public function getEnv(): array + { + return $this->env; + } + + /** + * Get the default env file path + * + * @return string + */ + public static function getEnvFilePath(): string + { + return base_path('.env'); + } +} diff --git a/tests/Config/EnvFileTest.php b/tests/Config/EnvFileTest.php new file mode 100644 index 000000000..ec5384342 --- /dev/null +++ b/tests/Config/EnvFileTest.php @@ -0,0 +1,142 @@ +assertInstanceOf(EnvFile::class, $env); + + $arr = $env->getEnv(); + + $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->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/config/test.env'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + + $env = EnvFile::read($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); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdates() + { + $filePath = __DIR__ . '/../fixtures/config/test.env'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + + $env = EnvFile::read($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); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdatesArray() + { + $filePath = __DIR__ . '/../fixtures/config/test.env'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + + $env = EnvFile::read($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); + + unlink($tmpFile); + } + + public function testCasting() + { + $filePath = __DIR__ . '/../fixtures/config/test.env'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + + $env = EnvFile::read($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/config/test.env'; + $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + + $env = EnvFile::read($filePath); + + $this->assertEquals(file_get_contents($filePath), $env->render()); + } +} diff --git a/tests/fixtures/config/test.env b/tests/fixtures/config/test.env new file mode 100644 index 000000000..c548e3f0a --- /dev/null +++ b/tests/fixtures/config/test.env @@ -0,0 +1,21 @@ +APP_DEBUG=true +APP_URL="http://localhost" +APP_KEY="changeme" + +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 From 7fe60ccde6487800b4fa2231d1cf1398a97e9d7a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sun, 25 Jul 2021 16:53:38 +0100 Subject: [PATCH 027/103] Removed File class usage in favour of file_put_contents --- src/Config/EnvFile.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php index d8b3d1064..8ddb25ec2 100644 --- a/src/Config/EnvFile.php +++ b/src/Config/EnvFile.php @@ -3,7 +3,6 @@ use Winter\Storm\Config\ConfigFileInterface; use Dotenv\Environment\DotenvFactory; use Dotenv\Loader; -use File; /** * Class EnvFile @@ -89,7 +88,7 @@ public function write(string $filePath = null): void $filePath = $this->file; } - File::put($filePath, $this->render()); + file_put_contents($filePath, $this->render()); } /** From bbae0586cc95ec6ad703b03196b69f59d3829d4f Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sun, 25 Jul 2021 17:00:58 +0100 Subject: [PATCH 028/103] Commented out return types --- src/Config/ConfigFileInterface.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Config/ConfigFileInterface.php b/src/Config/ConfigFileInterface.php index 9da690540..0e8b449ab 100644 --- a/src/Config/ConfigFileInterface.php +++ b/src/Config/ConfigFileInterface.php @@ -1,5 +1,9 @@ Date: Fri, 6 Aug 2021 15:13:33 +0100 Subject: [PATCH 029/103] Removed redundent line --- tests/Config/EnvFileTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Config/EnvFileTest.php b/tests/Config/EnvFileTest.php index ec5384342..8ed6f452f 100644 --- a/tests/Config/EnvFileTest.php +++ b/tests/Config/EnvFileTest.php @@ -133,7 +133,6 @@ public function testCasting() public function testRender() { $filePath = __DIR__ . '/../fixtures/config/test.env'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; $env = EnvFile::read($filePath); From d58411d767e89aecef93da8d23aca617ae534a5a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 6 Aug 2021 15:14:00 +0100 Subject: [PATCH 030/103] Added test comments --- tests/fixtures/config/test.env | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/fixtures/config/test.env b/tests/fixtures/config/test.env index c548e3f0a..92353d653 100644 --- a/tests/fixtures/config/test.env +++ b/tests/fixtures/config/test.env @@ -1,6 +1,9 @@ +# WINTERCMS + APP_DEBUG=true APP_URL="http://localhost" APP_KEY="changeme" +# HELLO WORLD DB_USE_CONFIG_FOR_TESTING=false CACHE_DRIVER="file" @@ -19,3 +22,4 @@ ASSET_CACHE=false DATABASE_TEMPLATES=false LINK_POLICY="detect" ENABLE_CSRF=true +#ENV_TEST="wintercms" From a8cf24ead760c11e5543ee63cc6dc8ee766c9045 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 6 Aug 2021 15:16:29 +0100 Subject: [PATCH 031/103] Switched parser for simple interpreter --- src/Config/EnvFile.php | 153 ++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 46 deletions(-) diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php index 8ddb25ec2..1264e8635 100644 --- a/src/Config/EnvFile.php +++ b/src/Config/EnvFile.php @@ -15,6 +15,11 @@ class EnvFile implements ConfigFileInterface */ protected $env = []; + /** + * @var array contains the env lookup map + */ + protected $map = []; + /** * @var string|null contains the filepath used to read / write */ @@ -25,10 +30,11 @@ class EnvFile implements ConfigFileInterface * @param array $env * @param string $file */ - public function __construct(array $env, string $file) + public function __construct(string $file) { - $this->env = $env; $this->file = $file; + + list($this->env, $this->map) = $this->parse($file); } /** @@ -43,9 +49,7 @@ public static function read(?string $file = null): ?EnvFile $file = static::getEnvFilePath(); } - $loader = new Loader([$file], new DotenvFactory(), false); - - return new static($loader->load(), $file); + return new static($file); } /** @@ -72,7 +76,33 @@ public function set($key, $value = null): EnvFile return $this; } - $this->env[$key] = $value; + 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 + * + * @return $this + */ + public function addNewLine(): EnvFile + { + $this->env[] = [ + 'type' => 'nl' + ]; return $this; } @@ -99,54 +129,25 @@ public function write(string $filePath = null): void public function render(): string { $out = ''; - $key = null; - // count the elements in each block - $count = 0; - - $arrayKeys = array_keys($this->env); - - foreach ($this->env as $item => $value) { - // get the prefix eg. DB_ - $prefix = explode('_', $item)[0] ?? null; - - if ($key && $key !== $prefix) { - // get the position of the prefix in the next position of $this->env - $pos = $this->strpos($arrayKeys[array_search($item, $arrayKeys) + 1] ?? '', $prefix); - if ($pos === 0 || $count > 1) { - $out .= PHP_EOL; - $count = 0; - } - } - if ($key && $key === $prefix) { - $count++; + foreach ($this->env as $env) { + + if ($env['type'] === 'nl') { + $out .= PHP_EOL; + continue; } - $key = $prefix; + if ($env['type'] === 'comment') { + $out .= $env['value'] . PHP_EOL; + continue; + } - $out .= $item . '=' . $this->wrapValue($value) . PHP_EOL; + $out .= $env['key'] . '=' . $this->wrapValue($env['value']) . PHP_EOL; } return $out; } - /** - * Allow for haystack check before execution - * - * @param string $haystack - * @param string $needle - * @param int $offset - * @return false|int - */ - public function strpos(string $haystack, string $needle, int $offset = 0) - { - if (!$haystack) { - return false; - } - - return \strpos($haystack, $needle, $offset); - } - /** * Wrap a value in quotes if needed * @@ -181,6 +182,57 @@ protected function wrapValue($value): string } } + /** + * Parse a .env file, returns an array of the env file data and a key => pos map + * + * @param string $file + * @return array + */ + protected function parse(string $file): array + { + if (!file_exists($file) || !($contents = file($file)) || !count($contents)) { + return [[], []]; + } + + $env = []; + $map = []; + $commentCounter = 0; + + foreach ($contents as $line) { + switch (!($line = trim($line)) ? 'nl' : (strpos($line, '#') === 0) ? 'comment' : 'var') { + case 'nl': + $env[] = [ + 'type' => 'nl' + ]; + break; + case 'comment': + $env[] = [ + 'type' => 'comment', + 'key' => 'comment' . $commentCounter++, + 'value' => $line + ]; + break; + case 'var': + $parts = explode('=', $line); + $env[] = [ + 'type' => 'var', + 'key' => $parts[0], + 'value' => trim($parts[1], '"') + ]; + break; + } + } + + foreach ($env as $index => $item) { + if ($item['type'] !== 'var') { + continue; + } + $map[$item['key']] = $index; + } + + return [$env, $map]; + } + /** * Get the current env array * @@ -188,7 +240,16 @@ protected function wrapValue($value): string */ public function getEnv(): array { - return $this->env; + $env = []; + + foreach ($this->env as $item) { + if ($item['type'] !== 'var') { + continue; + } + $env[$item['key']] = $item['value']; + } + + return $env; } /** From b0a9c70810890f10b6c5bced47128905c391d5fc Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 6 Aug 2021 15:21:05 +0100 Subject: [PATCH 032/103] Fixed styling issue and unparenthesized ternary issue --- src/Config/EnvFile.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php index 1264e8635..94f046276 100644 --- a/src/Config/EnvFile.php +++ b/src/Config/EnvFile.php @@ -129,9 +129,7 @@ public function write(string $filePath = null): void public function render(): string { $out = ''; - foreach ($this->env as $env) { - if ($env['type'] === 'nl') { $out .= PHP_EOL; continue; @@ -199,7 +197,7 @@ protected function parse(string $file): array $commentCounter = 0; foreach ($contents as $line) { - switch (!($line = trim($line)) ? 'nl' : (strpos($line, '#') === 0) ? 'comment' : 'var') { + switch (!($line = trim($line)) ? 'nl' : ((strpos($line, '#') === 0) ? 'comment' : 'var')) { case 'nl': $env[] = [ 'type' => 'nl' From 14fd3c8a14cd2f7398b3918f454fd4a1eecaa618 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 27 Oct 2021 16:39:23 +0100 Subject: [PATCH 033/103] Improved EnvFile tests --- tests/Config/EnvFileTest.php | 8 ++++++++ tests/fixtures/config/test.env | 1 + 2 files changed, 9 insertions(+) diff --git a/tests/Config/EnvFileTest.php b/tests/Config/EnvFileTest.php index 8ed6f452f..c040f61eb 100644 --- a/tests/Config/EnvFileTest.php +++ b/tests/Config/EnvFileTest.php @@ -19,6 +19,7 @@ public function testReadFile() $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']); @@ -42,6 +43,7 @@ public function testWriteFile() $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); } @@ -63,6 +65,9 @@ public function testWriteFileWithUpdates() $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); } @@ -87,6 +92,9 @@ public function testWriteFileWithUpdatesArray() $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); } diff --git a/tests/fixtures/config/test.env b/tests/fixtures/config/test.env index 92353d653..2af86fdd7 100644 --- a/tests/fixtures/config/test.env +++ b/tests/fixtures/config/test.env @@ -23,3 +23,4 @@ DATABASE_TEMPLATES=false LINK_POLICY="detect" ENABLE_CSRF=true #ENV_TEST="wintercms" +KEY_WITH_NO_VALUE From cc439ebb8445ebc32469ea549307b710595f082b Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 27 Oct 2021 16:39:45 +0100 Subject: [PATCH 034/103] Improved parsing and rendering --- src/Config/EnvFile.php | 69 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php index 94f046276..ebbc1722a 100644 --- a/src/Config/EnvFile.php +++ b/src/Config/EnvFile.php @@ -130,17 +130,16 @@ public function render(): string { $out = ''; foreach ($this->env as $env) { - if ($env['type'] === 'nl') { - $out .= PHP_EOL; - continue; - } - - if ($env['type'] === 'comment') { - $out .= $env['value'] . PHP_EOL; - continue; + switch ($env['type']) { + case 'comment': + $out .= $env['value']; + break; + case 'var': + $out .= $env['key'] . '=' . $this->escapeValue($env['value']); + break; } - $out .= $env['key'] . '=' . $this->wrapValue($env['value']) . PHP_EOL; + $out .= PHP_EOL; } return $out; @@ -152,7 +151,7 @@ public function render(): string * @param $value * @return string */ - protected function wrapValue($value): string + protected function escapeValue($value): string { if (is_numeric($value)) { return $value; @@ -194,31 +193,37 @@ protected function parse(string $file): array $env = []; $map = []; - $commentCounter = 0; foreach ($contents as $line) { - switch (!($line = trim($line)) ? 'nl' : ((strpos($line, '#') === 0) ? 'comment' : 'var')) { - case 'nl': - $env[] = [ - 'type' => 'nl' - ]; - break; - case 'comment': - $env[] = [ - 'type' => 'comment', - 'key' => 'comment' . $commentCounter++, - 'value' => $line - ]; - break; - case 'var': - $parts = explode('=', $line); - $env[] = [ - 'type' => 'var', - 'key' => $parts[0], - 'value' => trim($parts[1], '"') - ]; - break; + $type = !($line = trim($line)) + ? 'nl' + : ( + str_starts_with($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) { From 71765a80a0417db43653506f29d358ddb9fde503 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 23 Nov 2021 17:13:36 +0000 Subject: [PATCH 035/103] Added support for creating file on read and appending items to empty array --- src/Config/ConfigFile.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 0cde384e4..29650aaef 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -1,5 +1,6 @@ seek(explode('.', $key), $this->ast[0]->expr->items); - $valueType = gettype($value); + + if (!count($this->ast[0]->expr->items)) { + $this->ast[0]->expr->items[] = new ArrayItem( + $this->makeAstNode($valueType, $value), + $this->makeAstNode(gettype($key), $key) + ); + return $this; + } + + $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); $class = get_class($target->value); if ($class === FuncCall::class) { From 7d8090a8ea4b184daed3ae3e738df4b783cdc02d Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 10:40:02 +0000 Subject: [PATCH 036/103] Added constructor to enable short syntax --- src/Config/WinterPrinter.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Config/WinterPrinter.php b/src/Config/WinterPrinter.php index d4480f584..bfa5661b7 100644 --- a/src/Config/WinterPrinter.php +++ b/src/Config/WinterPrinter.php @@ -9,6 +9,15 @@ */ class WinterPrinter extends Standard { + public function __construct(array $options = []) + { + if (!isset($options['shortArraySyntax'])) { + $options['shortArraySyntax'] = true; + } + + parent::__construct($options); + } + /** * @param array $nodes * @param bool $trailingComma From 69cd1520a633627b12a3d59f4d0bb8834ab62eba Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 10:41:15 +0000 Subject: [PATCH 037/103] Added tests for recursive array creation --- tests/Config/ConfigFileTest.php | 158 ++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 71a57579a..de15b1e61 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -225,4 +225,162 @@ public function testRender() $this->assertArrayHasKey('aNumber', $result); $this->assertEquals(69, $result['aNumber']); } + + public function testReadCreateFile() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + + $this->assertFalse(file_exists($file)); + + $config = ConfigFile::read($file, true); + + $this->assertInstanceOf(ConfigFile::class, $config); + + $config->write(); + + $this->assertTrue(file_exists($file)); + $this->assertEquals(sprintf('set('w.i.n.t.e.r', 'cms'); + + $result = eval('?>' . $config->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/config/empty.php'; + $config = ConfigFile::read($file, true); + $config->set('w.0.n.1.e.2', 'cms'); + + $result = eval('?>' . $config->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/config/empty.php'; + $config = ConfigFile::read($file, true); + $config->set('w.i.n.t.e.r', 'Winter CMS'); + $config->set('w.i.n.b', 'is'); + $config->set('w.i.n.t.a', 'very'); + $config->set('w.i.n.c.l', 'good'); + $config->set('w.i.n.c.e', 'and'); + $config->set('w.i.n.c.f', 'awesome'); + $config->set('w.i.n.g', 'for'); + $config->set('w.i.2.g', 'development'); + + $config->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($expected, $contents); + + unlink($file); + } + + public function testWriteDotDuplicateIntKeys() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + $config->set([ + 'w.i.n.t.e.r' => 'Winter CMS', + 'w.i.2.g' => 'development', + ]); + $config->set('w.i.2.g', 'development'); + + $config->write(); + + $contents = file_get_contents($file); + + $expected = << [ + 'i' => [ + 'n' => [ + 't' => [ + 'e' => [ + 'r' => 'Winter CMS', + ], + ], + ], + 2 => [ + 'g' => 'development', + ], + ], + ], +]; + +PHP; + + $this->assertEquals($expected, $contents); + + unlink($file); + } + + public function testWriteIllegalOffset() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $this->expectException(\ApplicationException::class); + + $config->set([ + 'w.i.n.t.e.r' => 'Winter CMS', + 'w.i.n.t.e.r.2' => 'test', + ]); + } } From ce8f252bab79ed2bae5ce4a4d52c2b7c2171b853 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 10:41:51 +0000 Subject: [PATCH 038/103] Added support for recursive array creation using dot notation --- src/Config/ConfigFile.php | 129 ++++++++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 27 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 29650aaef..85d245162 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -1,8 +1,9 @@ create(ParserFactory::PREFER_PHP7); try { - $ast = $parser->parse($content); + $ast = $parser->parse( + $exists + ? file_get_contents($file) + : sprintf('seek(explode('.', $key), $this->ast[0]->expr); + $valueType = gettype($value); - - if (!count($this->ast[0]->expr->items)) { - $this->ast[0]->expr->items[] = new ArrayItem( - $this->makeAstNode($valueType, $value), - $this->makeAstNode(gettype($key), $key) - ); + + // part of a path found + if ($target && $remaining) { + $target->value->items[] = $this->makeArrayItem(implode('.', $remaining), $valueType, $value); return $this; } - $target = $this->seek(explode('.', $key), $this->ast[0]->expr->items); - $class = get_class($target->value); + // path to not found + if (is_null($target)) { + $this->ast[0]->expr->items[] = $this->makeArrayItem($key, $valueType, $value); + return $this; + } - if ($class === FuncCall::class) { + if (!isset($target->value)) { + return $this; + } + + // special handling of function objects + if (get_class($target->value) === FuncCall::class) { if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { return $this; } @@ -134,11 +145,30 @@ public function set($key, $value = null): ConfigFile 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 + * + * @param string $key + * @param string $valueType + * @param $value + * @return ArrayItem + */ + 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(gettype($key), $key) + ); + } + /** * Generate an AST node, using `PhpParser` classes, for a value * @@ -161,26 +191,71 @@ protected function makeAstNode(string $type, $value) } /** - * Get a referenced var from the `$pointer` array + * Returns an ArrayItem generated from a dot notation path + * + * @param string $key + * @param string $valueType + * @param $value + * @return ArrayItem + */ + 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(gettype($pathKey), $pathKey)); + + if ($index !== array_key_last($path)) { + $arrayItem = new Array_([$arrayItem]); + } + } + + return $arrayItem; + } + + /** + * 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 - * @return mixed|null + * @param int $depth + * @return array */ - protected function seek(array $path, &$pointer) + protected function seek(array $path, &$pointer, int $depth = 0): array { + if (!$pointer) { + return [null, $path]; + } + $key = array_shift($path); - foreach ($pointer as $index => &$item) { - if ($item->key->value === $key) { + + if (isset($pointer->value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) { + throw new ApplicationException(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->value->items); + return $this->seek($path, $item, ++$depth); } - return $item; + return [$item, []]; } } - return null; + array_unshift($path, $key); + + return [($depth > 0) ? $pointer : null, $path]; } /** From ba5ed71bef48f2f50141ac4bf88f06cda0a616b6 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 11:48:13 +0000 Subject: [PATCH 039/103] Added ConfigFunction class --- src/Config/ConfigFunction.php | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Config/ConfigFunction.php diff --git a/src/Config/ConfigFunction.php b/src/Config/ConfigFunction.php new file mode 100644 index 000000000..04e49d0a9 --- /dev/null +++ b/src/Config/ConfigFunction.php @@ -0,0 +1,46 @@ +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; + } +} From 7787df1c5a67f144527846eb3ba7809396357c92 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 11:48:47 +0000 Subject: [PATCH 040/103] Added tests for adding new functions to a config --- tests/Config/ConfigFileTest.php | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index de15b1e61..a891969b0 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -383,4 +383,55 @@ public function testWriteIllegalOffset() 'w.i.n.t.e.r.2' => 'test', ]); } + + public function testWriteFunctionCall() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'key' => $config->function('env', ['KEY_A', true]) + ]); + + $config->set([ + 'key2' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) + ]); + + $expected = << env('KEY_A', true), + 'key2' => nl2br('KEY_B', false), +]; + +PHP; + + $this->assertEquals($expected, $config->render()); + } + + public function testWriteFunctionCallOverwrite() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'key' => $config->function('env', ['KEY_A', true]) + ]); + + $config->set([ + 'key' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) + ]); + + $expected = << nl2br('KEY_B', false), +]; + +PHP; + + $this->assertEquals($expected, $config->render()); + } } From 51ef5875c7c1c8562f62b22baf5e084803c4f9b4 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 11:50:00 +0000 Subject: [PATCH 041/103] Added support for setting function calls --- src/Config/ConfigFile.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 85d245162..ab4b522c4 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -134,7 +134,7 @@ public function set($key, $value = null): ConfigFile } // special handling of function objects - if (get_class($target->value) === FuncCall::class) { + if (get_class($target->value) === FuncCall::class && !$value instanceof ConfigFunction) { if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { return $this; } @@ -174,10 +174,14 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI * * @param string $type * @param mixed $value - * @return ConstFetch|LNumber|String_ + * @return ConstFetch|LNumber|String_|FuncCall */ protected function makeAstNode(string $type, $value) { + if ($value instanceof ConfigFunction) { + $type = 'function'; + } + switch ($type) { case 'string': return new String_($value); @@ -185,6 +189,13 @@ protected function makeAstNode(string $type, $value) 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(gettype($arg), $arg)); + }, $value->getArgs()) + ); default: throw new \RuntimeException('not implemented replacement type: ' . $type); } @@ -273,6 +284,11 @@ public function write(string $filePath = null): void file_put_contents($filePath, $this->render()); } + public function function(string $name, array $args): ConfigFunction + { + return new ConfigFunction($name, $args); + } + /** * Get the printed AST as php code * From 4c421b9526e0ec5a7043a855109651737ac71282 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 25 Nov 2021 11:54:51 +0000 Subject: [PATCH 042/103] Added proper class doc --- src/Config/ConfigFunction.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Config/ConfigFunction.php b/src/Config/ConfigFunction.php index 04e49d0a9..32d2b0f4d 100644 --- a/src/Config/ConfigFunction.php +++ b/src/Config/ConfigFunction.php @@ -1,7 +1,10 @@ Date: Thu, 25 Nov 2021 11:59:30 +0000 Subject: [PATCH 043/103] Improved function value type handling --- src/Config/ConfigFile.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index ab4b522c4..7e84c278f 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -115,7 +115,7 @@ public function set($key, $value = null): ConfigFile // try to find a reference to ast object list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[0]->expr); - $valueType = gettype($value); + $valueType = $value instanceof ConfigFunction ? 'function' : gettype($value); // part of a path found if ($target && $remaining) { @@ -134,7 +134,7 @@ public function set($key, $value = null): ConfigFile } // special handling of function objects - if (get_class($target->value) === FuncCall::class && !$value instanceof ConfigFunction) { + if (get_class($target->value) === FuncCall::class && $valueType !== 'function') { if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { return $this; } @@ -178,10 +178,6 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI */ protected function makeAstNode(string $type, $value) { - if ($value instanceof ConfigFunction) { - $type = 'function'; - } - switch ($type) { case 'string': return new String_($value); From 0581fc6b3507194a782bbdc8a4b5f8aae901c93a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 2 Dec 2021 11:02:01 +0000 Subject: [PATCH 044/103] Added test for null insertion --- tests/Config/ConfigFileTest.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index a891969b0..6f6af5cca 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -430,6 +430,29 @@ public function testWriteFunctionCallOverwrite() 'key' => nl2br('KEY_B', false), ]; +PHP; + + $this->assertEquals($expected, $config->render()); + } + + public function testInsertNull() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'key' => $config->function('env', ['KEY_A', null]), + 'key2' => null + ]); + + $expected = << env('KEY_A', null), + 'key2' => null, +]; + PHP; $this->assertEquals($expected, $config->render()); From a010a9cbccc5765eb05081c398e18b7dd5950439 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 2 Dec 2021 11:02:26 +0000 Subject: [PATCH 045/103] Added support for null inserts --- src/Config/ConfigFile.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 7e84c278f..34d800d5a 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -108,10 +108,6 @@ public function set($key, $value = null): ConfigFile return $this; } - if ($key && is_null($value)) { - throw new ApplicationException('You must specify a value to set for the given key.'); - } - // try to find a reference to ast object list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[0]->expr); @@ -178,7 +174,7 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI */ protected function makeAstNode(string $type, $value) { - switch ($type) { + switch (strtolower($type)) { case 'string': return new String_($value); case 'boolean': @@ -192,6 +188,9 @@ protected function makeAstNode(string $type, $value) return new Arg($this->makeAstNode(gettype($arg), $arg)); }, $value->getArgs()) ); + case 'null': + return new ConstFetch(new Name('null')); + break; default: throw new \RuntimeException('not implemented replacement type: ' . $type); } From f9aedbe957da2ecd9a667dc7d4424a6388cd65a2 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 02:52:04 +0000 Subject: [PATCH 046/103] Added double quote escaping --- src/Config/EnvFile.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config/EnvFile.php b/src/Config/EnvFile.php index ebbc1722a..487668afd 100644 --- a/src/Config/EnvFile.php +++ b/src/Config/EnvFile.php @@ -175,7 +175,8 @@ protected function escapeValue($value): string case 'null': return $value; default: - return '"' . $value . '"'; + // addslashes() wont work as it'll escape single quotes and they will be read literally + return '"' . str_replace('"', '\"', $value) . '"'; } } From 57a649a3622995a6c5b1d62136efa03fa94e6254 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 02:58:52 +0000 Subject: [PATCH 047/103] Updated ApplicationExceptions to SystemExceptions --- src/Config/ConfigFile.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 34d800d5a..9a89dbf72 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -13,7 +13,7 @@ use PhpParser\Node\Stmt; use PhpParser\ParserFactory; use PhpParser\PrettyPrinterAbstract; -use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Exception\SystemException; /** * Class ConfigFile @@ -76,7 +76,7 @@ public static function read(string $file, bool $createMissing = false): ?ConfigF : sprintf('value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) { - throw new ApplicationException(sprintf( + throw new SystemException(sprintf( 'Illegal offset, you are trying to set a position occupied by a value (%s)', get_class($pointer->value) )); From cda726fa1f6464cfce74592d9758d75c25f52776 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 03:04:39 +0000 Subject: [PATCH 048/103] Updated test to match the correct exception --- tests/Config/ConfigFileTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 6f6af5cca..9e27590f9 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -376,7 +376,7 @@ public function testWriteIllegalOffset() $file = __DIR__ . '/../fixtures/config/empty.php'; $config = ConfigFile::read($file, true); - $this->expectException(\ApplicationException::class); + $this->expectException(\Winter\Storm\Exception\SystemException::class); $config->set([ 'w.i.n.t.e.r' => 'Winter CMS', From dcef6b18e35331335189a69936faa12683abe98f Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 03:05:06 +0000 Subject: [PATCH 049/103] Added support for setting env() args to correct types --- src/Config/ConfigFile.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 9a89dbf72..8cf86e0ca 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -135,9 +135,9 @@ public function set($key, $value = null): ConfigFile return $this; } if (isset($target->value->args[0]) && !isset($target->value->args[1])) { - $target->value->args[1] = new Arg(new String_('')); + $target->value->args[1] = new Arg($this->makeAstNode($valueType, $value)); } - $target->value->args[1]->value->value = $value; + $target->value->args[1]->value = $this->makeAstNode($valueType, $value); return $this; } From bae73fa07fc20123fdfde6fdbae6358da4a56209 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 03:58:18 +0000 Subject: [PATCH 050/103] Added config sort tests --- tests/Config/ConfigFileTest.php | 107 ++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 9e27590f9..47d27e751 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -457,4 +457,111 @@ public function testInsertNull() $this->assertEquals($expected, $config->render()); } + + public function testSortAsc() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'b.b' => 'b', + 'b.a' => 'a', + 'a.a.b' => 'b', + 'a.a.a' => 'a', + 'a.c' => 'c', + 'a.b' => 'b', + ]); + + $config->sort(); + + $expected = << [ + 'a' => [ + 'a' => 'a', + 'b' => 'b', + ], + 'b' => 'b', + 'c' => 'c', + ], + 'b' => [ + 'a' => 'a', + 'b' => 'b', + ], +]; + +PHP; + + $this->assertEquals($expected, $config->render()); + } + + + public function testSortDesc() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'b.a' => 'a', + 'a.a.a' => 'a', + 'a.a.b' => 'b', + 'a.b' => 'b', + 'a.c' => 'c', + 'b.b' => 'b', + ]); + + $config->sort(ConfigFile::SORT_DESC); + + $expected = << [ + 'b' => 'b', + 'a' => 'a', + ], + 'a' => [ + 'c' => 'c', + 'b' => 'b', + 'a' => [ + 'b' => 'b', + 'a' => 'a', + ], + ], +]; + +PHP; + + $this->assertEquals($expected, $config->render()); + } + + public function testSortUsort() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'a' => 'a', + 'c' => 'c', + 'b' => 'b' + ]); + + $config->sort(function ($a, $b) { + return $a->key->value === 'b' || $b->key->value === 'b' ? 0 : 1; + }); + + $expected = << 'c', + 'a' => 'a', + 'b' => 'b', +]; + +PHP; + $this->assertEquals($expected, $config->render()); + } } From f0c94546ae197f4e686a8f249d2367ecc1563c1f Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 14 Jan 2022 03:59:02 +0000 Subject: [PATCH 051/103] Added sort() method for ConfigFile --- src/Config/ConfigFile.php | 68 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 8cf86e0ca..7a0139067 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -21,6 +21,9 @@ */ class ConfigFile implements ConfigFileInterface { + const SORT_ASC = 'asc'; + const SORT_DESC = 'desc'; + /** * @var Stmt[]|null Abstract syntax tree produced by `PhpParser` */ @@ -264,6 +267,71 @@ protected function seek(array $path, &$pointer, int $depth = 0): array return [($depth > 0) ? $pointer : null, $path]; } + /** + * Sort the config, supports: ConfigFile::SORT_ASC, ConfigFile::SORT_DESC, callable + * + * @param string|callable + * @return ConfigFile + */ + public function sort($type = self::SORT_ASC): ConfigFile + { + if (is_callable($type)) { + usort($this->ast[0]->expr->items, $type); + return $this; + } + + switch ($type) { + case static::SORT_ASC: + $this->sortAsc($this->ast[0]->expr->items); + break; + case static::SORT_DESC: + $this->sortDesc($this->ast[0]->expr->items); + break; + default: + throw new \InvalidArgumentException('sort type not implmented'); + } + + return $this; + } + + /** + * Asc recursive sort an Array_ item array + * + * @param array + * @return void + */ + protected function sortAsc(array &$array): void + { + foreach ($array as &$item) { + if (isset($item->value) && $item->value instanceof Array_) { + $this->sortAsc($item->value->items); + } + } + + usort($array, function ($a, $b) { + return $a->key->value <=> $b->key->value; + }); + } + + /** + * Desc recursive sort an Array_ item array + * + * @param array + * @return void + */ + protected function sortDesc(array &$array): void + { + foreach ($array as &$item) { + if (isset($item->value) && $item->value instanceof Array_) { + $this->sortDesc($item->value->items); + } + } + + usort($array, function ($a, $b) { + return $b->key->value <=> $a->key->value; + }); + } + /** * Write the current config to a file * From 110afdf9c0bf3e2f3ab6104e04bcdc48f105ea1a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 15 Jan 2022 16:06:33 +0000 Subject: [PATCH 052/103] Simplified internal sorting methods --- src/Config/ConfigFile.php | 48 ++++++++++++--------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 7a0139067..8afc5b045 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -270,22 +270,20 @@ protected function seek(array $path, &$pointer, int $depth = 0): array /** * Sort the config, supports: ConfigFile::SORT_ASC, ConfigFile::SORT_DESC, callable * - * @param string|callable + * @param string|callable $mode * @return ConfigFile */ - public function sort($type = self::SORT_ASC): ConfigFile + public function sort($mode = self::SORT_ASC): ConfigFile { - if (is_callable($type)) { - usort($this->ast[0]->expr->items, $type); + if (is_callable($mode)) { + usort($this->ast[0]->expr->items, $mode); return $this; } - switch ($type) { + switch ($mode) { case static::SORT_ASC: - $this->sortAsc($this->ast[0]->expr->items); - break; case static::SORT_DESC: - $this->sortDesc($this->ast[0]->expr->items); + $this->sortRecursive($this->ast[0]->expr->items, $mode); break; default: throw new \InvalidArgumentException('sort type not implmented'); @@ -295,40 +293,24 @@ public function sort($type = self::SORT_ASC): ConfigFile } /** - * Asc recursive sort an Array_ item array - * - * @param array - * @return void - */ - protected function sortAsc(array &$array): void - { - foreach ($array as &$item) { - if (isset($item->value) && $item->value instanceof Array_) { - $this->sortAsc($item->value->items); - } - } - - usort($array, function ($a, $b) { - return $a->key->value <=> $b->key->value; - }); - } - - /** - * Desc recursive sort an Array_ item array + * Recursive sort an Array_ item array * - * @param array + * @param array $array + * @param string $mode * @return void */ - protected function sortDesc(array &$array): void + protected function sortRecursive(array &$array, string $mode): void { foreach ($array as &$item) { if (isset($item->value) && $item->value instanceof Array_) { - $this->sortDesc($item->value->items); + $this->sortRecursive($item->value->items, $mode); } } - usort($array, function ($a, $b) { - return $b->key->value <=> $a->key->value; + usort($array, function ($a, $b) use ($mode) { + return $mode === static::SORT_ASC + ? $a->key->value <=> $b->key->value + : $b->key->value <=> $a->key->value; }); } From bf6c16edcec7b489f56042fa724b17e0b2f8b2e3 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 15 Jan 2022 17:12:46 +0000 Subject: [PATCH 053/103] Fixed test to be consitant across php versions (RFC: stable_sorting) --- tests/Config/ConfigFileTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 47d27e751..7e365723e 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -544,21 +544,23 @@ public function testSortUsort() $config->set([ 'a' => 'a', - 'c' => 'c', 'b' => 'b' ]); $config->sort(function ($a, $b) { - return $a->key->value === 'b' || $b->key->value === 'b' ? 0 : 1; + static $i; + if (!isset($i)) { + $i = 1; + } + return $i--; }); $expected = << 'c', - 'a' => 'a', 'b' => 'b', + 'a' => 'a', ]; PHP; From 4f4a00ae29f46dbcd1c5983ad9abd8eb0dddf8e2 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 15 Jan 2022 17:13:57 +0000 Subject: [PATCH 054/103] Switched line ending to unix style when creating empty file --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 8afc5b045..ed7161254 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -76,7 +76,7 @@ public static function read(string $file, bool $createMissing = false): ?ConfigF $ast = $parser->parse( $exists ? file_get_contents($file) - : sprintf(' Date: Mon, 17 Jan 2022 15:36:38 +0000 Subject: [PATCH 055/103] Switched line ending for rendered output --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index ed7161254..37fdacc47 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -341,7 +341,7 @@ public function function(string $name, array $args): ConfigFunction */ public function render(): string { - return $this->printer->prettyPrintFile($this->ast) . PHP_EOL; + return $this->printer->prettyPrintFile($this->ast) . "\n"; } /** From 2532b674b6e78d7ebcbd9c8351fffb8a4a59d6c4 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Mon, 17 Jan 2022 15:37:14 +0000 Subject: [PATCH 056/103] Switched heredoc test samples for quoted strings --- tests/Config/ConfigFileTest.php | 40 +++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 7e365723e..bd74597d5 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -295,8 +295,7 @@ public function testWriteDotNotationMultiple() $contents = file_get_contents($file); - $expected = <<assertEquals($expected, $contents); @@ -344,8 +343,7 @@ public function testWriteDotDuplicateIntKeys() $contents = file_get_contents($file); - $expected = <<assertEquals($expected, $contents); @@ -397,15 +395,14 @@ public function testWriteFunctionCall() 'key2' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) ]); - $expected = <<assertEquals($expected, $config->render()); } @@ -423,14 +420,13 @@ public function testWriteFunctionCallOverwrite() 'key' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) ]); - $expected = <<assertEquals($expected, $config->render()); } @@ -445,15 +441,14 @@ public function testInsertNull() 'key2' => null ]); - $expected = <<assertEquals($expected, $config->render()); } @@ -474,8 +469,7 @@ public function testSortAsc() $config->sort(); - $expected = <<assertEquals($expected, $config->render()); } @@ -514,8 +508,7 @@ public function testSortDesc() $config->sort(ConfigFile::SORT_DESC); - $expected = <<assertEquals($expected, $config->render()); } @@ -555,15 +548,14 @@ public function testSortUsort() return $i--; }); - $expected = <<assertEquals($expected, $config->render()); } } From 1db7bfc411f9ce6aad6334e53367c45f66f1a1f8 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 20 Jan 2022 00:22:54 +0000 Subject: [PATCH 057/103] Removed incorrect newline at end of expected results --- tests/Config/ConfigFileTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index bd74597d5..00ba2f9cb 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -321,7 +321,6 @@ public function testWriteDotNotationMultiple() ], ], ]; - "; $this->assertEquals($expected, $contents); @@ -361,7 +360,6 @@ public function testWriteDotDuplicateIntKeys() ], ], ]; - "; $this->assertEquals($expected, $contents); @@ -401,7 +399,6 @@ public function testWriteFunctionCall() 'key' => env('KEY_A', true), 'key2' => nl2br('KEY_B', false), ]; - "; $this->assertEquals($expected, $config->render()); @@ -425,7 +422,6 @@ public function testWriteFunctionCallOverwrite() return [ 'key' => nl2br('KEY_B', false), ]; - "; $this->assertEquals($expected, $config->render()); @@ -447,7 +443,6 @@ public function testInsertNull() 'key' => env('KEY_A', null), 'key2' => null, ]; - "; $this->assertEquals($expected, $config->render()); @@ -485,7 +480,6 @@ public function testSortAsc() 'b' => 'b', ], ]; - "; $this->assertEquals($expected, $config->render()); @@ -524,7 +518,6 @@ public function testSortDesc() ], ], ]; - "; $this->assertEquals($expected, $config->render()); @@ -554,7 +547,6 @@ public function testSortUsort() 'b' => 'b', 'a' => 'a', ]; - "; $this->assertEquals($expected, $config->render()); } From 36a4044042652fbcc4f86f6dfe7ab8ab353e0da3 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 20 Jan 2022 00:54:49 +0000 Subject: [PATCH 058/103] Reverted to heredoc and removed carriage returns from expected samples --- tests/Config/ConfigFileTest.php | 64 ++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 00ba2f9cb..91cf03a39 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -295,7 +295,8 @@ public function testWriteDotNotationMultiple() $contents = file_get_contents($file); - $expected = " [ @@ -321,9 +322,10 @@ public function testWriteDotNotationMultiple() ], ], ]; -"; - $this->assertEquals($expected, $contents); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $contents); unlink($file); } @@ -342,7 +344,8 @@ public function testWriteDotDuplicateIntKeys() $contents = file_get_contents($file); - $expected = " [ @@ -360,9 +363,10 @@ public function testWriteDotDuplicateIntKeys() ], ], ]; -"; - $this->assertEquals($expected, $contents); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $contents); unlink($file); } @@ -393,15 +397,17 @@ public function testWriteFunctionCall() 'key2' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) ]); - $expected = " env('KEY_A', true), 'key2' => nl2br('KEY_B', false), ]; -"; - $this->assertEquals($expected, $config->render()); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } public function testWriteFunctionCallOverwrite() @@ -417,14 +423,16 @@ public function testWriteFunctionCallOverwrite() 'key' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) ]); - $expected = " nl2br('KEY_B', false), ]; -"; - $this->assertEquals($expected, $config->render()); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } public function testInsertNull() @@ -437,15 +445,17 @@ public function testInsertNull() 'key2' => null ]); - $expected = " env('KEY_A', null), 'key2' => null, ]; -"; - $this->assertEquals($expected, $config->render()); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } public function testSortAsc() @@ -464,7 +474,8 @@ public function testSortAsc() $config->sort(); - $expected = " [ @@ -480,9 +491,10 @@ public function testSortAsc() 'b' => 'b', ], ]; -"; - $this->assertEquals($expected, $config->render()); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } @@ -502,7 +514,8 @@ public function testSortDesc() $config->sort(ConfigFile::SORT_DESC); - $expected = " [ @@ -518,9 +531,10 @@ public function testSortDesc() ], ], ]; -"; - $this->assertEquals($expected, $config->render()); +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } public function testSortUsort() @@ -541,13 +555,15 @@ public function testSortUsort() return $i--; }); - $expected = " 'b', 'a' => 'a', ]; -"; - $this->assertEquals($expected, $config->render()); + +PHP; + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); } } From 45e305897a404a71689a75a5cf0ea04b2f31fc41 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 20 Jan 2022 00:59:03 +0000 Subject: [PATCH 059/103] Removed usage of PHP_EOL in favour of lf --- tests/Config/ConfigFileTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 91cf03a39..779e5e94a 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -239,7 +239,7 @@ public function testReadCreateFile() $config->write(); $this->assertTrue(file_exists($file)); - $this->assertEquals(sprintf('assertEquals(sprintf(' Date: Thu, 27 Jan 2022 13:31:40 +0000 Subject: [PATCH 060/103] Added class to represent const expressions --- src/Config/ConfigConst.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/Config/ConfigConst.php diff --git a/src/Config/ConfigConst.php b/src/Config/ConfigConst.php new file mode 100644 index 000000000..e9c23db82 --- /dev/null +++ b/src/Config/ConfigConst.php @@ -0,0 +1,33 @@ +name = $name; + } + + /** + * Get the function name + * + * @return string + */ + public function getName(): string + { + return $this->name; + } +} From 87d2d9c0a45d50dc67a404a298e55b599a8f9249 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:32:32 +0000 Subject: [PATCH 061/103] Fixed incorrect comment --- src/Config/ConfigFunction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFunction.php b/src/Config/ConfigFunction.php index 32d2b0f4d..5878368e8 100644 --- a/src/Config/ConfigFunction.php +++ b/src/Config/ConfigFunction.php @@ -1,7 +1,7 @@ Date: Thu, 27 Jan 2022 13:33:09 +0000 Subject: [PATCH 062/103] Added tests for setting arrays and const values --- tests/Config/ConfigFileTest.php | 104 ++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 779e5e94a..5c596f3d6 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -384,6 +384,110 @@ public function testWriteIllegalOffset() ]); } + public function testSetArray() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'w' => [ + 'i' => 'n', + 't' => [ + 'e', + 'r' + ] + ] + ]); + + $expected = << [ + 'i' => 'n', + 't' => [ + 'e', + 'r', + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + } + + public function testWriteConstCall() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'curl_port' => $config->const('CURLOPT_PORT') + ]); + + $config->set([ + 'curl_return' => new \Winter\Storm\Config\ConfigConst('CURLOPT_RETURNTRANSFER') + ]); + + $expected = << CURLOPT_PORT, + 'curl_return' => CURLOPT_RETURNTRANSFER, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + } + + public function testWriteArrayFunctionsAndConstCall() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->set([ + 'path.to.config' => [ + 'test' => $config->function('env', ['TEST_KEY', 'default']), + 'details' => [ + 'test1', + 'test2', + 'additional' => [ + $config->const('\Winter\Storm\Config\ConfigFile::SORT_ASC'), + $config->const('\Winter\Storm\Config\ConfigFile::SORT_DESC') + ] + ] + ] + ]); + + $expected = << [ + 'to' => [ + 'config' => [ + 'test' => env('TEST_KEY', 'default'), + 'details' => [ + 'test1', + 'test2', + 'additional' => [ + \Winter\Storm\Config\ConfigFile::SORT_ASC, + \Winter\Storm\Config\ConfigFile::SORT_DESC, + ], + ], + ], + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + } + public function testWriteFunctionCall() { $file = __DIR__ . '/../fixtures/config/empty.php'; From 0caba9d65304de2fafdc2efc8022f8c99fb1b5e6 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:34:11 +0000 Subject: [PATCH 063/103] Fixed incorrect method description comment --- src/Config/ConfigConst.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigConst.php b/src/Config/ConfigConst.php index e9c23db82..a44f9a58f 100644 --- a/src/Config/ConfigConst.php +++ b/src/Config/ConfigConst.php @@ -22,7 +22,7 @@ public function __construct(string $name) } /** - * Get the function name + * Get the const name * * @return string */ From 1ca70f31d16c2b08735786587955cb56b7d82f94 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:36:04 +0000 Subject: [PATCH 064/103] Added support for setting an array and const value --- src/Config/ConfigFile.php | 88 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 37fdacc47..0549e4076 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -114,7 +114,7 @@ public function set($key, $value = null): ConfigFile // try to find a reference to ast object list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[0]->expr); - $valueType = $value instanceof ConfigFunction ? 'function' : gettype($value); + $valueType = $this->getType($value); // part of a path found if ($target && $remaining) { @@ -164,7 +164,7 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI ? $this->makeAstArrayRecursive($key, $valueType, $value) : new ArrayItem( $this->makeAstNode($valueType, $value), - $this->makeAstNode(gettype($key), $key) + $this->makeAstNode($this->getType($key), $key) ); } @@ -173,7 +173,7 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI * * @param string $type * @param mixed $value - * @return ConstFetch|LNumber|String_|FuncCall + * @return ConstFetch|LNumber|String_|Array_|FuncCall */ protected function makeAstNode(string $type, $value) { @@ -188,17 +188,74 @@ protected function makeAstNode(string $type, $value) return new FuncCall( new Name($value->getName()), array_map(function ($arg) { - return new Arg($this->makeAstNode(gettype($arg), $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')); - break; + case 'array': + return $this->castArray($value); default: throw new \RuntimeException('not implemented replacement type: ' . $type); } } + /** + * Cast an array to AST + * + * @param array $array + * @return Array_ + */ + protected function castArray(array $array): Array_ + { + return ($caster = function ($array, $ast) use (&$caster) { + $useKeys = []; + $keys = array_keys($array); + for ($i = 0; $i < count($keys); $i++) { + $useKeys[$keys[$i]] = false; + if (!is_numeric($keys[$i]) || $keys[$i] !== $i) { + $useKeys[$keys[$i]] = true; + } + } + 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 + * @return string + */ + protected function getType($var): string + { + if ($var instanceof ConfigFunction) { + return 'function'; + } + + if ($var instanceof ConfigConst) { + return 'const'; + } + + return gettype($var); + } + /** * Returns an ArrayItem generated from a dot notation path * @@ -217,7 +274,7 @@ protected function makeAstArrayRecursive(string $key, string $valueType, $value) if (is_numeric($pathKey)) { $pathKey = (int) $pathKey; } - $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode(gettype($pathKey), $pathKey)); + $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode($this->getType($pathKey), $pathKey)); if ($index !== array_key_last($path)) { $arrayItem = new Array_([$arrayItem]); @@ -235,6 +292,7 @@ protected function makeAstArrayRecursive(string $key, string $valueType, $value) * @param $pointer * @param int $depth * @return array + * @throws SystemException */ protected function seek(array $path, &$pointer, int $depth = 0): array { @@ -329,11 +387,29 @@ public function write(string $filePath = null): void file_put_contents($filePath, $this->render()); } + /** + * Returns a new instance of ConfigFunction + * + * @param string $name + * @param array $args + * @return ConfigFunction + */ public function function(string $name, array $args): ConfigFunction { return new ConfigFunction($name, $args); } + /** + * Returns a new instance of ConfigConst + * + * @param string $name + * @return ConfigConst + */ + public function const(string $name): ConfigConst + { + return new ConfigConst($name); + } + /** * Get the printed AST as php code * From 4314dfe27612e3a715fbe2488529a3f0a72a5c1f Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:40:21 +0000 Subject: [PATCH 065/103] Simplified key insertion logic --- src/Config/ConfigFile.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 0549e4076..ab59ab5d9 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -212,11 +212,10 @@ protected function castArray(array $array): Array_ { return ($caster = function ($array, $ast) use (&$caster) { $useKeys = []; - $keys = array_keys($array); - for ($i = 0; $i < count($keys); $i++) { - $useKeys[$keys[$i]] = false; - if (!is_numeric($keys[$i]) || $keys[$i] !== $i) { - $useKeys[$keys[$i]] = true; + foreach (array_keys($array) as $i => $key) { + $useKeys[$key] = false; + if (!is_numeric($key) || $key !== $i) { + $useKeys[$key] = true; } } foreach ($array as $key => $item) { From f60a0ff677ae48cd314dfd44ff2b381f99318daf Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:41:05 +0000 Subject: [PATCH 066/103] Added test for numeric array keys --- tests/Config/ConfigFileTest.php | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 5c596f3d6..57d584931 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -412,6 +412,41 @@ public function testSetArray() ], ]; +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + } + + public function testSetNumericArray() + { + $file = __DIR__ . '/../fixtures/config/empty.php'; + $config = ConfigFile::read($file, true); + + $config->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), $config->render()); From 29067a814ad299a5db836bc94dd72e4593124ddd Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 13:55:04 +0000 Subject: [PATCH 067/103] Fixed spelling mistake --- src/Config/ConfigFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index ab59ab5d9..7d436476b 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -343,7 +343,7 @@ public function sort($mode = self::SORT_ASC): ConfigFile $this->sortRecursive($this->ast[0]->expr->items, $mode); break; default: - throw new \InvalidArgumentException('sort type not implmented'); + throw new \InvalidArgumentException('sort type not implemented'); } return $this; From c2a0aa253dd52d6a3bdbae34c46e1577ad467640 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 27 Jan 2022 14:41:58 +0000 Subject: [PATCH 068/103] Swapped out redundent if statement for assigning result of expression --- src/Config/ConfigFile.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Config/ConfigFile.php b/src/Config/ConfigFile.php index 7d436476b..f929a1b58 100644 --- a/src/Config/ConfigFile.php +++ b/src/Config/ConfigFile.php @@ -213,10 +213,7 @@ protected function castArray(array $array): Array_ return ($caster = function ($array, $ast) use (&$caster) { $useKeys = []; foreach (array_keys($array) as $i => $key) { - $useKeys[$key] = false; - if (!is_numeric($key) || $key !== $i) { - $useKeys[$key] = true; - } + $useKeys[$key] = (!is_numeric($key) || $key !== $i); } foreach ($array as $key => $item) { if (is_array($item)) { From 2d64825c79c29a43c2fdf83f1782a9acc32350ea Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 14:18:22 -0600 Subject: [PATCH 069/103] Move config writer into Parse --- .../Contracts/FileInterface.php} | 0 src/{Config => Parse}/EnvFile.php | 0 src/{Config/ConfigFile.php => Parse/PHP/ArrayFile.php} | 8 ++------ .../WinterPrinter.php => Parse/PHP/ArrayPrinter.php} | 0 src/{Config/ConfigConst.php => Parse/PHP/PHPConstant.php} | 0 .../ConfigFunction.php => Parse/PHP/PHPFunction.php} | 0 6 files changed, 2 insertions(+), 6 deletions(-) rename src/{Config/ConfigFileInterface.php => Parse/Contracts/FileInterface.php} (100%) rename src/{Config => Parse}/EnvFile.php (100%) rename src/{Config/ConfigFile.php => Parse/PHP/ArrayFile.php} (98%) rename src/{Config/WinterPrinter.php => Parse/PHP/ArrayPrinter.php} (100%) rename src/{Config/ConfigConst.php => Parse/PHP/PHPConstant.php} (100%) rename src/{Config/ConfigFunction.php => Parse/PHP/PHPFunction.php} (100%) diff --git a/src/Config/ConfigFileInterface.php b/src/Parse/Contracts/FileInterface.php similarity index 100% rename from src/Config/ConfigFileInterface.php rename to src/Parse/Contracts/FileInterface.php diff --git a/src/Config/EnvFile.php b/src/Parse/EnvFile.php similarity index 100% rename from src/Config/EnvFile.php rename to src/Parse/EnvFile.php diff --git a/src/Config/ConfigFile.php b/src/Parse/PHP/ArrayFile.php similarity index 98% rename from src/Config/ConfigFile.php rename to src/Parse/PHP/ArrayFile.php index f929a1b58..0e1d310a7 100644 --- a/src/Config/ConfigFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -1,6 +1,5 @@ Date: Wed, 16 Feb 2022 14:42:24 -0600 Subject: [PATCH 070/103] Move ArrayFile tests out of config and into parse --- tests/{Config => Parse}/ConfigFileTest.php | 0 tests/{Config => Parse}/EnvFileTest.php | 0 tests/fixtures/{config => parse}/env-config.php | 0 tests/fixtures/{config => parse}/test.env | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/{Config => Parse}/ConfigFileTest.php (100%) rename tests/{Config => Parse}/EnvFileTest.php (100%) rename tests/fixtures/{config => parse}/env-config.php (100%) rename tests/fixtures/{config => parse}/test.env (100%) diff --git a/tests/Config/ConfigFileTest.php b/tests/Parse/ConfigFileTest.php similarity index 100% rename from tests/Config/ConfigFileTest.php rename to tests/Parse/ConfigFileTest.php diff --git a/tests/Config/EnvFileTest.php b/tests/Parse/EnvFileTest.php similarity index 100% rename from tests/Config/EnvFileTest.php rename to tests/Parse/EnvFileTest.php diff --git a/tests/fixtures/config/env-config.php b/tests/fixtures/parse/env-config.php similarity index 100% rename from tests/fixtures/config/env-config.php rename to tests/fixtures/parse/env-config.php diff --git a/tests/fixtures/config/test.env b/tests/fixtures/parse/test.env similarity index 100% rename from tests/fixtures/config/test.env rename to tests/fixtures/parse/test.env From 6f6d0732449704684807f82928789beb8282ca60 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 14:55:25 -0600 Subject: [PATCH 071/103] Various cleanup and code review --- src/Parse/Contracts/FileInterface.php | 24 +- src/Parse/EnvFile.php | 48 +-- src/Parse/PHP/ArrayFile.php | 69 ++--- src/Parse/PHP/ArrayPrinter.php | 12 +- .../PHP/{PHPConstant.php => PHPConst.php} | 14 +- src/Parse/PHP/PHPFunction.php | 9 +- .../{ConfigFileTest.php => ArrayFileTest.php} | 284 +++++++++--------- tests/Parse/EnvFileTest.php | 22 +- tests/fixtures/parse/sample-array-file.php | 148 +++++++++ 9 files changed, 364 insertions(+), 266 deletions(-) rename src/Parse/PHP/{PHPConstant.php => PHPConst.php} (51%) rename tests/Parse/{ConfigFileTest.php => ArrayFileTest.php} (61%) create mode 100644 tests/fixtures/parse/sample-array-file.php diff --git a/src/Parse/Contracts/FileInterface.php b/src/Parse/Contracts/FileInterface.php index 0e8b449ab..286886029 100644 --- a/src/Parse/Contracts/FileInterface.php +++ b/src/Parse/Contracts/FileInterface.php @@ -1,43 +1,31 @@ -file = $file; + $this->filePath = $filePath; - list($this->env, $this->map) = $this->parse($file); + list($this->env, $this->map) = $this->parse($filePath); } /** * Return a new instance of `EnvFile` ready for modification of the file. - * - * @param string|null $file - * @return EnvFile|null */ - public static function read(?string $file = null): ?EnvFile + public static function read(?string $filePath = null): ?EnvFile { - if (!$file) { - $file = static::getEnvFilePath(); + if (!$filePath) { + $filePath = base_path('.env'); } - return new static($file); + return new static($filePath); } /** @@ -115,7 +109,7 @@ public function addNewLine(): EnvFile public function write(string $filePath = null): void { if (!$filePath) { - $filePath = $this->file; + $filePath = $this->filePath; } file_put_contents($filePath, $this->render()); @@ -183,12 +177,12 @@ protected function escapeValue($value): string /** * Parse a .env file, returns an array of the env file data and a key => pos map * - * @param string $file + * @param string $filePath * @return array */ - protected function parse(string $file): array + protected function parse(string $filePath): array { - if (!file_exists($file) || !($contents = file($file)) || !count($contents)) { + if (!file_exists($filePath) || !($contents = file($filePath)) || !count($contents)) { return [[], []]; } @@ -255,14 +249,4 @@ public function getEnv(): array return $env; } - - /** - * Get the default env file path - * - * @return string - */ - public static function getEnvFilePath(): string - { - return base_path('.env'); - } } diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 0e1d310a7..527fbe35b 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -1,4 +1,4 @@ -ast = $ast; - $this->file = $file; - $this->printer = $printer ?? new WinterPrinter(); + $this->filePath = $filePath; + $this->printer = $printer ?? new ArrayPrinter(); } /** - * Return a new instance of `ConfigFile` ready for modification of the file. + * Return a new instance of `ArrayFile` ready for modification of the file. * - * @param string $file + * @param string $filePath * @param bool $createMissing - * @return ConfigFile|null + * @return ArrayFile|null */ - public static function read(string $file, bool $createMissing = false): ?ConfigFile + public static function read(string $filePath, bool $createMissing = false): ?ArrayFile { - $exists = file_exists($file); + $exists = file_exists($filePath); if (!$exists && !$createMissing) { throw new \InvalidArgumentException('file not found'); @@ -71,14 +69,14 @@ public static function read(string $file, bool $createMissing = false): ?ConfigF try { $ast = $parser->parse( $exists - ? file_get_contents($file) + ? file_get_contents($filePath) : sprintf(' $value) { @@ -237,11 +235,11 @@ protected function castArray(array $array): Array_ */ protected function getType($var): string { - if ($var instanceof ConfigFunction) { + if ($var instanceof PHPFunction) { return 'function'; } - if ($var instanceof ConfigConst) { + if ($var instanceof PHPConst) { return 'const'; } @@ -318,12 +316,11 @@ protected function seek(array $path, &$pointer, int $depth = 0): array } /** - * Sort the config, supports: ConfigFile::SORT_ASC, ConfigFile::SORT_DESC, callable + * Sort the config, supports: ArrayFile::SORT_ASC, ArrayFile::SORT_DESC, callable * * @param string|callable $mode - * @return ConfigFile */ - public function sort($mode = self::SORT_ASC): ConfigFile + public function sort($mode = self::SORT_ASC): ArrayFile { if (is_callable($mode)) { usort($this->ast[0]->expr->items, $mode); @@ -372,34 +369,34 @@ protected function sortRecursive(array &$array, string $mode): void */ public function write(string $filePath = null): void { - if (!$filePath && $this->file) { - $filePath = $this->file; + if (!$filePath && $this->filePath) { + $filePath = $this->filePath; } file_put_contents($filePath, $this->render()); } /** - * Returns a new instance of ConfigFunction + * Returns a new instance of PHPFunction * * @param string $name * @param array $args - * @return ConfigFunction + * @return PHPFunction */ - public function function(string $name, array $args): ConfigFunction + public function function(string $name, array $args): PHPFunction { - return new ConfigFunction($name, $args); + return new PHPFunction($name, $args); } /** - * Returns a new instance of ConfigConst + * Returns a new instance of PHPConst * * @param string $name - * @return ConfigConst + * @return PHPConst */ - public function const(string $name): ConfigConst + public function const(string $name): PHPConst { - return new ConfigConst($name); + return new PHPConst($name); } /** diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index bfa5661b7..d9b4b7a33 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -1,13 +1,9 @@ -name = $name; @@ -23,8 +17,6 @@ public function __construct(string $name) /** * Get the const name - * - * @return string */ public function getName(): string { diff --git a/src/Parse/PHP/PHPFunction.php b/src/Parse/PHP/PHPFunction.php index 5878368e8..afd820cf4 100644 --- a/src/Parse/PHP/PHPFunction.php +++ b/src/Parse/PHP/PHPFunction.php @@ -1,12 +1,9 @@ -assertInstanceOf(ConfigFile::class, $config); + $this->assertInstanceOf(ArrayFile::class, $arrayFile); - $ast = $config->getAst(); + $ast = $arrayFile->getAst(); $this->assertTrue(isset($ast[0]->expr->items[0]->key->value)); $this->assertEquals('debug', $ast[0]->expr->items[0]->key->value); @@ -20,11 +20,11 @@ public function testReadFile() public function testWriteFile() { - $filePath = __DIR__ . '/../fixtures/config/sample-config.php'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $config = ConfigFile::read($filePath); - $config->write($tmpFile); + $arrayFile = ArrayFile::read($filePath); + $arrayFile->write($tmpFile); $result = include $tmpFile; $this->assertArrayHasKey('connections', $result); @@ -37,12 +37,12 @@ public function testWriteFile() public function testWriteFileWithUpdates() { - $filePath = __DIR__ . '/../fixtures/config/sample-config.php'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $config = ConfigFile::read($filePath); - $config->set('connections.sqlite.driver', 'winter'); - $config->write($tmpFile); + $arrayFile = ArrayFile::read($filePath); + $arrayFile->set('connections.sqlite.driver', 'winter'); + $arrayFile->write($tmpFile); $result = include $tmpFile; $this->assertArrayHasKey('connections', $result); @@ -55,15 +55,15 @@ public function testWriteFileWithUpdates() public function testWriteFileWithUpdatesArray() { - $filePath = __DIR__ . '/../fixtures/config/sample-config.php'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $config = ConfigFile::read($filePath); - $config->set([ + $arrayFile = ArrayFile::read($filePath); + $arrayFile->set([ 'connections.sqlite.driver' => 'winter', 'connections.sqlite.prefix' => 'test', ]); - $config->write($tmpFile); + $arrayFile->write($tmpFile); $result = include $tmpFile; $this->assertArrayHasKey('connections', $result); @@ -77,11 +77,11 @@ public function testWriteFileWithUpdatesArray() public function testWriteEnvUpdates() { - $filePath = __DIR__ . '/../fixtures/config/env-config.php'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-config.php'; + $filePath = __DIR__ . '/../fixtures/parse/env-config.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $config = ConfigFile::read($filePath); - $config->write($tmpFile); + $arrayFile = ArrayFile::read($filePath); + $arrayFile->write($tmpFile); $result = include $tmpFile; @@ -91,11 +91,11 @@ public function testWriteEnvUpdates() $this->assertEquals('default', $result['sample']['value']); $this->assertNull($result['sample']['no_default']); - $config->set([ + $arrayFile->set([ 'sample.value' => 'winter', 'sample.no_default' => 'test', ]); - $config->write($tmpFile); + $arrayFile->write($tmpFile); $result = include $tmpFile; @@ -110,24 +110,24 @@ public function testWriteEnvUpdates() public function testCasting() { - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $result = eval('?>' . $arrayFile->render()); $this->assertTrue(is_array($result)); $this->assertArrayHasKey('url', $result); $this->assertEquals('http://localhost', $result['url']); - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('url', false); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/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']); - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('url', 1234); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile->set('url', 1234); + $result = eval('?>' . $arrayFile->render()); $this->assertTrue(is_array($result)); $this->assertArrayHasKey('url', $result); @@ -139,9 +139,9 @@ public function testRender() /* * Rewrite a single level string */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('url', 'https://wintercms.com'); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile->set('url', 'https://wintercms.com'); + $result = eval('?>' . $arrayFile->render()); $this->assertTrue(is_array($result)); $this->assertArrayHasKey('url', $result); @@ -150,9 +150,9 @@ public function testRender() /* * Rewrite a second level string */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('memcached.host', '69.69.69.69'); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/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']); @@ -161,9 +161,9 @@ public function testRender() /* * Rewrite a third level string */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('connections.mysql.host', '127.0.0.1'); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/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']); @@ -173,10 +173,10 @@ public function testRender() /*un- * Test alternative quoting */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('timezone', 'The Fifth Dimension') + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile->set('timezone', 'The Fifth Dimension') ->set('timezoneAgain', 'The "Sixth" Dimension'); - $result = eval('?>' . $config->render()); + $result = eval('?>' . $arrayFile->render()); $this->assertArrayHasKey('timezone', $result); $this->assertArrayHasKey('timezoneAgain', $result); @@ -186,15 +186,15 @@ public function testRender() /* * Rewrite a boolean */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('debug', false) + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/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('?>' . $config->render()); + $result = eval('?>' . $arrayFile->render()); $this->assertArrayHasKey('debug', $result); $this->assertArrayHasKey('debugAgain', $result); @@ -218,9 +218,9 @@ public function testRender() /* * Rewrite an integer */ - $config = ConfigFile::read(__DIR__ . '/../fixtures/config/sample-config.php'); - $config->set('aNumber', 69); - $result = eval('?>' . $config->render()); + $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile->set('aNumber', 69); + $result = eval('?>' . $arrayFile->render()); $this->assertArrayHasKey('aNumber', $result); $this->assertEquals(69, $result['aNumber']); @@ -228,15 +228,15 @@ public function testRender() public function testReadCreateFile() { - $file = __DIR__ . '/../fixtures/config/empty.php'; + $file = __DIR__ . '/../fixtures/parse/empty.php'; $this->assertFalse(file_exists($file)); - $config = ConfigFile::read($file, true); + $arrayFile = ArrayFile::read($file, true); - $this->assertInstanceOf(ConfigFile::class, $config); + $this->assertInstanceOf(ArrayFile::class, $arrayFile); - $config->write(); + $arrayFile->write(); $this->assertTrue(file_exists($file)); $this->assertEquals(sprintf('set('w.i.n.t.e.r', 'cms'); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); + $arrayFile->set('w.i.n.t.e.r', 'cms'); - $result = eval('?>' . $config->render()); + $result = eval('?>' . $arrayFile->render()); $this->assertArrayHasKey('w', $result); $this->assertArrayHasKey('i', $result['w']); @@ -263,11 +263,11 @@ public function testWriteDotNotation() public function testWriteDotNotationMixedCase() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); - $config->set('w.0.n.1.e.2', 'cms'); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); + $arrayFile->set('w.0.n.1.e.2', 'cms'); - $result = eval('?>' . $config->render()); + $result = eval('?>' . $arrayFile->render()); $this->assertArrayHasKey('w', $result); $this->assertArrayHasKey(0, $result['w']); @@ -280,18 +280,18 @@ public function testWriteDotNotationMixedCase() public function testWriteDotNotationMultiple() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); - $config->set('w.i.n.t.e.r', 'Winter CMS'); - $config->set('w.i.n.b', 'is'); - $config->set('w.i.n.t.a', 'very'); - $config->set('w.i.n.c.l', 'good'); - $config->set('w.i.n.c.e', 'and'); - $config->set('w.i.n.c.f', 'awesome'); - $config->set('w.i.n.g', 'for'); - $config->set('w.i.2.g', 'development'); - - $config->write(); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); + $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); @@ -332,15 +332,15 @@ public function testWriteDotNotationMultiple() public function testWriteDotDuplicateIntKeys() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); - $config->set([ + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); + $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', 'w.i.2.g' => 'development', ]); - $config->set('w.i.2.g', 'development'); + $arrayFile->set('w.i.2.g', 'development'); - $config->write(); + $arrayFile->write(); $contents = file_get_contents($file); @@ -373,12 +373,12 @@ public function testWriteDotDuplicateIntKeys() public function testWriteIllegalOffset() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); $this->expectException(\Winter\Storm\Exception\SystemException::class); - $config->set([ + $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', 'w.i.n.t.e.r.2' => 'test', ]); @@ -386,10 +386,10 @@ public function testWriteIllegalOffset() public function testSetArray() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'w' => [ 'i' => 'n', 't' => [ @@ -414,15 +414,15 @@ public function testSetArray() PHP; - $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testSetNumericArray() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'winter' => [ 1 => 'a', 2 => 'b', @@ -449,20 +449,20 @@ public function testSetNumericArray() PHP; - $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testWriteConstCall() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ - 'curl_port' => $config->const('CURLOPT_PORT') + $arrayFile->set([ + 'curl_port' => $arrayFile->const('CURLOPT_PORT') ]); - $config->set([ - 'curl_return' => new \Winter\Storm\Config\ConfigConst('CURLOPT_RETURNTRANSFER') + $arrayFile->set([ + 'curl_return' => new \Winter\Storm\Parse\PHP\PHPConst('CURLOPT_RETURNTRANSFER') ]); $expected = <<assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testWriteArrayFunctionsAndConstCall() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'path.to.config' => [ - 'test' => $config->function('env', ['TEST_KEY', 'default']), + 'test' => $arrayFile->function('env', ['TEST_KEY', 'default']), 'details' => [ 'test1', 'test2', 'additional' => [ - $config->const('\Winter\Storm\Config\ConfigFile::SORT_ASC'), - $config->const('\Winter\Storm\Config\ConfigFile::SORT_DESC') + $arrayFile->const('\Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC'), + $arrayFile->const('\Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC') ] ] ] @@ -509,8 +509,8 @@ public function testWriteArrayFunctionsAndConstCall() 'test1', 'test2', 'additional' => [ - \Winter\Storm\Config\ConfigFile::SORT_ASC, - \Winter\Storm\Config\ConfigFile::SORT_DESC, + \Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC, + \Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC, ], ], ], @@ -520,20 +520,20 @@ public function testWriteArrayFunctionsAndConstCall() PHP; - $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testWriteFunctionCall() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ - 'key' => $config->function('env', ['KEY_A', true]) + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', true]) ]); - $config->set([ - 'key2' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) + $arrayFile->set([ + 'key2' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false]) ]); $expected = <<assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testWriteFunctionCallOverwrite() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ - 'key' => $config->function('env', ['KEY_A', true]) + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', true]) ]); - $config->set([ - 'key' => new \Winter\Storm\Config\ConfigFunction('nl2br', ['KEY_B', false]) + $arrayFile->set([ + 'key' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false]) ]); $expected = <<assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testInsertNull() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ - 'key' => $config->function('env', ['KEY_A', null]), + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', null]), 'key2' => null ]); @@ -594,15 +594,15 @@ public function testInsertNull() PHP; - $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testSortAsc() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'b.b' => 'b', 'b.a' => 'a', 'a.a.b' => 'b', @@ -611,7 +611,7 @@ public function testSortAsc() 'a.b' => 'b', ]); - $config->sort(); + $arrayFile->sort(); $expected = <<assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testSortDesc() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'b.a' => 'a', 'a.a.a' => 'a', 'a.a.b' => 'b', @@ -651,7 +651,7 @@ public function testSortDesc() 'b.b' => 'b', ]); - $config->sort(ConfigFile::SORT_DESC); + $arrayFile->sort(ArrayFile::SORT_DESC); $expected = <<assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } public function testSortUsort() { - $file = __DIR__ . '/../fixtures/config/empty.php'; - $config = ConfigFile::read($file, true); + $file = __DIR__ . '/../fixtures/parse/empty.php'; + $arrayFile = ArrayFile::read($file, true); - $config->set([ + $arrayFile->set([ 'a' => 'a', 'b' => 'b' ]); - $config->sort(function ($a, $b) { + $arrayFile->sort(function ($a, $b) { static $i; if (!isset($i)) { $i = 1; @@ -703,6 +703,6 @@ public function testSortUsort() ]; PHP; - $this->assertEquals(str_replace("\r", '', $expected), $config->render()); + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); } } diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php index c040f61eb..9323f8b4a 100644 --- a/tests/Parse/EnvFileTest.php +++ b/tests/Parse/EnvFileTest.php @@ -1,12 +1,12 @@ write($tmpFile); @@ -50,8 +50,8 @@ public function testWriteFile() public function testWriteFileWithUpdates() { - $filePath = __DIR__ . '/../fixtures/config/test.env'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; $env = EnvFile::read($filePath); $env->set('APP_KEY', 'winter'); @@ -74,8 +74,8 @@ public function testWriteFileWithUpdates() public function testWriteFileWithUpdatesArray() { - $filePath = __DIR__ . '/../fixtures/config/test.env'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; $env = EnvFile::read($filePath); $env->set([ @@ -101,8 +101,8 @@ public function testWriteFileWithUpdatesArray() public function testCasting() { - $filePath = __DIR__ . '/../fixtures/config/test.env'; - $tmpFile = __DIR__ . '/../fixtures/config/temp-test.env'; + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; $env = EnvFile::read($filePath); $env->set(['APP_KEY' => 'winter']); @@ -140,7 +140,7 @@ public function testCasting() public function testRender() { - $filePath = __DIR__ . '/../fixtures/config/test.env'; + $filePath = __DIR__ . '/../fixtures/parse/test.env'; $env = EnvFile::read($filePath); diff --git a/tests/fixtures/parse/sample-array-file.php b/tests/fixtures/parse/sample-array-file.php new file mode 100644 index 000000000..582b10b1d --- /dev/null +++ b/tests/fixtures/parse/sample-array-file.php @@ -0,0 +1,148 @@ + true, + + "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, + ], + + ], +]; From 4f43d6c7a3c76d404d8bde4bddbc54676cb4070b Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 14:55:35 -0600 Subject: [PATCH 072/103] Fix test name mismatch --- ...isationTest.php => ExtensionAndEmitterSerializationTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/Support/{ExtensionAndEmitterSerialisationTest.php => ExtensionAndEmitterSerializationTest.php} (93%) diff --git a/tests/Support/ExtensionAndEmitterSerialisationTest.php b/tests/Support/ExtensionAndEmitterSerializationTest.php similarity index 93% rename from tests/Support/ExtensionAndEmitterSerialisationTest.php rename to tests/Support/ExtensionAndEmitterSerializationTest.php index 62cdd1577..abe331763 100644 --- a/tests/Support/ExtensionAndEmitterSerialisationTest.php +++ b/tests/Support/ExtensionAndEmitterSerializationTest.php @@ -2,7 +2,7 @@ use Winter\Storm\Extension\Extendable; -class ExtensionAndEmitterSynchronisationTest extends TestCase +class ExtensionAndEmitterSerializationTest extends TestCase { /** * Test whether nested closures in two different traits get serialized properly. From f4da8f6aa7380d3daafe0002905affd9baaac507 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 14:56:59 -0600 Subject: [PATCH 073/103] PHPConst -> PHPConstant --- src/Parse/PHP/ArrayFile.php | 10 +++++----- src/Parse/PHP/{PHPConst.php => PHPConstant.php} | 2 +- tests/Parse/ArrayFileTest.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/Parse/PHP/{PHPConst.php => PHPConstant.php} (95%) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 527fbe35b..e37a483bd 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -239,7 +239,7 @@ protected function getType($var): string return 'function'; } - if ($var instanceof PHPConst) { + if ($var instanceof PHPConstant) { return 'const'; } @@ -389,14 +389,14 @@ public function function(string $name, array $args): PHPFunction } /** - * Returns a new instance of PHPConst + * Returns a new instance of PHPConstant * * @param string $name - * @return PHPConst + * @return PHPConstant */ - public function const(string $name): PHPConst + public function const(string $name): PHPConstant { - return new PHPConst($name); + return new PHPConstant($name); } /** diff --git a/src/Parse/PHP/PHPConst.php b/src/Parse/PHP/PHPConstant.php similarity index 95% rename from src/Parse/PHP/PHPConst.php rename to src/Parse/PHP/PHPConstant.php index fbf0dbd01..887a9eac5 100644 --- a/src/Parse/PHP/PHPConst.php +++ b/src/Parse/PHP/PHPConstant.php @@ -3,7 +3,7 @@ /** * Used with ArrayFile to inject a constant into a PHP array file */ -class PHPConst +class PHPConstant { /** * @var string function name diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index a693f18a7..dd03bb7de 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -462,7 +462,7 @@ public function testWriteConstCall() ]); $arrayFile->set([ - 'curl_return' => new \Winter\Storm\Parse\PHP\PHPConst('CURLOPT_RETURNTRANSFER') + 'curl_return' => new \Winter\Storm\Parse\PHP\PHPConstant('CURLOPT_RETURNTRANSFER') ]); $expected = << Date: Wed, 16 Feb 2022 15:05:27 -0600 Subject: [PATCH 074/103] FileInterface -> DataFileInterface, read() -> open() --- src/Parse/Contracts/DataFileInterface.php | 29 +++++++++++ src/Parse/Contracts/FileInterface.php | 31 ----------- src/Parse/EnvFile.php | 6 +-- src/Parse/PHP/ArrayFile.php | 6 +-- src/Parse/PHP/PHPFunction.php | 5 +- tests/Parse/ArrayFileTest.php | 60 +++++++++++----------- tests/Parse/EnvFileTest.php | 12 ++--- tests/fixtures/parse/sample-array-file.php | 1 + 8 files changed, 75 insertions(+), 75 deletions(-) create mode 100644 src/Parse/Contracts/DataFileInterface.php delete mode 100644 src/Parse/Contracts/FileInterface.php diff --git a/src/Parse/Contracts/DataFileInterface.php b/src/Parse/Contracts/DataFileInterface.php new file mode 100644 index 000000000..40e9dcb3e --- /dev/null +++ b/src/Parse/Contracts/DataFileInterface.php @@ -0,0 +1,29 @@ +assertInstanceOf(ArrayFile::class, $arrayFile); @@ -23,7 +23,7 @@ public function testWriteFile() $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $arrayFile = ArrayFile::read($filePath); + $arrayFile = ArrayFile::open($filePath); $arrayFile->write($tmpFile); $result = include $tmpFile; @@ -40,7 +40,7 @@ public function testWriteFileWithUpdates() $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $arrayFile = ArrayFile::read($filePath); + $arrayFile = ArrayFile::open($filePath); $arrayFile->set('connections.sqlite.driver', 'winter'); $arrayFile->write($tmpFile); @@ -58,7 +58,7 @@ public function testWriteFileWithUpdatesArray() $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $arrayFile = ArrayFile::read($filePath); + $arrayFile = ArrayFile::open($filePath); $arrayFile->set([ 'connections.sqlite.driver' => 'winter', 'connections.sqlite.prefix' => 'test', @@ -80,7 +80,7 @@ public function testWriteEnvUpdates() $filePath = __DIR__ . '/../fixtures/parse/env-config.php'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; - $arrayFile = ArrayFile::read($filePath); + $arrayFile = ArrayFile::open($filePath); $arrayFile->write($tmpFile); $result = include $tmpFile; @@ -110,14 +110,14 @@ public function testWriteEnvUpdates() public function testCasting() { - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/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::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('url', false); $result = eval('?>' . $arrayFile->render()); @@ -125,7 +125,7 @@ public function testCasting() $this->assertArrayHasKey('url', $result); $this->assertFalse($result['url']); - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('url', 1234); $result = eval('?>' . $arrayFile->render()); @@ -139,7 +139,7 @@ public function testRender() /* * Rewrite a single level string */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('url', 'https://wintercms.com'); $result = eval('?>' . $arrayFile->render()); @@ -150,7 +150,7 @@ public function testRender() /* * Rewrite a second level string */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('memcached.host', '69.69.69.69'); $result = eval('?>' . $arrayFile->render()); @@ -161,7 +161,7 @@ public function testRender() /* * Rewrite a third level string */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('connections.mysql.host', '127.0.0.1'); $result = eval('?>' . $arrayFile->render()); @@ -173,7 +173,7 @@ public function testRender() /*un- * Test alternative quoting */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('timezone', 'The Fifth Dimension') ->set('timezoneAgain', 'The "Sixth" Dimension'); $result = eval('?>' . $arrayFile->render()); @@ -186,7 +186,7 @@ public function testRender() /* * Rewrite a boolean */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('debug', false) ->set('debugAgain', true) ->set('bullyIan', true) @@ -218,7 +218,7 @@ public function testRender() /* * Rewrite an integer */ - $arrayFile = ArrayFile::read(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); $arrayFile->set('aNumber', 69); $result = eval('?>' . $arrayFile->render()); @@ -232,7 +232,7 @@ public function testReadCreateFile() $this->assertFalse(file_exists($file)); - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $this->assertInstanceOf(ArrayFile::class, $arrayFile); @@ -247,7 +247,7 @@ public function testReadCreateFile() public function testWriteDotNotation() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set('w.i.n.t.e.r', 'cms'); $result = eval('?>' . $arrayFile->render()); @@ -264,7 +264,7 @@ public function testWriteDotNotation() public function testWriteDotNotationMixedCase() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set('w.0.n.1.e.2', 'cms'); $result = eval('?>' . $arrayFile->render()); @@ -281,7 +281,7 @@ public function testWriteDotNotationMixedCase() public function testWriteDotNotationMultiple() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $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'); @@ -333,7 +333,7 @@ public function testWriteDotNotationMultiple() public function testWriteDotDuplicateIntKeys() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', 'w.i.2.g' => 'development', @@ -374,7 +374,7 @@ public function testWriteDotDuplicateIntKeys() public function testWriteIllegalOffset() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $this->expectException(\Winter\Storm\Exception\SystemException::class); @@ -387,7 +387,7 @@ public function testWriteIllegalOffset() public function testSetArray() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'w' => [ @@ -420,7 +420,7 @@ public function testSetArray() public function testSetNumericArray() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'winter' => [ @@ -455,7 +455,7 @@ public function testSetNumericArray() public function testWriteConstCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'curl_port' => $arrayFile->const('CURLOPT_PORT') @@ -481,7 +481,7 @@ public function testWriteConstCall() public function testWriteArrayFunctionsAndConstCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'path.to.config' => [ @@ -526,7 +526,7 @@ public function testWriteArrayFunctionsAndConstCall() public function testWriteFunctionCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', true]) @@ -552,7 +552,7 @@ public function testWriteFunctionCall() public function testWriteFunctionCallOverwrite() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', true]) @@ -577,7 +577,7 @@ public function testWriteFunctionCallOverwrite() public function testInsertNull() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', null]), @@ -600,7 +600,7 @@ public function testInsertNull() public function testSortAsc() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'b.b' => 'b', @@ -640,7 +640,7 @@ public function testSortAsc() public function testSortDesc() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'b.a' => 'a', @@ -679,7 +679,7 @@ public function testSortDesc() public function testSortUsort() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::read($file, true); + $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ 'a' => 'a', diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php index 9323f8b4a..8c3ddb3d6 100644 --- a/tests/Parse/EnvFileTest.php +++ b/tests/Parse/EnvFileTest.php @@ -8,7 +8,7 @@ public function testReadFile() { $filePath = __DIR__ . '/../fixtures/parse/test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $this->assertInstanceOf(EnvFile::class, $env); @@ -33,7 +33,7 @@ public function testWriteFile() $filePath = __DIR__ . '/../fixtures/parse/test.env'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $env->write($tmpFile); $result = file_get_contents($tmpFile); @@ -53,7 +53,7 @@ public function testWriteFileWithUpdates() $filePath = __DIR__ . '/../fixtures/parse/test.env'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $env->set('APP_KEY', 'winter'); $env->write($tmpFile); @@ -77,7 +77,7 @@ public function testWriteFileWithUpdatesArray() $filePath = __DIR__ . '/../fixtures/parse/test.env'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $env->set([ 'APP_KEY' => 'winter', 'ROUTES_CACHE' => 'winter', @@ -104,7 +104,7 @@ public function testCasting() $filePath = __DIR__ . '/../fixtures/parse/test.env'; $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $env->set(['APP_KEY' => 'winter']); $env->write($tmpFile); @@ -142,7 +142,7 @@ public function testRender() { $filePath = __DIR__ . '/../fixtures/parse/test.env'; - $env = EnvFile::read($filePath); + $env = EnvFile::open($filePath); $this->assertEquals(file_get_contents($filePath), $env->render()); } diff --git a/tests/fixtures/parse/sample-array-file.php b/tests/fixtures/parse/sample-array-file.php index 582b10b1d..9cdb0c712 100644 --- a/tests/fixtures/parse/sample-array-file.php +++ b/tests/fixtures/parse/sample-array-file.php @@ -15,6 +15,7 @@ 'debug' => true, + // phpcs:ignore "debugAgain" => FALSE , "bullyIan" => 0, From 5643d4d76517281bcd0cb7d2b9d9baab0aea9c35 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 15:10:37 -0600 Subject: [PATCH 075/103] Require PHP Codesniffer > 3.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aaaf4f781..64519e00a 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,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" From 5cfe40dbc0e0139758f43a44fde97015f7942d77 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 15:35:17 -0600 Subject: [PATCH 076/103] Switch back to Laravel's default logic for setting application keys Difference that justifies us continuing to override the command is that we will automatically create the .env file if it doesn't exist. --- src/Foundation/Console/KeyGenerateCommand.php | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/src/Foundation/Console/KeyGenerateCommand.php b/src/Foundation/Console/KeyGenerateCommand.php index 84ffd0845..556e0d60f 100644 --- a/src/Foundation/Console/KeyGenerateCommand.php +++ b/src/Foundation/Console/KeyGenerateCommand.php @@ -3,80 +3,50 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Foundation\Console\KeyGenerateCommand as KeyGenerateCommandBase; +use Winter\Storm\Parse\EnvFile; + class KeyGenerateCommand extends KeyGenerateCommandBase { /** - * Create a new key generator command. + * Write a new environment file with the given key. * - * @param \Illuminate\Filesystem\Filesystem $files + * @param string $key * @return void */ - public function __construct(Filesystem $files) + protected function writeNewEnvironmentFileWith($key) { - parent::__construct(); + $currentKey = $this->laravel['config']['app.key']; - $this->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]; - } } From 57eafe4acde72e3c0a17862392aa58662fdf85f3 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 16:22:06 -0600 Subject: [PATCH 077/103] const() -> constant() --- src/Parse/PHP/ArrayFile.php | 2 +- tests/Parse/ArrayFileTest.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index c75a476fc..ac973fca5 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -394,7 +394,7 @@ public function function(string $name, array $args): PHPFunction * @param string $name * @return PHPConstant */ - public function const(string $name): PHPConstant + public function constant(string $name): PHPConstant { return new PHPConstant($name); } diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index 7b3343259..81690d41e 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -458,7 +458,7 @@ public function testWriteConstCall() $arrayFile = ArrayFile::open($file, true); $arrayFile->set([ - 'curl_port' => $arrayFile->const('CURLOPT_PORT') + 'curl_port' => $arrayFile->constant('CURLOPT_PORT') ]); $arrayFile->set([ @@ -490,8 +490,8 @@ public function testWriteArrayFunctionsAndConstCall() 'test1', 'test2', 'additional' => [ - $arrayFile->const('\Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC'), - $arrayFile->const('\Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC') + $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC'), + $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC') ] ] ] From 9c0057c5bae929fb71706654ea5d1e352584afdd Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 21:42:51 -0600 Subject: [PATCH 078/103] Improvements to the EnvFile parser --- src/Parse/EnvFile.php | 37 +++++++++++-------------------------- tests/Parse/EnvFileTest.php | 2 +- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/Parse/EnvFile.php b/src/Parse/EnvFile.php index cc6cd22a7..dacc06f8c 100644 --- a/src/Parse/EnvFile.php +++ b/src/Parse/EnvFile.php @@ -8,24 +8,22 @@ class EnvFile implements DataFileInterface { /** - * @var array contains the env during modification + * @var array Lines of env data */ protected $env = []; /** - * @var array contains the env lookup map + * @var array Map of variable names to line indexes */ protected $map = []; /** - * @var string|null contains the filepath used to read / write + * @var string|null Filepath currently being worked on */ protected $filePath = null; /** - * EnvFile constructor. - * @param array $env - * @param string $filePath + * EnvFile constructor */ public function __construct(string $filePath) { @@ -59,7 +57,6 @@ public static function open(?string $filePath = null): ?EnvFile * ``` * @param array|string $key * @param mixed|null $value - * @return $this */ public function set($key, $value = null): EnvFile { @@ -89,10 +86,8 @@ public function set($key, $value = null): EnvFile /** * Push a newline onto the end of the env file - * - * @return $this */ - public function addNewLine(): EnvFile + public function addEmptyLine(): EnvFile { $this->env[] = [ 'type' => 'nl' @@ -102,9 +97,7 @@ public function addNewLine(): EnvFile } /** - * Write the current env to a file - * - * @param string|null $filePath + * Write the current env lines to a fileh */ public function write(string $filePath = null): void { @@ -116,9 +109,7 @@ public function write(string $filePath = null): void } /** - * Get the env as a string - * - * @return string + * Get the env lines data as a string */ public function render(): string { @@ -142,8 +133,7 @@ public function render(): string /** * Wrap a value in quotes if needed * - * @param $value - * @return string + * @param mixed $value */ protected function escapeValue($value): string { @@ -175,10 +165,7 @@ protected function escapeValue($value): string } /** - * Parse a .env file, returns an array of the env file data and a key => pos map - * - * @param string $filePath - * @return array + * Parse a .env file, returns an array of the env file data and a key => position map */ protected function parse(string $filePath): array { @@ -232,11 +219,9 @@ protected function parse(string $filePath): array } /** - * Get the current env array - * - * @return array + * Get the variables from the current env lines data as an associative array */ - public function getEnv(): array + public function getVariables(): array { $env = []; diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php index 8c3ddb3d6..647770d69 100644 --- a/tests/Parse/EnvFileTest.php +++ b/tests/Parse/EnvFileTest.php @@ -12,7 +12,7 @@ public function testReadFile() $this->assertInstanceOf(EnvFile::class, $env); - $arr = $env->getEnv(); + $arr = $env->getVariables(); $this->assertArrayHasKey('APP_URL', $arr); $this->assertArrayHasKey('APP_KEY', $arr); From 5b186ac7c25e5505c2fc5af1151f626bebc6d054 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 21:48:21 -0600 Subject: [PATCH 079/103] Use Str facade instead of helper functions --- src/Parse/EnvFile.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Parse/EnvFile.php b/src/Parse/EnvFile.php index dacc06f8c..b143f2f94 100644 --- a/src/Parse/EnvFile.php +++ b/src/Parse/EnvFile.php @@ -1,5 +1,6 @@ Date: Wed, 16 Feb 2022 22:55:40 -0600 Subject: [PATCH 080/103] Cleanup docblocks --- src/Parse/PHP/ArrayFile.php | 47 ++++++++++--------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index ac973fca5..6894dc160 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -52,15 +52,14 @@ public function __construct(array $ast, string $filePath = null, PrettyPrinterAb /** * Return a new instance of `ArrayFile` ready for modification of the file. * - * @param string $filePath - * @param bool $createMissing - * @return ArrayFile|null + * @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 $createMissing = false): ?ArrayFile + public static function open(string $filePath, bool $throwIfMissing = false): ?ArrayFile { $exists = file_exists($filePath); - if (!$exists && !$createMissing) { + if (!$exists && $throwIfMissing) { throw new \InvalidArgumentException('file not found'); } @@ -93,7 +92,6 @@ public static function open(string $filePath, bool $createMissing = false): ?Arr * * @param string|array $key * @param mixed|null $value - * @return $this */ public function set($key, $value = null): ArrayFile { @@ -149,8 +147,7 @@ public function set($key, $value = null): ArrayFile * * @param string $key * @param string $valueType - * @param $value - * @return ArrayItem + * @param mixed $value */ protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem { @@ -167,6 +164,7 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI * * @param string $type * @param mixed $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) @@ -192,15 +190,12 @@ protected function makeAstNode(string $type, $value) case 'array': return $this->castArray($value); default: - throw new \RuntimeException('not implemented replacement type: ' . $type); + throw new \RuntimeException("An unimlemented replacement type ($type) was encountered"); } } /** * Cast an array to AST - * - * @param array $array - * @return Array_ */ protected function castArray(array $array): Array_ { @@ -231,7 +226,6 @@ protected function castArray(array $array): Array_ * Returns type of var passed * * @param mixed $var - * @return string */ protected function getType($var): string { @@ -251,8 +245,7 @@ protected function getType($var): string * * @param string $key * @param string $valueType - * @param $value - * @return ArrayItem + * @param mixed $value */ protected function makeAstArrayRecursive(string $key, string $valueType, $value): ArrayItem { @@ -281,8 +274,7 @@ protected function makeAstArrayRecursive(string $key, string $valueType, $value) * @param array $path * @param $pointer * @param int $depth - * @return array - * @throws SystemException + * @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 { @@ -319,6 +311,7 @@ protected function seek(array $path, &$pointer, int $depth = 0): array * 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 { @@ -333,7 +326,7 @@ public function sort($mode = self::SORT_ASC): ArrayFile $this->sortRecursive($this->ast[0]->expr->items, $mode); break; default: - throw new \InvalidArgumentException('sort type not implemented'); + throw new \InvalidArgumentException('Requested sort type is invalid'); } return $this; @@ -341,10 +334,6 @@ public function sort($mode = self::SORT_ASC): ArrayFile /** * Recursive sort an Array_ item array - * - * @param array $array - * @param string $mode - * @return void */ protected function sortRecursive(array &$array, string $mode): void { @@ -363,9 +352,6 @@ protected function sortRecursive(array &$array, string $mode): void /** * Write the current config to a file - * - * @param string|null $filePath - * @return void */ public function write(string $filePath = null): void { @@ -378,10 +364,6 @@ public function write(string $filePath = null): void /** * Returns a new instance of PHPFunction - * - * @param string $name - * @param array $args - * @return PHPFunction */ public function function(string $name, array $args): PHPFunction { @@ -390,9 +372,6 @@ public function function(string $name, array $args): PHPFunction /** * Returns a new instance of PHPConstant - * - * @param string $name - * @return PHPConstant */ public function constant(string $name): PHPConstant { @@ -400,9 +379,7 @@ public function constant(string $name): PHPConstant } /** - * Get the printed AST as php code - * - * @return string + * Get the printed AST as PHP code */ public function render(): string { From 74ca4d84aad3af1e54bfb714fedbe667fa069236 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 16 Feb 2022 23:03:20 -0600 Subject: [PATCH 081/103] Fix tests, add test case for $throwIfMissing --- tests/Parse/ArrayFileTest.php | 41 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index 81690d41e..fd8713614 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -232,7 +232,7 @@ public function testReadCreateFile() $this->assertFalse(file_exists($file)); - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $this->assertInstanceOf(ArrayFile::class, $arrayFile); @@ -247,7 +247,7 @@ public function testReadCreateFile() public function testWriteDotNotation() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set('w.i.n.t.e.r', 'cms'); $result = eval('?>' . $arrayFile->render()); @@ -264,7 +264,7 @@ public function testWriteDotNotation() public function testWriteDotNotationMixedCase() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set('w.0.n.1.e.2', 'cms'); $result = eval('?>' . $arrayFile->render()); @@ -281,7 +281,7 @@ public function testWriteDotNotationMixedCase() public function testWriteDotNotationMultiple() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $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'); @@ -333,7 +333,7 @@ public function testWriteDotNotationMultiple() public function testWriteDotDuplicateIntKeys() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', 'w.i.2.g' => 'development', @@ -374,7 +374,7 @@ public function testWriteDotDuplicateIntKeys() public function testWriteIllegalOffset() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $this->expectException(\Winter\Storm\Exception\SystemException::class); @@ -384,10 +384,19 @@ public function testWriteIllegalOffset() ]); } + public function testThrowExceptionIfMissing() + { + $file = __DIR__ . '/../fixtures/parse/missing.php'; + + $this->expectException(\InvalidArgumentException::class); + + $arrayFile = ArrayFile::open($file, true); + } + public function testSetArray() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'w' => [ @@ -420,7 +429,7 @@ public function testSetArray() public function testSetNumericArray() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'winter' => [ @@ -455,7 +464,7 @@ public function testSetNumericArray() public function testWriteConstCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'curl_port' => $arrayFile->constant('CURLOPT_PORT') @@ -481,7 +490,7 @@ public function testWriteConstCall() public function testWriteArrayFunctionsAndConstCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'path.to.config' => [ @@ -526,7 +535,7 @@ public function testWriteArrayFunctionsAndConstCall() public function testWriteFunctionCall() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', true]) @@ -552,7 +561,7 @@ public function testWriteFunctionCall() public function testWriteFunctionCallOverwrite() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', true]) @@ -577,7 +586,7 @@ public function testWriteFunctionCallOverwrite() public function testInsertNull() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'key' => $arrayFile->function('env', ['KEY_A', null]), @@ -600,7 +609,7 @@ public function testInsertNull() public function testSortAsc() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'b.b' => 'b', @@ -640,7 +649,7 @@ public function testSortAsc() public function testSortDesc() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'b.a' => 'a', @@ -679,7 +688,7 @@ public function testSortDesc() public function testSortUsort() { $file = __DIR__ . '/../fixtures/parse/empty.php'; - $arrayFile = ArrayFile::open($file, true); + $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'a' => 'a', From f12c2b98f7f44ec947a0acbba9a5d14ff6518cf0 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 17 Feb 2022 20:26:52 -0600 Subject: [PATCH 082/103] Refactor ConfigWriter to use ArrayFile parser internally --- src/Config/ConfigWriter.php | 210 +++--------------------------------- tests/Parse/EnvFileTest.php | 40 +++++++ 2 files changed, 55 insertions(+), 195 deletions(-) diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php index 9761b1cba..2aec2485a 100644 --- a/src/Config/ConfigWriter.php +++ b/src/Config/ConfigWriter.php @@ -2,217 +2,37 @@ use Exception; +use PhpParser\ParserFactory; +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) . '/'; - } + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); - /** - * 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 Exception($e); } - return $result; + return (new ArrayFile($ast, null))->set($newValues)->render(); } } diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php index 647770d69..48412ce36 100644 --- a/tests/Parse/EnvFileTest.php +++ b/tests/Parse/EnvFileTest.php @@ -99,6 +99,46 @@ public function testWriteFileWithUpdatesArray() 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'; From 4a036ea58ab71ab5d7f9dc2989f59f9a9c02f9ac Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 1 Mar 2022 18:38:13 +0000 Subject: [PATCH 083/103] Added config test fixtures --- tests/fixtures/parse/expression.php | 7 +++++++ tests/fixtures/parse/import.php | 8 ++++++++ tests/fixtures/parse/invalid.php | 9 +++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/fixtures/parse/expression.php create mode 100644 tests/fixtures/parse/import.php create mode 100644 tests/fixtures/parse/invalid.php diff --git a/tests/fixtures/parse/expression.php b/tests/fixtures/parse/expression.php new file mode 100644 index 000000000..4a9b08d6a --- /dev/null +++ b/tests/fixtures/parse/expression.php @@ -0,0 +1,7 @@ + $bar +]; diff --git a/tests/fixtures/parse/import.php b/tests/fixtures/parse/import.php new file mode 100644 index 000000000..9cc2108f2 --- /dev/null +++ b/tests/fixtures/parse/import.php @@ -0,0 +1,8 @@ + Response::HTTP_OK, + 'bar' => Response::HTTP_I_AM_A_TEAPOT +]; diff --git a/tests/fixtures/parse/invalid.php b/tests/fixtures/parse/invalid.php new file mode 100644 index 000000000..3eaa533fd --- /dev/null +++ b/tests/fixtures/parse/invalid.php @@ -0,0 +1,9 @@ + winterTest('foo') +]; From ec040435c486b0a5cb369137cf3d5dfa2d932c2e Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 1 Mar 2022 18:38:38 +0000 Subject: [PATCH 084/103] Added support for leading imports & expressions before a return stmt --- src/Parse/PHP/ArrayFile.php | 36 ++++++++++++++++++-- tests/Parse/ArrayFileTest.php | 62 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 6894dc160..2de0718c4 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -35,12 +35,19 @@ class ArrayFile implements DataFileInterface */ protected $printer = null; + /** + * @var int|null Index of ast containing return stmt + */ + protected $astReturnIndex = null; + /** * ArrayFile constructor. */ public function __construct(array $ast, string $filePath = null, PrettyPrinterAbstract $printer = null) { - if (!($ast[0] instanceof Stmt\Return_)) { + $this->astReturnIndex = $this->getAstReturnIndex($ast); + + if (is_null($this->astReturnIndex)) { throw new \InvalidArgumentException('ArrayFiles must start with a return statement'); } @@ -104,7 +111,7 @@ public function set($key, $value = null): ArrayFile } // try to find a reference to ast object - list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[0]->expr); + list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[$this->astReturnIndex]->expr); $valueType = $this->getType($value); @@ -116,7 +123,7 @@ public function set($key, $value = null): ArrayFile // path to not found if (is_null($target)) { - $this->ast[0]->expr->items[] = $this->makeArrayItem($key, $valueType, $value); + $this->ast[$this->astReturnIndex]->expr->items[] = $this->makeArrayItem($key, $valueType, $value); return $this; } @@ -267,6 +274,29 @@ protected function makeAstArrayRecursive(string $key, string $valueType, $value) 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 diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index fd8713614..f83c62ed2 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -226,6 +226,68 @@ public function testRender() $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/invalid.php'); + } + + public function testConfigImports() + { + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/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/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/expression.php'); + + $expected = << \$bar, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + public function testReadCreateFile() { $file = __DIR__ . '/../fixtures/parse/empty.php'; From 0ec555300f060178e0599b95be58863dcb1b9e06 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 2 Mar 2022 16:52:37 +0000 Subject: [PATCH 085/103] Moved fixtures into arrayfile dir --- tests/fixtures/parse/{ => arrayfile}/env-config.php | 0 tests/fixtures/parse/{ => arrayfile}/expression.php | 0 tests/fixtures/parse/{ => arrayfile}/import.php | 0 tests/fixtures/parse/{ => arrayfile}/invalid.php | 0 tests/fixtures/parse/{ => arrayfile}/sample-array-file.php | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename tests/fixtures/parse/{ => arrayfile}/env-config.php (100%) rename tests/fixtures/parse/{ => arrayfile}/expression.php (100%) rename tests/fixtures/parse/{ => arrayfile}/import.php (100%) rename tests/fixtures/parse/{ => arrayfile}/invalid.php (100%) rename tests/fixtures/parse/{ => arrayfile}/sample-array-file.php (100%) diff --git a/tests/fixtures/parse/env-config.php b/tests/fixtures/parse/arrayfile/env-config.php similarity index 100% rename from tests/fixtures/parse/env-config.php rename to tests/fixtures/parse/arrayfile/env-config.php diff --git a/tests/fixtures/parse/expression.php b/tests/fixtures/parse/arrayfile/expression.php similarity index 100% rename from tests/fixtures/parse/expression.php rename to tests/fixtures/parse/arrayfile/expression.php diff --git a/tests/fixtures/parse/import.php b/tests/fixtures/parse/arrayfile/import.php similarity index 100% rename from tests/fixtures/parse/import.php rename to tests/fixtures/parse/arrayfile/import.php diff --git a/tests/fixtures/parse/invalid.php b/tests/fixtures/parse/arrayfile/invalid.php similarity index 100% rename from tests/fixtures/parse/invalid.php rename to tests/fixtures/parse/arrayfile/invalid.php diff --git a/tests/fixtures/parse/sample-array-file.php b/tests/fixtures/parse/arrayfile/sample-array-file.php similarity index 100% rename from tests/fixtures/parse/sample-array-file.php rename to tests/fixtures/parse/arrayfile/sample-array-file.php From 440e1fb41abc6809e67ae49be7ea2f9e2fd139fa Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 2 Mar 2022 16:53:47 +0000 Subject: [PATCH 086/103] Added include test to ensure parens on correct include stmts --- tests/Parse/ArrayFileTest.php | 101 +++++++++++++-------- tests/fixtures/parse/arrayfile/include.php | 13 +++ 2 files changed, 75 insertions(+), 39 deletions(-) create mode 100644 tests/fixtures/parse/arrayfile/include.php diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index f83c62ed2..bc26b88b0 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -6,7 +6,7 @@ class ArrayFileTest extends TestCase { public function testReadFile() { - $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; $arrayFile = ArrayFile::open($filePath); @@ -20,8 +20,8 @@ public function testReadFile() public function testWriteFile() { - $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; - $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; + $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); @@ -37,8 +37,8 @@ public function testWriteFile() public function testWriteFileWithUpdates() { - $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; - $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; + $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'); @@ -55,8 +55,8 @@ public function testWriteFileWithUpdates() public function testWriteFileWithUpdatesArray() { - $filePath = __DIR__ . '/../fixtures/parse/sample-array-file.php'; - $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; $arrayFile = ArrayFile::open($filePath); $arrayFile->set([ @@ -77,8 +77,8 @@ public function testWriteFileWithUpdatesArray() public function testWriteEnvUpdates() { - $filePath = __DIR__ . '/../fixtures/parse/env-config.php'; - $tmpFile = __DIR__ . '/../fixtures/parse/temp-array-file.php'; + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/env-config.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; $arrayFile = ArrayFile::open($filePath); $arrayFile->write($tmpFile); @@ -110,14 +110,14 @@ public function testWriteEnvUpdates() public function testCasting() { - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $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/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('url', false); $result = eval('?>' . $arrayFile->render()); @@ -125,7 +125,7 @@ public function testCasting() $this->assertArrayHasKey('url', $result); $this->assertFalse($result['url']); - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('url', 1234); $result = eval('?>' . $arrayFile->render()); @@ -139,7 +139,7 @@ public function testRender() /* * Rewrite a single level string */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('url', 'https://wintercms.com'); $result = eval('?>' . $arrayFile->render()); @@ -150,7 +150,7 @@ public function testRender() /* * Rewrite a second level string */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('memcached.host', '69.69.69.69'); $result = eval('?>' . $arrayFile->render()); @@ -161,7 +161,7 @@ public function testRender() /* * Rewrite a third level string */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('connections.mysql.host', '127.0.0.1'); $result = eval('?>' . $arrayFile->render()); @@ -173,7 +173,7 @@ public function testRender() /*un- * Test alternative quoting */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $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()); @@ -186,7 +186,7 @@ public function testRender() /* * Rewrite a boolean */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('debug', false) ->set('debugAgain', true) ->set('bullyIan', true) @@ -218,7 +218,7 @@ public function testRender() /* * Rewrite an integer */ - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/sample-array-file.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); $arrayFile->set('aNumber', 69); $result = eval('?>' . $arrayFile->render()); @@ -231,12 +231,12 @@ public function testConfigInvalid() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('ArrayFiles must start with a return statement'); - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/invalid.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/invalid.php'); } public function testConfigImports() { - $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/import.php'); + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/import.php'); $expected = <<set('foo', $arrayFile->constant('Response::HTTP_CONFLICT')); $expected = <<assertFalse(file_exists($file)); @@ -308,7 +308,7 @@ public function testReadCreateFile() public function testWriteDotNotation() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set('w.i.n.t.e.r', 'cms'); @@ -325,7 +325,7 @@ public function testWriteDotNotation() public function testWriteDotNotationMixedCase() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set('w.0.n.1.e.2', 'cms'); @@ -342,7 +342,7 @@ public function testWriteDotNotationMixedCase() public function testWriteDotNotationMultiple() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $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'); @@ -394,7 +394,7 @@ public function testWriteDotNotationMultiple() public function testWriteDotDuplicateIntKeys() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ 'w.i.n.t.e.r' => 'Winter CMS', @@ -435,7 +435,7 @@ public function testWriteDotDuplicateIntKeys() public function testWriteIllegalOffset() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $this->expectException(\Winter\Storm\Exception\SystemException::class); @@ -448,7 +448,7 @@ public function testWriteIllegalOffset() public function testThrowExceptionIfMissing() { - $file = __DIR__ . '/../fixtures/parse/missing.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/missing.php'; $this->expectException(\InvalidArgumentException::class); @@ -457,7 +457,7 @@ public function testThrowExceptionIfMissing() public function testSetArray() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -490,7 +490,7 @@ public function testSetArray() public function testSetNumericArray() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -525,7 +525,7 @@ public function testSetNumericArray() public function testWriteConstCall() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -551,7 +551,7 @@ public function testWriteConstCall() public function testWriteArrayFunctionsAndConstCall() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -596,7 +596,7 @@ public function testWriteArrayFunctionsAndConstCall() public function testWriteFunctionCall() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -622,7 +622,7 @@ public function testWriteFunctionCall() public function testWriteFunctionCallOverwrite() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -647,7 +647,7 @@ public function testWriteFunctionCallOverwrite() public function testInsertNull() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -670,7 +670,7 @@ public function testInsertNull() public function testSortAsc() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -710,7 +710,7 @@ public function testSortAsc() public function testSortDesc() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -749,7 +749,7 @@ public function testSortDesc() public function testSortUsort() { - $file = __DIR__ . '/../fixtures/parse/empty.php'; + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; $arrayFile = ArrayFile::open($file); $arrayFile->set([ @@ -773,6 +773,29 @@ public function testSortUsort() '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()); } 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' +]; From c098853f8e082e4a29e73c8dc21d873195337777 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 2 Mar 2022 16:54:43 +0000 Subject: [PATCH 087/103] Added code to check include position in ast and append parens correctly --- src/Parse/PHP/ArrayPrinter.php | 69 +++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index d9b4b7a33..29324d4b3 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -1,6 +1,7 @@ hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof ArrayItem)) { + if ($this->hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof Expr\ArrayItem)) { return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl; } else { return $this->pCommaSeparated($nodes); } } + /** + * Prints reformatted text of the passed comments. + * + * @param Comment[] $comments List of comments + * + * @return string Reformatted text of comments + */ protected function pComments(array $comments): string { $formattedComments = []; @@ -40,4 +48,61 @@ protected function pComments(array $comments): string return $padding . implode($this->nl, $formattedComments) . $padding; } + + 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', + ]; + + $includeInConfigArray = false; + + if ($map[$node->type] === 'include') { + foreach (debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50) as $backtrace) { + if ( + $backtrace['function'] !== 'pStmts' + || !isset($backtrace['args'][0]) + || !is_array($backtrace['args'][0]) + ) { + continue; + } + + foreach ($backtrace['args'][0] as $arg) { + if (!($arg instanceof Stmt\Return_)) { + continue; + } + + $includeInConfigArray = ($iterator = function ($arg) use (&$iterator, $node) { + if ($arg instanceof Expr\Array_) { + foreach ($arg->items as $item) { + if ($iterator($item)) { + return true; + } + } + } + if ($arg instanceof Expr\ArrayItem) { + if ($arg->value instanceof Expr\FuncCall) { + foreach ($arg->value->args as $funcArg) { + if ($iterator($funcArg->value)) { + return true; + } + } + } + } + if ($arg instanceof Expr\Include_ && $node === $arg) { + return true; + } + return false; + })($arg->expr); + } + } + } + + return $includeInConfigArray + ? $map[$node->type] . '(' . $this->p($node->expr) . ')' + : $map[$node->type] . ' ' . $this->p($node->expr); + } } From 892ed2a1dd87e4bb75adce332fe96288fcf16260 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 2 Mar 2022 16:56:33 +0000 Subject: [PATCH 088/103] Simplified code by always apending parens on include/require stmts --- src/Parse/PHP/ArrayPrinter.php | 47 +--------------------------------- tests/Parse/ArrayFileTest.php | 8 +++--- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 29324d4b3..6c0118e8f 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -58,51 +58,6 @@ protected function pExpr_Include(Expr\Include_ $node) Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once', ]; - $includeInConfigArray = false; - - if ($map[$node->type] === 'include') { - foreach (debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 50) as $backtrace) { - if ( - $backtrace['function'] !== 'pStmts' - || !isset($backtrace['args'][0]) - || !is_array($backtrace['args'][0]) - ) { - continue; - } - - foreach ($backtrace['args'][0] as $arg) { - if (!($arg instanceof Stmt\Return_)) { - continue; - } - - $includeInConfigArray = ($iterator = function ($arg) use (&$iterator, $node) { - if ($arg instanceof Expr\Array_) { - foreach ($arg->items as $item) { - if ($iterator($item)) { - return true; - } - } - } - if ($arg instanceof Expr\ArrayItem) { - if ($arg->value instanceof Expr\FuncCall) { - foreach ($arg->value->args as $funcArg) { - if ($iterator($funcArg->value)) { - return true; - } - } - } - } - if ($arg instanceof Expr\Include_ && $node === $arg) { - return true; - } - return false; - })($arg->expr); - } - } - } - - return $includeInConfigArray - ? $map[$node->type] . '(' . $this->p($node->expr) . ')' - : $map[$node->type] . ' ' . $this->p($node->expr); + return $map[$node->type] . '(' . $this->p($node->expr) . ')'; } } diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index bc26b88b0..ccea6d8d5 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -785,10 +785,10 @@ public function testIncludeFormatting() $expected = << array_merge(include(__DIR__ . '/sample-array-file.php'), [ 'bar' => 'foo', From 9fda6cd1cc4cf52b71c6297a18d22ed8d1a7454b Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 3 Mar 2022 19:30:00 -0600 Subject: [PATCH 089/103] Trim useless whitespace @jaxwilko's favourite piece of the whole ArrayFile parser ;) --- src/Parse/PHP/ArrayFile.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 2de0718c4..ddd78b23d 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -413,7 +413,12 @@ public function constant(string $name): PHPConstant */ public function render(): string { - return $this->printer->prettyPrintFile($this->ast) . "\n"; + // Make sure lines with only indentation are trimmed + return preg_replace( + '/^\s+$/m', + '', + $this->printer->prettyPrintFile($this->ast) . "\n" + ); } /** From 91f1dac58e7859fbcabc25ff5a2d9d0cfc684b16 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 4 Mar 2022 18:46:15 +0000 Subject: [PATCH 090/103] Added fixes for whitespace issues --- src/Parse/PHP/ArrayFile.php | 7 +--- src/Parse/PHP/ArrayPrinter.php | 38 ++++++++++++++++- tests/Parse/ArrayFileTest.php | 21 ++++++++++ .../parse/arrayfile/nested-comments.php | 41 +++++++++++++++++++ 4 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/parse/arrayfile/nested-comments.php diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index ddd78b23d..2de0718c4 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -413,12 +413,7 @@ public function constant(string $name): PHPConstant */ public function render(): string { - // Make sure lines with only indentation are trimmed - return preg_replace( - '/^\s+$/m', - '', - $this->printer->prettyPrintFile($this->ast) . "\n" - ); + return $this->printer->prettyPrintFile($this->ast) . "\n"; } /** diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 6c0118e8f..3fd87db39 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -1,5 +1,6 @@ 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; + } + /** * Prints reformatted text of the passed comments. * @@ -46,7 +82,7 @@ protected function pComments(array $comments): string $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : ''; - return $padding . implode($this->nl, $formattedComments) . $padding; + return "\n" . $this->nl . trim($padding . implode($this->nl, $formattedComments)) . "\n"; } protected function pExpr_Include(Expr\Include_ $node) diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index ccea6d8d5..965f51077 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -799,4 +799,25 @@ public function testIncludeFormatting() 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); + } } 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, + ], +]; From 78bcf118ea2458201a071482cd1b67bb7d6294d4 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 15 Mar 2022 22:24:01 +0000 Subject: [PATCH 091/103] Bound lexer to ArrayFile instance --- src/Parse/PHP/ArrayFile.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 2de0718c4..9bcc5907b 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -1,6 +1,7 @@ astReturnIndex = $this->getAstReturnIndex($ast); @@ -52,6 +58,7 @@ public function __construct(array $ast, string $filePath = null, PrettyPrinterAb } $this->ast = $ast; + $this->lexer = $lexer; $this->filePath = $filePath; $this->printer = $printer ?? new ArrayPrinter(); } @@ -70,7 +77,15 @@ public static function open(string $filePath, bool $throwIfMissing = false): ?Ar throw new \InvalidArgumentException('file not found'); } - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer = new Lexer\Emulative([ + 'usedAttributes' => [ + 'comments', + 'startTokenPos', + 'startLine', + 'endTokenPos', + 'endLine' + ] + ])); try { $ast = $parser->parse( @@ -82,7 +97,7 @@ public static function open(string $filePath, bool $throwIfMissing = false): ?Ar throw new SystemException($e); } - return new static($ast, $filePath); + return new static($ast, $lexer, $filePath); } /** @@ -413,7 +428,7 @@ public function constant(string $name): PHPConstant */ public function render(): string { - return $this->printer->prettyPrintFile($this->ast) . "\n"; + return $this->printer->render($this->ast, $this->lexer) . "\n"; } /** From 573e606d0727ef3a6680e506bc1e39672e08b2f6 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 15 Mar 2022 22:25:04 +0000 Subject: [PATCH 092/103] Added support for lexer token parsing and additional comment handling --- src/Parse/PHP/ArrayPrinter.php | 214 ++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 1 deletion(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 3fd87db39..2d3b89d24 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -1,5 +1,7 @@ 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 @@ -48,6 +94,7 @@ protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma) : foreach ($nodes as $idx => $node) { if ($node !== null) { $comments = $node->getComments(); + if ($comments) { $result .= $this->pComments($comments); } @@ -65,10 +112,175 @@ protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma) : 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][0] === T_WHITESPACE) { + continue; + } + + list($type, $string, $line) = $tokens[$pos]; + + if ($line > $endLine) { + break; + } + + if ($type === T_COMMENT || $type === T_DOC_COMMENT) { + $content[] = $string; + } elseif ($content) { + return $content; + } + } + + return empty($content) ? null : $content; + } + /** * Prints reformatted text of the passed comments. * - * @param Comment[] $comments List of comments + * @param array $comments List of comments * * @return string Reformatted text of comments */ From 6ee5ec3e39740f9a367fa702c7409c80df97359d Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 15 Mar 2022 22:25:40 +0000 Subject: [PATCH 093/103] Added non-attribute comment handling test --- tests/Parse/ArrayFileTest.php | 7 ++++ .../parse/arrayfile/single-line-comments.php | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/fixtures/parse/arrayfile/single-line-comments.php diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index 965f51077..1f4e6c2b4 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -820,4 +820,11 @@ public function testNestedComments() $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)), $arrayFile->render()); + } } 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 + ]), + ], +]; From 725fb4deed34296f600eb9cd8be1526637096e0e Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 15 Mar 2022 22:27:48 +0000 Subject: [PATCH 094/103] Added style fixes --- src/Parse/PHP/ArrayPrinter.php | 3 ++- tests/fixtures/parse/arrayfile/invalid.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 2d3b89d24..4d89e59cb 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -86,7 +86,8 @@ protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) * * @return string Comma separated pretty printed nodes in multiline style */ - protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma) : string { + protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string + { $this->indent(); $result = ''; diff --git a/tests/fixtures/parse/arrayfile/invalid.php b/tests/fixtures/parse/arrayfile/invalid.php index 3eaa533fd..c79300071 100644 --- a/tests/fixtures/parse/arrayfile/invalid.php +++ b/tests/fixtures/parse/arrayfile/invalid.php @@ -1,6 +1,6 @@ Date: Tue, 15 Mar 2022 22:29:21 +0000 Subject: [PATCH 095/103] More styling fixes in test fixture --- tests/fixtures/parse/arrayfile/invalid.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/parse/arrayfile/invalid.php b/tests/fixtures/parse/arrayfile/invalid.php index c79300071..f734f1b9f 100644 --- a/tests/fixtures/parse/arrayfile/invalid.php +++ b/tests/fixtures/parse/arrayfile/invalid.php @@ -1,6 +1,7 @@ Date: Tue, 15 Mar 2022 22:35:11 +0000 Subject: [PATCH 096/103] Added fix to ConfigWriter to support passing lexer to ArrayFile instance --- src/Config/ConfigWriter.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php index 2aec2485a..abf998f2c 100644 --- a/src/Config/ConfigWriter.php +++ b/src/Config/ConfigWriter.php @@ -2,7 +2,10 @@ use Exception; +use PhpParser\Error; +use PhpParser\Lexer\Emulative; use PhpParser\ParserFactory; +use Winter\Storm\Exception\SystemException; use Winter\Storm\Parse\PHP\ArrayFile; /** @@ -25,14 +28,22 @@ public function toFile(string $filePath, array $newValues): string public function toContent(string $contents, $newValues): string { - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer = new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startTokenPos', + 'startLine', + 'endTokenPos', + 'endLine' + ] + ])); try { $ast = $parser->parse($contents); } catch (Error $e) { - throw new Exception($e); + throw new SystemException($e); } - return (new ArrayFile($ast, null))->set($newValues)->render(); + return (new ArrayFile($ast, $lexer, null))->set($newValues)->render(); } } From 62950e02ed7095a5f2cbfb1ce376acd5fcef5341 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Tue, 15 Mar 2022 22:37:54 +0000 Subject: [PATCH 097/103] Added \r trim to comment test --- tests/Parse/ArrayFileTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index 1f4e6c2b4..3fbeb7e91 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -825,6 +825,9 @@ 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)), $arrayFile->render()); + $this->assertEquals( + str_replace("\r", '', file_get_contents($file)), + str_replace("\r", '', $arrayFile->render()) + ); } } From 5ca79e7b91ade1eaf382e132de3e7c5b1b3ea115 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Wed, 16 Mar 2022 11:32:56 +0800 Subject: [PATCH 098/103] Update src/Foundation/Console/KeyGenerateCommand.php --- src/Foundation/Console/KeyGenerateCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Foundation/Console/KeyGenerateCommand.php b/src/Foundation/Console/KeyGenerateCommand.php index 556e0d60f..be6fe6e1d 100644 --- a/src/Foundation/Console/KeyGenerateCommand.php +++ b/src/Foundation/Console/KeyGenerateCommand.php @@ -2,7 +2,6 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Foundation\Console\KeyGenerateCommand as KeyGenerateCommandBase; - use Winter\Storm\Parse\EnvFile; class KeyGenerateCommand extends KeyGenerateCommandBase From d14374da95def421dc4f6b82af1f444fc9fdb9c7 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Tue, 15 Mar 2022 21:38:40 -0600 Subject: [PATCH 099/103] KeyGenerateCommand cleanup --- src/Foundation/Console/KeyGenerateCommand.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Foundation/Console/KeyGenerateCommand.php b/src/Foundation/Console/KeyGenerateCommand.php index be6fe6e1d..931ed226c 100644 --- a/src/Foundation/Console/KeyGenerateCommand.php +++ b/src/Foundation/Console/KeyGenerateCommand.php @@ -1,6 +1,5 @@ laravel['config']['app.key']; - $env = EnvFile::open($this->laravel->environmentFilePath()); $env->set('APP_KEY', $key); $env->write(); From b0e7b4f599cbcc3fa8233269f7aa4ebed754e7d9 Mon Sep 17 00:00:00 2001 From: Ben Thomson Date: Wed, 16 Mar 2022 11:49:03 +0800 Subject: [PATCH 100/103] Code dusting and explicit typing from review --- src/Config/ConfigWriter.php | 5 +-- src/Parse/Contracts/DataFileInterface.php | 11 ++----- src/Parse/EnvFile.php | 20 ++++++------ src/Parse/PHP/ArrayFile.php | 38 +++++++++-------------- 4 files changed, 30 insertions(+), 44 deletions(-) diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php index abf998f2c..75f40831a 100644 --- a/src/Config/ConfigWriter.php +++ b/src/Config/ConfigWriter.php @@ -28,7 +28,7 @@ public function toFile(string $filePath, array $newValues): string public function toContent(string $contents, $newValues): string { - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer = new Emulative([ + $lexer = new Emulative([ 'usedAttributes' => [ 'comments', 'startTokenPos', @@ -36,7 +36,8 @@ public function toContent(string $contents, $newValues): string 'endTokenPos', 'endLine' ] - ])); + ]); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer); try { $ast = $parser->parse($contents); diff --git a/src/Parse/Contracts/DataFileInterface.php b/src/Parse/Contracts/DataFileInterface.php index 40e9dcb3e..949fc0066 100644 --- a/src/Parse/Contracts/DataFileInterface.php +++ b/src/Parse/Contracts/DataFileInterface.php @@ -5,22 +5,17 @@ interface DataFileInterface /** * Return a new instance of `DataFileInterface` ready for modification of the provided filepath. */ - public static function open(string $filePath): ?DataFileInterface; + public static function open(string $filePath): static; /** * Set a property within the data. - * - * @param string|array $key - * @param mixed|null $value */ - public function set($key, $value = null): DataFileInterface; + public function set(string|array $key, $value = null): static; /** * Write the current data to a file - * - * @param string|null $filePath */ - public function write(string $filePath = null): void; + public function write(?string $filePath = null): void; /** * Get the printed data diff --git a/src/Parse/EnvFile.php b/src/Parse/EnvFile.php index b143f2f94..6276885dc 100644 --- a/src/Parse/EnvFile.php +++ b/src/Parse/EnvFile.php @@ -9,24 +9,24 @@ class EnvFile implements DataFileInterface { /** - * @var array Lines of env data + * Lines of env data */ - protected $env = []; + protected array $env = []; /** - * @var array Map of variable names to line indexes + * Map of variable names to line indexes */ - protected $map = []; + protected array $map = []; /** - * @var string|null Filepath currently being worked on + * Filepath currently being worked on */ - protected $filePath = null; + protected ?string $filePath = null; /** * EnvFile constructor */ - public function __construct(string $filePath) + final public function __construct(string $filePath) { $this->filePath = $filePath; @@ -36,7 +36,7 @@ public function __construct(string $filePath) /** * Return a new instance of `EnvFile` ready for modification of the file. */ - public static function open(?string $filePath = null): ?EnvFile + public static function open(?string $filePath = null): static { if (!$filePath) { $filePath = base_path('.env'); @@ -56,10 +56,8 @@ public static function open(?string $filePath = null): ?EnvFile * 'DIF_PROPERTY' => 'example' * ]); * ``` - * @param array|string $key - * @param mixed|null $value */ - public function set($key, $value = null): EnvFile + public function set(array|string $key, $value = null): static { if (is_array($key)) { foreach ($key as $item => $value) { diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php index 9bcc5907b..fe18024aa 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -24,32 +24,32 @@ class ArrayFile implements DataFileInterface /** * @var Stmt[]|null Abstract syntax tree produced by `PhpParser` */ - protected $ast = null; + protected ?array $ast = null; /** - * @var Lexer|null Lexer for use by `PhpParser` + * Lexer for use by `PhpParser` */ - protected $lexer = null; + protected ?Lexer $lexer = null; /** - * @var string|null Path to the file + * Path to the file */ - protected $filePath = null; + protected ?string $filePath = null; /** - * @var PrettyPrinterAbstract|ArrayPrinter|null Printer used to define output syntax + * Printer used to define output syntax */ - protected $printer = null; + protected PrettyPrinterAbstract|ArrayPrinter|null $printer = null; /** - * @var int|null Index of ast containing return stmt + * Index of ast containing return stmt */ - protected $astReturnIndex = null; + protected ?int $astReturnIndex = null; /** * ArrayFile constructor. */ - public function __construct(array $ast, Lexer $lexer, string $filePath = null, PrettyPrinterAbstract $printer = null) + final public function __construct(array $ast, Lexer $lexer, string $filePath = null, PrettyPrinterAbstract $printer = null) { $this->astReturnIndex = $this->getAstReturnIndex($ast); @@ -69,7 +69,7 @@ public function __construct(array $ast, Lexer $lexer, string $filePath = null, P * @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): ?ArrayFile + public static function open(string $filePath, bool $throwIfMissing = false): static { $exists = file_exists($filePath); @@ -77,7 +77,7 @@ public static function open(string $filePath, bool $throwIfMissing = false): ?Ar throw new \InvalidArgumentException('file not found'); } - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer = new Lexer\Emulative([ + $lexer = new Lexer\Emulative([ 'usedAttributes' => [ 'comments', 'startTokenPos', @@ -85,7 +85,8 @@ public static function open(string $filePath, bool $throwIfMissing = false): ?Ar 'endTokenPos', 'endLine' ] - ])); + ]); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer); try { $ast = $parser->parse( @@ -111,11 +112,8 @@ public static function open(string $filePath, bool $throwIfMissing = false): ?Ar * 'property.key2.value' => 'example' * ]); * ``` - * - * @param string|array $key - * @param mixed|null $value */ - public function set($key, $value = null): ArrayFile + public function set(string|array $key, $value = null): static { if (is_array($key)) { foreach ($key as $name => $value) { @@ -166,10 +164,6 @@ public function set($key, $value = null): ArrayFile /** * Creates either a simple array item or a recursive array of items - * - * @param string $key - * @param string $valueType - * @param mixed $value */ protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem { @@ -184,8 +178,6 @@ protected function makeArrayItem(string $key, string $valueType, $value): ArrayI /** * Generate an AST node, using `PhpParser` classes, for a value * - * @param string $type - * @param mixed $value * @throws \RuntimeException If $type is not one of 'string', 'boolean', 'integer', 'function', 'const', 'null', or 'array' * @return ConstFetch|LNumber|String_|Array_|FuncCall */ From e6c8434f3052f10e850656be5b974d718cd2e48e Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 16 Mar 2022 12:16:38 +0000 Subject: [PATCH 101/103] Added subitem comment test --- tests/Parse/ArrayFileTest.php | 12 ++++ .../single-line-comments-subitem.php | 60 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/fixtures/parse/arrayfile/single-line-comments-subitem.php diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php index 3fbeb7e91..bb7a85090 100644 --- a/tests/Parse/ArrayFileTest.php +++ b/tests/Parse/ArrayFileTest.php @@ -825,6 +825,18 @@ 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/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', + ], + ], +]; From 48996796e78ba2b661189d4380b573b6219beea4 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 16 Mar 2022 12:18:12 +0000 Subject: [PATCH 102/103] Added fix for subitems being picked up in lexer tokens --- src/Parse/PHP/ArrayPrinter.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 4d89e59cb..5ad5f6f36 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -258,7 +258,11 @@ protected function getNodeComments(Node $node): ?array $content = []; while (++$pos < $end) { - if (!isset($tokens[$pos]) || !is_array($tokens[$pos]) || $tokens[$pos][0] === T_WHITESPACE) { + if (!isset($tokens[$pos]) || !is_array($tokens[$pos])) { + break; + } + + if ($tokens[$pos][0] === T_WHITESPACE) { continue; } @@ -271,7 +275,7 @@ protected function getNodeComments(Node $node): ?array if ($type === T_COMMENT || $type === T_DOC_COMMENT) { $content[] = $string; } elseif ($content) { - return $content; + break; } } From c7a5f6aae1ac15855055a57492e19523cde285e1 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Wed, 16 Mar 2022 13:06:32 +0000 Subject: [PATCH 103/103] Added fix for token parsing running into comma tokens and breaking --- src/Parse/PHP/ArrayPrinter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 5ad5f6f36..04bf38937 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -258,11 +258,11 @@ protected function getNodeComments(Node $node): ?array $content = []; while (++$pos < $end) { - if (!isset($tokens[$pos]) || !is_array($tokens[$pos])) { + if (!isset($tokens[$pos]) || (!is_array($tokens[$pos]) && $tokens[$pos] !== ',')) { break; } - if ($tokens[$pos][0] === T_WHITESPACE) { + if ($tokens[$pos][0] === T_WHITESPACE || $tokens[$pos] === ',') { continue; }