From 07795ba561cc3209b29605f6e2b8610e2ea32368 Mon Sep 17 00:00:00 2001 From: Jaume Date: Tue, 23 Feb 2021 18:04:42 +0100 Subject: [PATCH 1/4] Added process of quotes --- README.md | 38 ++++++++++++++++++++------ src/DotEnv.php | 63 ++++++++++++++++++++++++++++--------------- src/Option.php | 32 ++++++++++++++++++++++ src/Processor.php | 32 ++++++++++++++++++++++ tests/DotenvTest.php | 45 ++++++++++++++++++++++++++++--- tests/env/.env.quotes | 7 +++++ 6 files changed, 184 insertions(+), 33 deletions(-) create mode 100644 src/Option.php create mode 100644 src/Processor.php create mode 100644 tests/env/.env.quotes diff --git a/README.md b/README.md index 41f2a16..559c8b9 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,44 @@ ``` APP_ENV=dev DATABASE_DNS=mysql:host=localhost;dbname=test; -DATABASE_USER=root +DATABASE_USER="root" DATABASE_PASSWORD=root +MODULE_ENABLED=true ``` -**How to use ?** +**Load the variables** ```php load(); +$absolutePathToEnvFile = __DIR__ . '/.env'; -echo getenv('APP_ENV'); -// dev -echo getenv('DATABASE_DNS'); -// mysql:host=localhost;dbname=test; +(new DotEnv($absolutePathToEnvFile))->load(); ``` + +**Use them!** +```php +/** + * string(33) "mysql:host=localhost;dbname=test;" + */ +var_dump(getenv('DATABASE_DNS')); + +/** + * Removes double and single quotes from the variable: + * + * string(4) "root" + */ +var_dump(getenv('DATABASE_USER')); + +/** + * Processes booleans as such: + * + * bool(true) + */ +var_dump(getenv('MODULE_ENABLED')); +``` + Ideal for small project -Simple and easy! + +Simple and easy! \ No newline at end of file diff --git a/src/DotEnv.php b/src/DotEnv.php index 8fae94c..9b0cffb 100644 --- a/src/DotEnv.php +++ b/src/DotEnv.php @@ -4,19 +4,6 @@ class DotEnv { - /** - * Convert true and false to booleans, instead of: - * - * VARIABLE=false -> ['VARIABLE' => 'false'] - * - * it will be - * - * VARIABLE=false -> ['VARIABLE' => false] - * - * default = true - */ - const PROCESS_BOOLEANS = 'PROCESS_BOOLEANS'; - /** * The directory where the .env file can be located. * @@ -31,6 +18,13 @@ class DotEnv */ protected $options = []; + /** + * Process strings to obtain characteristics of it + * + * @var Processor + */ + protected $processor; + public function __construct(string $path, array $options = []) { if (!file_exists($path)) { @@ -40,12 +34,15 @@ public function __construct(string $path, array $options = []) $this->path = $path; $this->processOptions($options); + + $this->processor = new Processor(); } private function processOptions(array $options) : void { $this->options = array_merge([ - static::PROCESS_BOOLEANS => true + Option::PROCESS_BOOLEANS => true, + Option::PROCESS_QUOTES => true ], $options); } @@ -78,16 +75,40 @@ public function load() : void } } - private function processValue(string $value) { - $trimmedValue = trim($value); - - if (!empty($this->options[static::PROCESS_BOOLEANS])) { - $loweredValue = strtolower($trimmedValue); + private function processValue(string $value) + { + /** + * First trim spaces and quotes if configured + */ + $preprocessedValue = $this->preprocessValue($value); - $isBoolean = in_array($loweredValue, ['true', 'false'], true); + /** + * If the value is a boolean resolve it as such + */ + if (!empty($this->options[Option::PROCESS_BOOLEANS])) { + $isBoolean = $this->processor->isBoolean($preprocessedValue); if ($isBoolean) { - return $loweredValue === 'true'; + return $this->processor->resolveAsBoolean($preprocessedValue); + } + } + + /** + * Does not match any processor options, return as is + */ + return $preprocessedValue; + } + + private function preprocessValue(string $value) : string + { + $trimmedValue = trim($value); + + if (!empty($this->options[Option::PROCESS_QUOTES])) { + $wrappedBySingleQuotes = $this->processor->isWrappedByChar($trimmedValue, '\''); + $wrappedByDoubleQuotes = $this->processor->isWrappedByChar($trimmedValue, '"'); + + if ($wrappedBySingleQuotes || $wrappedByDoubleQuotes) { + return $this->processor->removeFirstAndLastChar($trimmedValue); } } diff --git a/src/Option.php b/src/Option.php new file mode 100644 index 0000000..b54d1f0 --- /dev/null +++ b/src/Option.php @@ -0,0 +1,32 @@ + ['VARIABLE' => 'false'] + * + * it will be + * + * VARIABLE=false -> ['VARIABLE' => false] + * + * default = true + */ + const PROCESS_BOOLEANS = 'PROCESS_BOOLEANS'; + + /** + * Remove double and single quotes at the start and end of the variables, instead of: + * + * VARIABLE="This is a "sample" value" -> ['VARIABLE' => '"This is a "sample" value"'] + * + * it will be + * + * VARIABLE="This is a "sample" value" -> ['VARIABLE' => 'This is a "sample" value'] + * + * default = true + */ + const PROCESS_QUOTES = 'PROCESS_QUOTES'; +} \ No newline at end of file diff --git a/src/Processor.php b/src/Processor.php new file mode 100644 index 0000000..3ffe2c2 --- /dev/null +++ b/src/Processor.php @@ -0,0 +1,32 @@ +env('.env.boolean'), [ - DotEnv::PROCESS_BOOLEANS => true + Option::PROCESS_BOOLEANS => true ]))->load(); $this->assertEquals(false, $_ENV['FALSE1']); $this->assertEquals(false, $_ENV['FALSE2']); $this->assertEquals(false, $_ENV['FALSE3']); - $this->assertEquals("'false'", $_ENV['FALSE4']); + $this->assertEquals(false, $_ENV['FALSE4']); $this->assertEquals('0', $_ENV['FALSE5']); $this->assertEquals(true, $_ENV['TRUE1']); $this->assertEquals(true, $_ENV['TRUE2']); $this->assertEquals(true, $_ENV['TRUE3']); - $this->assertEquals("'true'", $_ENV['TRUE4']); + $this->assertEquals(true, $_ENV['TRUE4']); $this->assertEquals('1', $_ENV['TRUE5']); } @@ -72,11 +73,47 @@ public function testProcessBoolean() public function testDontProcessBoolean() { (new DotEnv($this->env('.env.boolean'), [ - DotEnv::PROCESS_BOOLEANS => false + Option::PROCESS_BOOLEANS => false ]))->load(); $this->assertEquals('false', $_ENV['FALSE1']); $this->assertEquals('true', $_ENV['TRUE1']); } + + /** + * @runInSeparateProcess + */ + public function testProcessQuotes() + { + (new DotEnv($this->env('.env.quotes'), [ + Option::PROCESS_QUOTES => true + ]))->load(); + + $this->assertEquals('q1', $_ENV['QUOTED1']); + $this->assertEquals('q2', $_ENV['QUOTED2']); + $this->assertEquals('"q3"', $_ENV['QUOTED3']); + $this->assertEquals('This is a "sample" value', $_ENV['QUOTED4']); + $this->assertEquals('\"This is a "sample" value\"', $_ENV['QUOTED5']); + $this->assertEquals('"q6', $_ENV['QUOTED6']); + $this->assertEquals('q7"', $_ENV['QUOTED7']); + } + + /** + * @runInSeparateProcess + */ + public function testDontProcessQuotes() + { + (new DotEnv($this->env('.env.quotes'), [ + Option::PROCESS_QUOTES => false + ]))->load(); + + $this->assertEquals('"q1"', $_ENV['QUOTED1']); + $this->assertEquals('\'q2\'', $_ENV['QUOTED2']); + $this->assertEquals('""q3""', $_ENV['QUOTED3']); + $this->assertEquals('"This is a "sample" value"', $_ENV['QUOTED4']); + $this->assertEquals('\"This is a "sample" value\"', $_ENV['QUOTED5']); + $this->assertEquals('"q6', $_ENV['QUOTED6']); + $this->assertEquals('q7"', $_ENV['QUOTED7']); + } } diff --git a/tests/env/.env.quotes b/tests/env/.env.quotes new file mode 100644 index 0000000..520da00 --- /dev/null +++ b/tests/env/.env.quotes @@ -0,0 +1,7 @@ +QUOTED1="q1" +QUOTED2='q2' +QUOTED3=""q3"" +QUOTED4="This is a "sample" value" +QUOTED5=\"This is a "sample" value\" +QUOTED6="q6 +QUOTED7=q7" From b03ddf857aab5c36beab76238a0f2819c82b6986 Mon Sep 17 00:00:00 2001 From: Jaume Date: Sun, 28 Mar 2021 12:29:37 +0200 Subject: [PATCH 2/4] Branch refactor --- src/DotEnv.php | 82 ++++++++++++++--------------- src/Processor.php | 32 ----------- src/Processor/AbstractProcessor.php | 16 ++++++ src/Processor/BooleanProcessor.php | 17 ++++++ src/Processor/IProcessor.php | 11 ++++ src/Processor/QuotedProcessor.php | 31 +++++++++++ tests/DotenvTest.php | 24 +++++---- tests/env/.env.default | 4 +- 8 files changed, 132 insertions(+), 85 deletions(-) delete mode 100644 src/Processor.php create mode 100644 src/Processor/AbstractProcessor.php create mode 100644 src/Processor/BooleanProcessor.php create mode 100644 src/Processor/IProcessor.php create mode 100644 src/Processor/QuotedProcessor.php diff --git a/src/DotEnv.php b/src/DotEnv.php index 9b0cffb..91546b6 100644 --- a/src/DotEnv.php +++ b/src/DotEnv.php @@ -2,6 +2,10 @@ namespace DevCoder; +use DevCoder\Processor\AbstractProcessor; +use DevCoder\Processor\BooleanProcessor; +use DevCoder\Processor\QuotedProcessor; + class DotEnv { /** @@ -14,18 +18,11 @@ class DotEnv /** * Configure the options on which the parsed will act * - * @var array - */ - protected $options = []; - - /** - * Process strings to obtain characteristics of it - * - * @var Processor + * @var string[] */ - protected $processor; + protected $processors = []; - public function __construct(string $path, array $options = []) + public function __construct(string $path, array $processors = null) { if (!file_exists($path)) { throw new \InvalidArgumentException(sprintf('%s does not exist', $path)); @@ -33,17 +30,30 @@ public function __construct(string $path, array $options = []) $this->path = $path; - $this->processOptions($options); - - $this->processor = new Processor(); + $this->setProcessors($processors); } - private function processOptions(array $options) : void + private function setProcessors(array $processors = null) : DotEnv { - $this->options = array_merge([ - Option::PROCESS_BOOLEANS => true, - Option::PROCESS_QUOTES => true - ], $options); + /** + * Fill with default processors + */ + if ($processors === null){ + $this->processors = [ + BooleanProcessor::class, + QuotedProcessor::class + ]; + + return $this; + } + + foreach ($processors as $processor) { + if (is_subclass_of($processor, AbstractProcessor::class)) { + $this->processors[] = $processor; + } + } + + return $this; } /** @@ -75,43 +85,31 @@ public function load() : void } } + /** + * Process the value with the configured processors + * + * @param string $value The value to process + * @return string|bool + */ private function processValue(string $value) { /** * First trim spaces and quotes if configured */ - $preprocessedValue = $this->preprocessValue($value); + $trimmedValue = trim($value); - /** - * If the value is a boolean resolve it as such - */ - if (!empty($this->options[Option::PROCESS_BOOLEANS])) { - $isBoolean = $this->processor->isBoolean($preprocessedValue); + foreach ($this->processors as $processor) { + /** @var AbstractProcessor $processorInstance */ + $processorInstance = new $processor($trimmedValue); - if ($isBoolean) { - return $this->processor->resolveAsBoolean($preprocessedValue); + if ($processorInstance->canBeProcessed()) { + return $processorInstance->execute(); } } /** * Does not match any processor options, return as is */ - return $preprocessedValue; - } - - private function preprocessValue(string $value) : string - { - $trimmedValue = trim($value); - - if (!empty($this->options[Option::PROCESS_QUOTES])) { - $wrappedBySingleQuotes = $this->processor->isWrappedByChar($trimmedValue, '\''); - $wrappedByDoubleQuotes = $this->processor->isWrappedByChar($trimmedValue, '"'); - - if ($wrappedBySingleQuotes || $wrappedByDoubleQuotes) { - return $this->processor->removeFirstAndLastChar($trimmedValue); - } - } - return $trimmedValue; } } diff --git a/src/Processor.php b/src/Processor.php deleted file mode 100644 index 3ffe2c2..0000000 --- a/src/Processor.php +++ /dev/null @@ -1,32 +0,0 @@ -value = $value; + } +} \ No newline at end of file diff --git a/src/Processor/BooleanProcessor.php b/src/Processor/BooleanProcessor.php new file mode 100644 index 0000000..3a38803 --- /dev/null +++ b/src/Processor/BooleanProcessor.php @@ -0,0 +1,17 @@ +value); + + return in_array($loweredValue, ['true', 'false'], true); + } + + public function execute() + { + return strtolower($this->value) === 'true'; + } +} \ No newline at end of file diff --git a/src/Processor/IProcessor.php b/src/Processor/IProcessor.php new file mode 100644 index 0000000..eac7e7d --- /dev/null +++ b/src/Processor/IProcessor.php @@ -0,0 +1,11 @@ +isWrappedByChar($this->value, '"'); + + if ($wrappedByDoubleQuotes) { + return true; + } + + return $this->isWrappedByChar($this->value, '\''); + } + + public function execute() + { + /** + * Since this function is used for the quote removal + * we don't need mb_substr + */ + return substr($this->value, 1, -1); + } + + public function isWrappedByChar(string $value, string $char) : bool + { + return $value[0] === $char && $value[-1] === $char; + } +} \ No newline at end of file diff --git a/tests/DotenvTest.php b/tests/DotenvTest.php index 7e3c9e5..164aedd 100644 --- a/tests/DotenvTest.php +++ b/tests/DotenvTest.php @@ -4,6 +4,8 @@ use DevCoder\DotEnv; use DevCoder\Option; +use DevCoder\Processor\BooleanProcessor; +use DevCoder\Processor\QuotedProcessor; use PHPUnit\Framework\TestCase; class DotenvTest extends TestCase @@ -24,6 +26,8 @@ public function testLoad() { $this->assertEquals('password', getenv('DATABASE_PASSWORD')); $this->assertFalse(getenv('GOOGLE_API')); $this->assertFalse(getenv('GOOGLE_MANAGER_KEY')); + $this->assertEquals(true, getenv('BOOLEAN_LITERAL')); + $this->assertEquals('true', getenv('BOOLEAN_QUOTED')); $this->assertEquals('dev', $_ENV['APP_ENV']); $this->assertEquals('mysql:host=localhost;dbname=test;', $_ENV['DATABASE_DNS']); @@ -31,6 +35,8 @@ public function testLoad() { $this->assertEquals('password', $_ENV['DATABASE_PASSWORD']); $this->assertFalse(array_key_exists('GOOGLE_API', $_ENV)); $this->assertFalse(array_key_exists('GOOGLE_MANAGER_KEY', $_ENV)); + $this->assertEquals(true, $_ENV['BOOLEAN_LITERAL']); + $this->assertEquals('true', $_ENV['BOOLEAN_QUOTED']); $this->assertEquals('dev', $_SERVER['APP_ENV']); $this->assertEquals('mysql:host=localhost;dbname=test;', $_SERVER['DATABASE_DNS']); @@ -38,6 +44,8 @@ public function testLoad() { $this->assertEquals('password', $_SERVER['DATABASE_PASSWORD']); $this->assertFalse(array_key_exists('GOOGLE_API', $_SERVER)); $this->assertFalse(array_key_exists('GOOGLE_MANAGER_KEY', $_SERVER)); + $this->assertEquals(true, $_SERVER['BOOLEAN_LITERAL']); + $this->assertEquals('true', $_SERVER['BOOLEAN_QUOTED']); } public function testFileNotExist() { @@ -51,19 +59,19 @@ public function testFileNotExist() { public function testProcessBoolean() { (new DotEnv($this->env('.env.boolean'), [ - Option::PROCESS_BOOLEANS => true + BooleanProcessor::class ]))->load(); $this->assertEquals(false, $_ENV['FALSE1']); $this->assertEquals(false, $_ENV['FALSE2']); $this->assertEquals(false, $_ENV['FALSE3']); - $this->assertEquals(false, $_ENV['FALSE4']); + $this->assertEquals("'false'", $_ENV['FALSE4']); // Since we don't have the QuotedProcessor::class this will be the result $this->assertEquals('0', $_ENV['FALSE5']); $this->assertEquals(true, $_ENV['TRUE1']); $this->assertEquals(true, $_ENV['TRUE2']); $this->assertEquals(true, $_ENV['TRUE3']); - $this->assertEquals(true, $_ENV['TRUE4']); + $this->assertEquals("'true'", $_ENV['TRUE4']); // Since we don't have the QuotedProcessor::class this will be the result $this->assertEquals('1', $_ENV['TRUE5']); } @@ -72,9 +80,7 @@ public function testProcessBoolean() */ public function testDontProcessBoolean() { - (new DotEnv($this->env('.env.boolean'), [ - Option::PROCESS_BOOLEANS => false - ]))->load(); + (new DotEnv($this->env('.env.boolean'), []))->load(); $this->assertEquals('false', $_ENV['FALSE1']); @@ -87,7 +93,7 @@ public function testDontProcessBoolean() public function testProcessQuotes() { (new DotEnv($this->env('.env.quotes'), [ - Option::PROCESS_QUOTES => true + QuotedProcessor::class ]))->load(); $this->assertEquals('q1', $_ENV['QUOTED1']); @@ -104,9 +110,7 @@ public function testProcessQuotes() */ public function testDontProcessQuotes() { - (new DotEnv($this->env('.env.quotes'), [ - Option::PROCESS_QUOTES => false - ]))->load(); + (new DotEnv($this->env('.env.quotes'), []))->load(); $this->assertEquals('"q1"', $_ENV['QUOTED1']); $this->assertEquals('\'q2\'', $_ENV['QUOTED2']); diff --git a/tests/env/.env.default b/tests/env/.env.default index ada527e..7025890 100644 --- a/tests/env/.env.default +++ b/tests/env/.env.default @@ -4,4 +4,6 @@ DATABASE_DNS=mysql:host=localhost;dbname=test; DATABASE_USER=root DATABASE_PASSWORD = password #GOOGLE_API=DJfa7czhKaJ0Pig6j9XpSjT6NpXZUZwK - # GOOGLE_MANAGER_KEY=P7RkBUQHIkPUEPy3yCTT4gGYa2DjRth8 \ No newline at end of file + # GOOGLE_MANAGER_KEY=P7RkBUQHIkPUEPy3yCTT4gGYa2DjRth8 +BOOLEAN_LITERAL=true +BOOLEAN_QUOTED="true" \ No newline at end of file From 8305ce9cd4cfbb32f519efbc84e31d6b2b20e64b Mon Sep 17 00:00:00 2001 From: Jaume Date: Sun, 28 Mar 2021 12:35:45 +0200 Subject: [PATCH 3/4] Remove unused file and readme refactor --- README.md | 20 +++++++++++++++++--- src/Option.php | 32 -------------------------------- 2 files changed, 17 insertions(+), 35 deletions(-) delete mode 100644 src/Option.php diff --git a/README.md b/README.md index 559c8b9..b1aecf2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ DATABASE_PASSWORD=root MODULE_ENABLED=true ``` -**Load the variables** +## Load the variables ```php load(); ``` -**Use them!** +# Use them! ```php /** * string(33) "mysql:host=localhost;dbname=test;" @@ -44,4 +44,18 @@ var_dump(getenv('MODULE_ENABLED')); Ideal for small project -Simple and easy! \ No newline at end of file +Simple and easy! + +# Processors + +Also the variables are parsed according to the configuration passed as parameter to the constructor. The available processors are: + +## BooleanProcessor + +``VARIABLE=false`` will be processed to ```bool(false)``` + +NOTE: ``VARIABLE="true"`` will be processed to ```string(4) "true"``` + +## QuotedProcessor + +``VARIABLE="anything"`` will be processed to ```string(8) "anything"``` \ No newline at end of file diff --git a/src/Option.php b/src/Option.php deleted file mode 100644 index b54d1f0..0000000 --- a/src/Option.php +++ /dev/null @@ -1,32 +0,0 @@ - ['VARIABLE' => 'false'] - * - * it will be - * - * VARIABLE=false -> ['VARIABLE' => false] - * - * default = true - */ - const PROCESS_BOOLEANS = 'PROCESS_BOOLEANS'; - - /** - * Remove double and single quotes at the start and end of the variables, instead of: - * - * VARIABLE="This is a "sample" value" -> ['VARIABLE' => '"This is a "sample" value"'] - * - * it will be - * - * VARIABLE="This is a "sample" value" -> ['VARIABLE' => 'This is a "sample" value'] - * - * default = true - */ - const PROCESS_QUOTES = 'PROCESS_QUOTES'; -} \ No newline at end of file From 1c906db6b8fc0fac71347fc57a966ae8f0dfab66 Mon Sep 17 00:00:00 2001 From: jaume Date: Thu, 17 Jun 2021 18:56:31 +0200 Subject: [PATCH 4/4] Add test and refactor --- src/DotEnv.php | 2 +- src/Processor/QuotedProcessor.php | 2 +- tests/DotenvTest.php | 47 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/DotEnv.php b/src/DotEnv.php index 91546b6..a429c90 100644 --- a/src/DotEnv.php +++ b/src/DotEnv.php @@ -38,7 +38,7 @@ private function setProcessors(array $processors = null) : DotEnv /** * Fill with default processors */ - if ($processors === null){ + if ($processors === null) { $this->processors = [ BooleanProcessor::class, QuotedProcessor::class diff --git a/src/Processor/QuotedProcessor.php b/src/Processor/QuotedProcessor.php index 861ee9e..2e5cc40 100644 --- a/src/Processor/QuotedProcessor.php +++ b/src/Processor/QuotedProcessor.php @@ -24,7 +24,7 @@ public function execute() return substr($this->value, 1, -1); } - public function isWrappedByChar(string $value, string $char) : bool + private function isWrappedByChar(string $value, string $char) : bool { return $value[0] === $char && $value[-1] === $char; } diff --git a/tests/DotenvTest.php b/tests/DotenvTest.php index 164aedd..3429532 100644 --- a/tests/DotenvTest.php +++ b/tests/DotenvTest.php @@ -6,6 +6,7 @@ use DevCoder\Option; use DevCoder\Processor\BooleanProcessor; use DevCoder\Processor\QuotedProcessor; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\TestCase; class DotenvTest extends TestCase @@ -53,6 +54,52 @@ public function testFileNotExist() { (new DotEnv($this->env('.env.not-exists')))->load(); } + public function testUncompatibleProcessors() { + $this->assertProcessors( + [], + [] + ); + + $this->assertProcessors( + null, + [BooleanProcessor::class, QuotedProcessor::class] + ); + + $this->assertProcessors( + [null], + [] + ); + + $this->assertProcessors( + [new \stdClass()], + [] + ); + + $this->assertProcessors( + [QuotedProcessor::class, null], + [QuotedProcessor::class] + ); + } + + private function assertProcessors(array $processorsToUse = null, array $expectedProcessors = []) + { + $dotEnv = new DotEnv($this->env('.env.default'), $processorsToUse); + $dotEnv->load(); + + $this->assertEquals( + $expectedProcessors, + $this->getProtectedProperty($dotEnv, 'processors') + ); + } + + private function getProtectedProperty(object $object, string $property) { + $reflection = new \ReflectionClass($object); + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + + return $reflectionProperty->getValue($object); + } + /** * @runInSeparateProcess */