diff --git a/.gitignore b/.gitignore index 643a2ac..fa99288 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor composer.lock **/.env **/.target +.phpunit.cache diff --git a/composer.json b/composer.json index df2c929..aecd041 100644 --- a/composer.json +++ b/composer.json @@ -26,10 +26,10 @@ "exclude": ["/test/", "/test/*", "/phpstan.neon", "/psalm.xml", "/.phan/", "/.vscode/", "/phpunit.xml"] }, "require-dev": { - "phpunit/phpunit": "^9", "phan/phan": "^5.4", - "phpstan/phpstan": "^2.0", "phpstan/phpdoc-parser": "^2.0", - "phpstan/phpstan-deprecation-rules": "^2.0" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan": "2.1.x-dev", + "phpunit/phpunit": "^12" } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..cfc7a3d --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,18 @@ + + + PSR12 override rules (strict, standard). Switch spaces indent to tab. + + + + + + + + + + + + + + + diff --git a/phpunit.xml b/phpunit.xml index b6ad070..baaf75a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,8 @@ - - - - test/phpUnitTests/ - - + + + + + test/phpUnitTests/ + + diff --git a/src/DotEnv.php b/src/DotEnv.php index 671bbec..a4b8202 100644 --- a/src/DotEnv.php +++ b/src/DotEnv.php @@ -6,115 +6,115 @@ namespace gullevek\dotEnv; class DotEnv { - /** @var string constant comment char, set to # */ - private const COMMENT_CHAR = '#'; + /** @var string constant comment char, set to # */ + private const COMMENT_CHAR = '#'; - /** - * parses .env file - * - * Rules for .env file - * variable is any alphanumeric string followed by = on the same line - * content starts with the first non space part - * strings can be contained in " - * strings MUST be contained in " if they are multiline - * if string starts with " it will match until another " is found - * anything AFTER " is ignored - * if there are two variables with the same name only the first is used - * variables are case sensitive - * - * [] Grouping Block Name as prefix until next or end if set, - * space replaced by _, all other var rules apply - * - * @param string $path Folder to file, default is __DIR__ - * @param string $env_file What file to load, default is .env - * @return int -1 other error - * 0 for success full load - * 1 for file loadable, no data or data already loaded - * 2 for file not readable or open failed - * 3 for file not found - */ - public static function readEnvFile( - string $path = __DIR__, - string $env_file = '.env' - ): int { - // default -1; - $status = -1; - $env_file_target = $path . DIRECTORY_SEPARATOR . $env_file; - // this is not a file -> abort - if (!is_file($env_file_target)) { - $status = 3; - return $status; - } - // cannot open file -> abort - if (!is_readable($env_file_target)) { - $status = 2; - return $status; - } - // open file - if (($fp = fopen($env_file_target, 'r')) === false) { - $status = 2; - return $status; - } - // set to readable but not yet any data loaded - $status = 1; - $block = false; - $var = ''; - $prefix_name = ''; - while (($line = fgets($fp)) !== false) { - // [] block must be a single line, or it will be ignored - if (preg_match("/^\s*\[([\w_.\s]+)\]/", $line, $matches)) { - $prefix_name = preg_replace("/\s+/", "_", $matches[1]) . "."; - } elseif (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) { - // main match for variable = value part - $var = $prefix_name . $matches[1]; - $value = $matches[2]; - $quotes = $matches[3]; - // write only if env is not set yet, and write only the first time - if (empty($_ENV[$var])) { - if (!empty($quotes)) { - // match greedy for first to last so we move any " if there are - if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) { - $value = $matches[1]; - } else { - // this is a multi line - $block = true; - // first " in string remove - // add removed new line back because this is a multi line - $value = ltrim($value, '"') . PHP_EOL; - } - } else { - // strip any quotes at end for unquoted single line - // an right hand spaces are removed too - $value = false !== ($pos = strpos($value, self::COMMENT_CHAR)) ? - rtrim(substr($value, 0, $pos)) : $value; - } - // if block is set, we strip line of slashes - $_ENV[$var] = $block === true ? stripslashes($value) : $value; - // set successful load - $status = 0; - } - } elseif ($block === true) { - // read line until there is a unescaped " - // this also strips everything after the last " - if (preg_match("/(.*[^\\\])\"/", $line, $matches)) { - $block = false; - // strip ending " and EVERYTHING that follows after that - $line = $matches[1]; - } - // just be sure it is init before we fill - if (!isset($_ENV[$var])) { - $_ENV[$var] = ''; - } elseif (!is_string($_ENV[$var])) { - // if this is not string, skip - continue; - } - // strip line of slashes - $_ENV[$var] .= stripslashes($line); - } - } - fclose($fp); - return $status; - } + /** + * parses .env file + * + * Rules for .env file + * variable is any alphanumeric string followed by = on the same line + * content starts with the first non space part + * strings can be contained in " + * strings MUST be contained in " if they are multiline + * if string starts with " it will match until another " is found + * anything AFTER " is ignored + * if there are two variables with the same name only the first is used + * variables are case sensitive + * + * [] Grouping Block Name as prefix until next or end if set, + * space replaced by _, all other var rules apply + * + * @param string $path Folder to file, default is __DIR__ + * @param string $env_file What file to load, default is .env + * @return int -1 other error + * 0 for success full load + * 1 for file loadable, no data or data already loaded + * 2 for file not readable or open failed + * 3 for file not found + */ + public static function readEnvFile( + string $path = __DIR__, + string $env_file = '.env' + ): int { + // default -1; + $status = -1; + $env_file_target = $path . DIRECTORY_SEPARATOR . $env_file; + // this is not a file -> abort + if (!is_file($env_file_target)) { + $status = 3; + return $status; + } + // cannot open file -> abort + if (!is_readable($env_file_target)) { + $status = 2; + return $status; + } + // open file + if (($fp = fopen($env_file_target, 'r')) === false) { + $status = 2; + return $status; + } + // set to readable but not yet any data loaded + $status = 1; + $block = false; + $var = ''; + $prefix_name = ''; + while (($line = fgets($fp)) !== false) { + // [] block must be a single line, or it will be ignored + if (preg_match("/^\s*\[([\w_.\s]+)\]/", $line, $matches)) { + $prefix_name = preg_replace("/\s+/", "_", $matches[1]) . "."; + } elseif (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) { + // main match for variable = value part + $var = $prefix_name . $matches[1]; + $value = $matches[2]; + $quotes = $matches[3]; + // write only if env is not set yet, and write only the first time + if (empty($_ENV[$var])) { + if (!empty($quotes)) { + // match greedy for first to last so we move any " if there are + if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) { + $value = $matches[1]; + } else { + // this is a multi line + $block = true; + // first " in string remove + // add removed new line back because this is a multi line + $value = ltrim($value, '"') . PHP_EOL; + } + } else { + // strip any quotes at end for unquoted single line + // an right hand spaces are removed too + $value = false !== ($pos = strpos($value, self::COMMENT_CHAR)) ? + rtrim(substr($value, 0, $pos)) : $value; + } + // if block is set, we strip line of slashes + $_ENV[$var] = $block === true ? stripslashes($value) : $value; + // set successful load + $status = 0; + } + } elseif ($block === true) { + // read line until there is a unescaped " + // this also strips everything after the last " + if (preg_match("/(.*[^\\\])\"/", $line, $matches)) { + $block = false; + // strip ending " and EVERYTHING that follows after that + $line = $matches[1]; + } + // just be sure it is init before we fill + if (!isset($_ENV[$var])) { + $_ENV[$var] = ''; + } elseif (!is_string($_ENV[$var])) { + // if this is not string, skip + continue; + } + // strip line of slashes + $_ENV[$var] .= stripslashes($line); + } + } + fclose($fp); + return $status; + } } // __END__ diff --git a/test/phpUnitTests/DotEnvTest.php b/test/phpUnitTests/DotEnvTest.php index ecb01df..41de7d3 100644 --- a/test/phpUnitTests/DotEnvTest.php +++ b/test/phpUnitTests/DotEnvTest.php @@ -5,209 +5,214 @@ declare(strict_types=1); namespace tests; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; /** * Test class for DotEnv - * @coversDefaultClass \gullevek\DotEnv - * @testdox \gullevek\DotEnv method tests */ +#[TestDox("\gullevek\DotEnv method tests")] +#[CoversClass(\gullevek\dotEnv\DotEnv::class)] +#[CoversMethod(\gullevek\dotEnv\DotEnv::class, 'readEnvFile')] final class DotEnvTest extends TestCase { - /** - * setup the .env files before test run - * - * @return void - */ - public static function setUpBeforeClass(): void - { - // create .env files - $file_content = __DIR__ . DIRECTORY_SEPARATOR - . 'dotenv' . DIRECTORY_SEPARATOR - . 'test.env'; - // copy to all folder levels - $env_files = [ - __DIR__ . DIRECTORY_SEPARATOR - . 'dotenv' . DIRECTORY_SEPARATOR - . '.env', - __DIR__ . DIRECTORY_SEPARATOR - . '.env', - __DIR__ . DIRECTORY_SEPARATOR - . '..' . DIRECTORY_SEPARATOR - . '.env', - ]; - // if not found, skip -> all will fail - if (is_file($file_content)) { - foreach ($env_files as $env_file) { - copy($file_content, $env_file); - } - } - } + /** + * setup the .env files before test run + * + * @return void + */ + public static function setUpBeforeClass(): void + { + // create .env files + $file_content = __DIR__ . DIRECTORY_SEPARATOR + . 'dotenv' . DIRECTORY_SEPARATOR + . 'test.env'; + // copy to all folder levels + $env_files = [ + __DIR__ . DIRECTORY_SEPARATOR + . 'dotenv' . DIRECTORY_SEPARATOR + . '.env', + __DIR__ . DIRECTORY_SEPARATOR + . '.env', + __DIR__ . DIRECTORY_SEPARATOR + . '..' . DIRECTORY_SEPARATOR + . '.env', + ]; + // if not found, skip -> all will fail + if (is_file($file_content)) { + foreach ($env_files as $env_file) { + copy($file_content, $env_file); + } + } + } - /** - * Undocumented function - * - * @return array - */ - public function envFileProvider(): array - { - $dot_env_content = [ - 'SOMETHING' => 'A', - 'OTHER' => 'B IS B', - 'Complex' => 'A B \"D is F', - 'HAS_SPACE' => 'ABC', - 'HAS_COMMENT_QUOTES_SPACE' => 'Comment at end with quotes and space', - 'HAS_COMMENT_QUOTES_NO_SPACE' => 'Comment at end with quotes no space', - 'HAS_COMMENT_NO_QUOTES_SPACE' => 'Comment at end no quotes and space', - 'HAS_COMMENT_NO_QUOTES_NO_SPACE' => 'Comment at end no quotes no space', - 'COMMENT_IN_TEXT_QUOTES' => 'Foo bar # comment in here', - 'HAS_EQUAL_NO_QUITES' => 'Is This = Valid', - 'HAS_EQUAL_QUITES' => 'Is This = Valid', - 'FAILURE' => 'ABC', - 'SIMPLEBOX' => 'A B C', - 'TITLE' => '1', - 'FOO' => '1.2', - 'SOME.TEST' => 'Test Var', - 'SOME.LIVE' => 'Live Var', - 'A_TEST1' => 'foo', - 'A_TEST2' => '${TEST1:-bar}', - 'A_TEST3' => '${TEST4:-bar}', - 'A_TEST5' => 'null', - 'A_TEST6' => '${TEST5-bar}', - 'A_TEST7' => '${TEST6:-bar}', - 'B_TEST1' => 'foo', - 'B_TEST2' => '${TEST1:=bar}', - 'B_TEST3' => '${TEST4:=bar}', - 'B_TEST5' => 'null', - 'B_TEST6' => '${TEST5=bar}', - 'B_TEST7' => '${TEST6=bar}', - 'Test' => 'A', - 'TEST' => 'B', - 'LINE' => "ABC\nDEF", - 'OTHERLINE' => "ABC\nAF\"ASFASDF\nMORESHIT", - 'SUPERLINE' => '', - '__FOO_BAR_1' => 'b', - '__FOOFOO' => 'f ', - 123123 => 'number', - 'EMPTY' => '', - 'Var_Test.TEST' => 'Block 1 D', - 'OtherSet.TEST' => 'Block 2 D', - ]; - // 0: folder relative to test folder, if unset __DIR__ - // 1: file, if unset .env - // 2: status to be returned - // 3: _ENV file content to be set - // 4: override chmod as octect in string - return [ - 'default' => [ - 'folder' => null, - 'file' => null, - 'status' => 3, - 'content' => [], - 'chmod' => null, - ], - 'cannot open file' => [ - 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', - 'file' => 'cannot_read.env', - 'status' => 2, - 'content' => [], - // 0000 - 'chmod' => '100000', - ], - 'empty file' => [ - 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', - 'file' => 'empty.env', - 'status' => 1, - 'content' => [], - // 0664 - 'chmod' => '100664', - ], - 'override all' => [ - 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', - 'file' => 'test.env', - 'status' => 0, - 'content' => $dot_env_content, - // 0664 - 'chmod' => '100664', - ], - 'override directory' => [ - 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', - 'file' => null, - 'status' => 0, - 'content' => $dot_env_content, - 'chmod' => null, - ], - ]; - } + /** + * Undocumented function + * + * @return array + */ + public static function envFileProvider(): array + { + $dot_env_content = [ + 'SOMETHING' => 'A', + 'OTHER' => 'B IS B', + 'Complex' => 'A B \"D is F', + 'HAS_SPACE' => 'ABC', + 'HAS_COMMENT_QUOTES_SPACE' => 'Comment at end with quotes and space', + 'HAS_COMMENT_QUOTES_NO_SPACE' => 'Comment at end with quotes no space', + 'HAS_COMMENT_NO_QUOTES_SPACE' => 'Comment at end no quotes and space', + 'HAS_COMMENT_NO_QUOTES_NO_SPACE' => 'Comment at end no quotes no space', + 'COMMENT_IN_TEXT_QUOTES' => 'Foo bar # comment in here', + 'HAS_EQUAL_NO_QUITES' => 'Is This = Valid', + 'HAS_EQUAL_QUITES' => 'Is This = Valid', + 'FAILURE' => 'ABC', + 'SIMPLEBOX' => 'A B C', + 'TITLE' => '1', + 'FOO' => '1.2', + 'SOME.TEST' => 'Test Var', + 'SOME.LIVE' => 'Live Var', + 'A_TEST1' => 'foo', + 'A_TEST2' => '${TEST1:-bar}', + 'A_TEST3' => '${TEST4:-bar}', + 'A_TEST5' => 'null', + 'A_TEST6' => '${TEST5-bar}', + 'A_TEST7' => '${TEST6:-bar}', + 'B_TEST1' => 'foo', + 'B_TEST2' => '${TEST1:=bar}', + 'B_TEST3' => '${TEST4:=bar}', + 'B_TEST5' => 'null', + 'B_TEST6' => '${TEST5=bar}', + 'B_TEST7' => '${TEST6=bar}', + 'Test' => 'A', + 'TEST' => 'B', + 'LINE' => "ABC\nDEF", + 'OTHERLINE' => "ABC\nAF\"ASFASDF\nMORESHIT", + 'SUPERLINE' => '', + '__FOO_BAR_1' => 'b', + '__FOOFOO' => 'f ', + 123123 => 'number', + 'EMPTY' => '', + 'Var_Test.TEST' => 'Block 1 D', + 'OtherSet.TEST' => 'Block 2 D', + ]; + // 0: folder relative to test folder, if unset __DIR__ + // 1: file, if unset .env + // 2: status to be returned + // 3: _ENV file content to be set + // 4: override chmod as octect in string + return [ + 'default' => [ + 'folder' => null, + 'file' => null, + 'expected_status' => 3, + 'expected_env' => [], + 'chmod' => null, + ], + 'cannot open file' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'cannot_read.env', + 'expected_status' => 2, + 'expected_env' => [], + // 0000 + 'chmod' => '100000', + ], + 'empty file' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'empty.env', + 'expected_status' => 1, + 'expected_env' => [], + // 0664 + 'chmod' => '100664', + ], + 'override all' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => 'test.env', + 'expected_status' => 0, + 'expected_env' => $dot_env_content, + // 0664 + 'chmod' => '100664', + ], + 'override directory' => [ + 'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv', + 'file' => null, + 'expected_status' => 0, + 'expected_env' => $dot_env_content, + 'chmod' => null, + ], + ]; + } - /** - * test read .env file - * - * @covers ::readEnvFile - * @dataProvider envFileProvider - * @testdox Read _ENV file from $folder / $file with expected status: $expected_status [$_dataName] - * - * @param string|null $folder - * @param string|null $file - * @param int $expected_status - * @param array $expected_env - * @param string|null $chmod - * @return void - */ - public function testReadEnvFile( - ?string $folder, - ?string $file, - int $expected_status, - array $expected_env, - ?string $chmod - ): void { - // skip if chmod is set to 10000 (000 no rights) if we are root - // as root there is no stop reading a file - if ( - !empty($chmod) && - $chmod == '100000' && - getmyuid() == 0 - ) { - $this->markTestSkipped( - "Skip cannot read file test because run user is root" - ); - return; - } - // reset $_ENV for clean compare - $_ENV = []; - // previous file perm - $old_chmod = null; - // if we have change permission for file - if ( - is_file($folder . DIRECTORY_SEPARATOR . $file) && - !empty($chmod) - ) { - // get the old permissions - $old_chmod = fileperms($folder . DIRECTORY_SEPARATOR . $file); - chmod($folder . DIRECTORY_SEPARATOR . $file, octdec($chmod)); - } - if ($folder !== null && $file !== null) { - $status = \gullevek\dotEnv\DotEnv::readEnvFile($folder, $file); - } elseif ($folder !== null) { - $status = \gullevek\dotEnv\DotEnv::readEnvFile($folder); - } else { - $status = \gullevek\dotEnv\DotEnv::readEnvFile(); - } - $this->assertEquals( - $status, - $expected_status, - 'Assert returned status equal' - ); - // now assert read data - $this->assertEquals( - $_ENV, - $expected_env, - 'Assert _ENV correct' - ); - // if we have file and chmod unset - if ($old_chmod !== null) { - chmod($folder . DIRECTORY_SEPARATOR . $file, $old_chmod); - } - } + /** + * test read .env file + * + * @param string|null $folder + * @param string|null $file + * @param int $expected_status + * @param array $expected_env + * @param string|null $chmod + * @return void + */ + #[Test] + #[TestDox('Read _ENV file from $folder / $file with expected status: $expected_status [$_dataName]')] + #[DataProvider('envFileProvider')] + public function testReadEnvFile( + ?string $folder, + ?string $file, + int $expected_status, + array $expected_env, + ?string $chmod + ): void { + // skip if chmod is set to 10000 (000 no rights) if we are root + // as root there is no stop reading a file + if ( + !empty($chmod) && + $chmod == '100000' && + getmyuid() == 0 + ) { + $this->markTestSkipped( + "Skip cannot read file test because run user is root" + ); + return; + } + // reset $_ENV for clean compare + $_ENV = []; + // previous file perm + $old_chmod = null; + // if we have change permission for file + if ( + is_file($folder . DIRECTORY_SEPARATOR . $file) && + !empty($chmod) + ) { + // get the old permissions + $old_chmod = fileperms($folder . DIRECTORY_SEPARATOR . $file); + chmod($folder . DIRECTORY_SEPARATOR . $file, octdec($chmod)); + } + if ($folder !== null && $file !== null) { + $status = \gullevek\dotEnv\DotEnv::readEnvFile($folder, $file); + } elseif ($folder !== null) { + $status = \gullevek\dotEnv\DotEnv::readEnvFile($folder); + } else { + $status = \gullevek\dotEnv\DotEnv::readEnvFile(); + } + $this->assertEquals( + $expected_status, + $status, + 'Assert returned status equal' + ); + // now assert read data + $this->assertEquals( + $expected_env, + $_ENV, + 'Assert _ENV correct' + ); + // if we have file and chmod unset + if ($old_chmod !== null) { + chmod($folder . DIRECTORY_SEPARATOR . $file, $old_chmod); + } + } } // __END__ diff --git a/test/read_env_file.php b/test/read_env_file.php index 58b0f15..59ab284 100644 --- a/test/read_env_file.php +++ b/test/read_env_file.php @@ -8,18 +8,18 @@ use gullevek\dotEnv\DotEnv; // copy test file to .env file in env folder $file_content = __DIR__ . DIRECTORY_SEPARATOR - . 'phpUnitTests' . DIRECTORY_SEPARATOR - . 'dotenv' . DIRECTORY_SEPARATOR - . 'test.env'; + . 'phpUnitTests' . DIRECTORY_SEPARATOR + . 'dotenv' . DIRECTORY_SEPARATOR + . 'test.env'; // env folder $env_file = __DIR__ . DIRECTORY_SEPARATOR - . 'env' . DIRECTORY_SEPARATOR - . '.env'; + . 'env' . DIRECTORY_SEPARATOR + . '.env'; if (!is_file($file_content)) { - die("Cannot read $file_content"); + die("Cannot read $file_content"); } if (copy($file_content, $env_file) === false) { - die("Cannot copy $file_content to $env_file"); + die("Cannot copy $file_content to $env_file"); } print "BASE: " . __DIR__ . "
";