Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement] Add ArrayFile & EnvFile Parsing, replace ConfigWriter internal logic #40

Merged
merged 108 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
4c71911
Required nikic/php-parser
jaxwilko Jun 30, 2021
dd20a56
Added custom pretty printer for winter style configs
jaxwilko Jun 30, 2021
00020fa
Added ConfigFile class for modifying configs
jaxwilko Jun 30, 2021
0707951
Added class doc block
jaxwilko Jun 30, 2021
45aadb7
Added doc blocks and applyed Winter code styling
jaxwilko Jun 30, 2021
eb76cc4
Removed debug code
jaxwilko Jun 30, 2021
0d4b2e2
Added method to retrieve ast
jaxwilko Jun 30, 2021
179af7c
Added ConfigFile tests
jaxwilko Jun 30, 2021
d75b3a8
Added additional handling for object types
jaxwilko Jun 30, 2021
524561f
Added fix so single line comments do not recieve nl padding
jaxwilko Jul 1, 2021
3520e88
Added array input and casting tests
jaxwilko Jul 2, 2021
09eff7b
Added type casting functionality
jaxwilko Jul 2, 2021
7156fec
Added env default update test
jaxwilko Jul 2, 2021
4e98e46
Switched to replace nodes rather than update in place for simplicity
jaxwilko Jul 2, 2021
ec8d643
Added doc block for makeAstNode method
jaxwilko Jul 2, 2021
d3f97e1
Cleaned up switch statment
jaxwilko Jul 2, 2021
31c405a
Update src/Config/ConfigFile.php
jaxwilko Jul 6, 2021
581e616
Update src/Config/ConfigFile.php
jaxwilko Jul 6, 2021
9904d7c
Update src/Config/ConfigFile.php
jaxwilko Jul 6, 2021
6859072
Update src/Config/ConfigFile.php
jaxwilko Jul 6, 2021
c097187
Added descriptive comments
jaxwilko Jul 14, 2021
418d6cd
Added fixes to set method to ensure types are correctly updated
jaxwilko Jul 14, 2021
b2fac15
Updated dot notation comment
jaxwilko Jul 14, 2021
7745ad1
Added interface for config file modifiers
jaxwilko Jul 25, 2021
96dba39
Added interface to ConfigFile class
jaxwilko Jul 25, 2021
d475c2c
Added EnvFile class and tests
jaxwilko Jul 25, 2021
7fe60cc
Removed File class usage in favour of file_put_contents
jaxwilko Jul 25, 2021
bbae058
Commented out return types
jaxwilko Jul 25, 2021
f74b15a
Removed redundent line
jaxwilko Aug 6, 2021
d58411d
Added test comments
jaxwilko Aug 6, 2021
a8cf24e
Switched parser for simple interpreter
jaxwilko Aug 6, 2021
b0a9c70
Fixed styling issue and unparenthesized ternary issue
jaxwilko Aug 6, 2021
14fd3c8
Improved EnvFile tests
jaxwilko Oct 27, 2021
cc439eb
Improved parsing and rendering
jaxwilko Oct 27, 2021
71765a8
Added support for creating file on read and appending items to empty …
jaxwilko Nov 23, 2021
7d8090a
Added constructor to enable short syntax
jaxwilko Nov 25, 2021
69cd152
Added tests for recursive array creation
jaxwilko Nov 25, 2021
ce8f252
Added support for recursive array creation using dot notation
jaxwilko Nov 25, 2021
ba5ed71
Added ConfigFunction class
jaxwilko Nov 25, 2021
7787df1
Added tests for adding new functions to a config
jaxwilko Nov 25, 2021
51ef587
Added support for setting function calls
jaxwilko Nov 25, 2021
4c421b9
Added proper class doc
jaxwilko Nov 25, 2021
b25b8cd
Improved function value type handling
jaxwilko Nov 25, 2021
0581fc6
Added test for null insertion
jaxwilko Dec 2, 2021
a010a9c
Added support for null inserts
jaxwilko Dec 2, 2021
f9aedbe
Added double quote escaping
jaxwilko Jan 14, 2022
cdbf2f4
Merge branch 'develop' into wip/config-writer-replacement
jaxwilko Jan 14, 2022
57a649a
Updated ApplicationExceptions to SystemExceptions
jaxwilko Jan 14, 2022
cda726f
Updated test to match the correct exception
jaxwilko Jan 14, 2022
dcef6b1
Added support for setting env() args to correct types
jaxwilko Jan 14, 2022
bae73fa
Added config sort tests
jaxwilko Jan 14, 2022
f0c9454
Added sort() method for ConfigFile
jaxwilko Jan 14, 2022
110afdf
Simplified internal sorting methods
jaxwilko Jan 15, 2022
bf6c16e
Fixed test to be consitant across php versions (RFC: stable_sorting)
jaxwilko Jan 15, 2022
4f4a00a
Switched line ending to unix style when creating empty file
jaxwilko Jan 15, 2022
715abd7
Switched line ending for rendered output
jaxwilko Jan 17, 2022
2532b67
Switched heredoc test samples for quoted strings
jaxwilko Jan 17, 2022
1db7bfc
Removed incorrect newline at end of expected results
jaxwilko Jan 20, 2022
36a4044
Reverted to heredoc and removed carriage returns from expected samples
jaxwilko Jan 20, 2022
45e3058
Removed usage of PHP_EOL in favour of lf
jaxwilko Jan 20, 2022
29efd28
Added class to represent const expressions
jaxwilko Jan 27, 2022
87d2d9c
Fixed incorrect comment
jaxwilko Jan 27, 2022
1406191
Added tests for setting arrays and const values
jaxwilko Jan 27, 2022
0caba9d
Fixed incorrect method description comment
jaxwilko Jan 27, 2022
1ca70f3
Added support for setting an array and const value
jaxwilko Jan 27, 2022
4314dfe
Simplified key insertion logic
jaxwilko Jan 27, 2022
f60a0ff
Added test for numeric array keys
jaxwilko Jan 27, 2022
29067a8
Fixed spelling mistake
jaxwilko Jan 27, 2022
c2a0aa2
Swapped out redundent if statement for assigning result of expression
jaxwilko Jan 27, 2022
4f19150
Merge 1.2 into config-writer-replacement
LukeTowers Feb 16, 2022
2d64825
Move config writer into Parse
LukeTowers Feb 16, 2022
7c9c63b
Move ArrayFile tests out of config and into parse
LukeTowers Feb 16, 2022
6f6d073
Various cleanup and code review
LukeTowers Feb 16, 2022
4f43d6c
Fix test name mismatch
LukeTowers Feb 16, 2022
f4da8f6
PHPConst -> PHPConstant
LukeTowers Feb 16, 2022
9ea747c
FileInterface -> DataFileInterface, read() -> open()
LukeTowers Feb 16, 2022
5643d4d
Require PHP Codesniffer > 3.2
LukeTowers Feb 16, 2022
5cfe40d
Switch back to Laravel's default logic for setting application keys
LukeTowers Feb 16, 2022
57eafe4
const() -> constant()
LukeTowers Feb 16, 2022
9c0057c
Improvements to the EnvFile parser
LukeTowers Feb 17, 2022
5b186ac
Use Str facade instead of helper functions
LukeTowers Feb 17, 2022
a9fa826
Cleanup docblocks
LukeTowers Feb 17, 2022
74ca4d8
Fix tests, add test case for $throwIfMissing
LukeTowers Feb 17, 2022
f12c2b9
Refactor ConfigWriter to use ArrayFile parser internally
LukeTowers Feb 18, 2022
4a036ea
Added config test fixtures
jaxwilko Mar 1, 2022
ec04043
Added support for leading imports & expressions before a return stmt
jaxwilko Mar 1, 2022
f74bd5a
Manual merge wip/1.2 into wip/config-writer-replacement
jaxwilko Mar 1, 2022
0ec5553
Moved fixtures into arrayfile dir
jaxwilko Mar 2, 2022
440e1fb
Added include test to ensure parens on correct include stmts
jaxwilko Mar 2, 2022
c098853
Added code to check include position in ast and append parens correctly
jaxwilko Mar 2, 2022
892ed2a
Simplified code by always apending parens on include/require stmts
jaxwilko Mar 2, 2022
cf98dcf
Merge branch 'wip/1.2' into wip/config-writer-replacement
LukeTowers Mar 4, 2022
9fda6cd
Trim useless whitespace
LukeTowers Mar 4, 2022
91f1dac
Added fixes for whitespace issues
jaxwilko Mar 4, 2022
78bcf11
Bound lexer to ArrayFile instance
jaxwilko Mar 15, 2022
573e606
Added support for lexer token parsing and additional comment handling
jaxwilko Mar 15, 2022
6ee5ec3
Added non-attribute comment handling test
jaxwilko Mar 15, 2022
725fb4d
Added style fixes
jaxwilko Mar 15, 2022
6e0ddb3
More styling fixes in test fixture
jaxwilko Mar 15, 2022
7e56951
Added fix to ConfigWriter to support passing lexer to ArrayFile instance
jaxwilko Mar 15, 2022
62950e0
Added \r trim to comment test
jaxwilko Mar 15, 2022
5ca79e7
Update src/Foundation/Console/KeyGenerateCommand.php
bennothommo Mar 16, 2022
d14374d
KeyGenerateCommand cleanup
LukeTowers Mar 16, 2022
b0e7b4f
Code dusting and explicit typing from review
bennothommo Mar 16, 2022
e6c8434
Added subitem comment test
jaxwilko Mar 16, 2022
4899679
Added fix for subitems being picked up in lexer tokens
jaxwilko Mar 16, 2022
e15806d
Merge branch 'wip/config-writer-replacement' of github.com:wintercms/…
jaxwilko Mar 16, 2022
c7a5f6a
Added fix for token parsing running into comma tokens and breaking
jaxwilko Mar 16, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"laravel/tinker": "^2.7",
"league/csv": "~9.1",
"nesbot/carbon": "^2.0",
"nikic/php-parser": "^4.10",
"scssphp/scssphp": "~1.0",
"symfony/yaml": "^6.0",
"twig/twig": "~3.0",
Expand All @@ -51,7 +52,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5.8",
"mockery/mockery": "^1.4.4",
"squizlabs/php_codesniffer": "3.*",
"squizlabs/php_codesniffer": "^3.2",
"php-parallel-lint/php-parallel-lint": "^1.0",
"meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0",
"dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1"
Expand Down
222 changes: 27 additions & 195 deletions src/Config/ConfigWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,217 +2,49 @@

use Exception;

use PhpParser\Error;
use PhpParser\Lexer\Emulative;
use PhpParser\ParserFactory;
use Winter\Storm\Exception\SystemException;
use Winter\Storm\Parse\PHP\ArrayFile;

/**
* Configuration rewriter
*
* https://github.com/daftspunk/laravel-config-writer
* @see https://wintercms.com/docs/services/parser#data-file-array
*
* This class lets you rewrite array values inside a basic configuration file
* that returns a single array definition (a Laravel config file) whilst maintaining
* the integrity of the file, leaving comments and advanced settings intact.
*
* The following value types are supported for writing:
* - strings
* - integers
* - booleans
* - nulls
* - single-dimension arrays
*
* To do:
* - When an entry does not exist, provide a way to create it
*
* Pro Regextip: Use [\s\S] instead of . for multiline support
*/
class ConfigWriter
{
public function toFile($filePath, $newValues, $useValidation = true)
{
$contents = file_get_contents($filePath);
$contents = $this->toContent($contents, $newValues, $useValidation);
file_put_contents($filePath, $contents);
return $contents;
}

public function toContent($contents, $newValues, $useValidation = true)
{
$contents = $this->parseContent($contents, $newValues);

if (!$useValidation) {
return $contents;
}

$result = eval('?>'.$contents);

foreach ($newValues as $key => $expectedValue) {
$parts = explode('.', $key);

$array = $result;
foreach ($parts as $part) {
if (!is_array($array) || !array_key_exists($part, $array)) {
throw new Exception(sprintf('Unable to rewrite key "%s" in config, does it exist?', $key));
}

$array = $array[$part];
}
$actualValue = $array;

if ($actualValue != $expectedValue) {
throw new Exception(sprintf('Unable to rewrite key "%s" in config, rewrite failed', $key));
}
}

return $contents;
}

protected function parseContent($contents, $newValues)
{
$result = $contents;

foreach ($newValues as $path => $value) {
$result = $this->parseContentValue($result, $path, $value);
}

return $result;
}

protected function parseContentValue($contents, $path, $value)
{
$result = $contents;
$items = explode('.', $path);
$key = array_pop($items);
$replaceValue = $this->writeValueToPhp($value);

$count = 0;
$patterns = [];
$patterns[] = $this->buildStringExpression($key, $items);
$patterns[] = $this->buildStringExpression($key, $items, '"');
$patterns[] = $this->buildConstantExpression($key, $items);
$patterns[] = $this->buildArrayExpression($key, $items);

foreach ($patterns as $pattern) {
$result = preg_replace($pattern, '${1}${2}'.$replaceValue, $result, 1, $count);

if ($count > 0) {
break;
}
}

return $result;
}

protected function writeValueToPhp($value)
{
if (is_string($value) && strpos($value, "'") === false) {
$replaceValue = "'".$value."'";
}
elseif (is_string($value) && strpos($value, '"') === false) {
$replaceValue = '"'.$value.'"';
}
elseif (is_bool($value)) {
$replaceValue = ($value ? 'true' : 'false');
}
elseif (is_null($value)) {
$replaceValue = 'null';
}
elseif (is_array($value) && count($value) === count($value, COUNT_RECURSIVE)) {
$replaceValue = $this->writeArrayToPhp($value);
}
else {
$replaceValue = $value;
}

$replaceValue = str_replace('$', '\$', $replaceValue);

return $replaceValue;
}

protected function writeArrayToPhp($array)
public function toFile(string $filePath, array $newValues): string
{
$result = [];

foreach ($array as $value) {
if (!is_array($value)) {
$result[] = $this->writeValueToPhp($value);
}
}

return '['.implode(', ', $result).']';
$arrayFile = ArrayFile::open($filePath)->set($newValues);
$arrayFile->write();
return $arrayFile->render();
}

protected function buildStringExpression($targetKey, $arrayItems = [], $quoteChar = "'")
public function toContent(string $contents, $newValues): string
{
$expression = [];

// Opening expression for array items ($1)
$expression[] = $this->buildArrayOpeningExpression($arrayItems);

// The target key opening
$expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)['.$quoteChar.']';

// The target value to be replaced ($2)
$expression[] = '([^'.$quoteChar.']*)';

// The target key closure
$expression[] = '['.$quoteChar.']';

return '/' . implode('', $expression) . '/';
}
$lexer = new Emulative([
'usedAttributes' => [
'comments',
'startTokenPos',
'startLine',
'endTokenPos',
'endLine'
]
]);
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer);

/**
* Common constants only (true, false, null, integers)
*/
protected function buildConstantExpression($targetKey, $arrayItems = [])
{
$expression = [];

// Opening expression for array items ($1)
$expression[] = $this->buildArrayOpeningExpression($arrayItems);

// The target key opening ($2)
$expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)';

// The target value to be replaced ($3)
$expression[] = '([tT][rR][uU][eE]|[fF][aA][lL][sS][eE]|[nN][uU][lL]{2}|[\d]+)';

return '/' . implode('', $expression) . '/';
}

/**
* Single level arrays only
*/
protected function buildArrayExpression($targetKey, $arrayItems = [])
{
$expression = [];

// Opening expression for array items ($1)
$expression[] = $this->buildArrayOpeningExpression($arrayItems);

// The target key opening ($2)
$expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)';

// The target value to be replaced ($3)
$expression[] = '(?:[aA][rR]{2}[aA][yY]\(|[\[])([^\]|)]*)[\]|)]';

return '/' . implode('', $expression) . '/';
}

protected function buildArrayOpeningExpression($arrayItems)
{
if (count($arrayItems)) {
$itemOpen = [];
foreach ($arrayItems as $item) {
// The left hand array assignment
$itemOpen[] = '[\'|"]'.$item.'[\'|"]\s*=>\s*(?:[aA][rR]{2}[aA][yY]\(|[\[])';
}

// Capture all opening array (non greedy)
$result = '(' . implode('[\s\S]*', $itemOpen) . '[\s\S]*?)';
}
else {
// Gotta capture something for $1
$result = '()';
try {
$ast = $parser->parse($contents);
} catch (Error $e) {
throw new SystemException($e);
}

return $result;
return (new ArrayFile($ast, $lexer, null))->set($newValues)->render();
}
}
74 changes: 20 additions & 54 deletions src/Foundation/Console/KeyGenerateCommand.php
Original file line number Diff line number Diff line change
@@ -1,82 +1,48 @@
<?php namespace Winter\Storm\Foundation\Console;

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();

$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('<comment>'.$key.'</comment>');
}

// 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];
}
}
24 changes: 24 additions & 0 deletions src/Parse/Contracts/DataFileInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php namespace Winter\Storm\Parse\Contracts;

interface DataFileInterface
{
/**
* Return a new instance of `DataFileInterface` ready for modification of the provided filepath.
*/
public static function open(string $filePath): static;

/**
* Set a property within the data.
*/
public function set(string|array $key, $value = null): static;

/**
* Write the current data to a file
*/
public function write(?string $filePath = null): void;

/**
* Get the printed data
*/
public function render(): string;
}
Loading