diff --git a/grumphp.yml b/grumphp.yml index 26082d2..ebda291 100644 --- a/grumphp.yml +++ b/grumphp.yml @@ -6,7 +6,9 @@ grumphp: tasks: phplint: ~ yamllint: ~ - jsonlint: ~ + jsonlint: + ignore_patterns: + - /^tests\/fixtures\/invalid\.json/ composer: ~ composer_normalize: ~ phpcs: ~ diff --git a/src/Environment.php b/src/Environment.php index d3cd97d..96aa170 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -319,4 +319,40 @@ protected static function redirect(string $url, int $code = 301): never header('Location: ' . $url); exit; } + + /** + * Process a file that contains environment variables to the current environment. + * + * @param string $file + * The path to the JSON file. + * + * @throws \RuntimeException + * If the file does not exist or is not readable. + * @throws \JsonException + * If the file contents is not valid JSON. + */ + public static function processEnvironmentFileJson(string $file): void + { + if (!is_file($file) || !is_readable($file)) { + throw new \RuntimeException("Environment file $file does not exist or is not readable."); + } + + $contents = @file_get_contents($file); + if ($contents === false) { + throw new \RuntimeException("Environment file $file was unable to be read."); + } + + // The depth is set intentionally low because we do not expect deeply + // nested JSON. + $values = json_decode($contents, true, 2, JSON_THROW_ON_ERROR); + + // We only support key value secrets that are strings. + $values = array_filter($values, static function ($value, $key) { + return is_string($key) && is_string($value); + }, ARRAY_FILTER_USE_BOTH); + + foreach ($values as $name => $value) { + static::set($name, $value); + } + } } diff --git a/tests/fixtures/invalid.json b/tests/fixtures/invalid.json new file mode 100644 index 0000000..6920164 --- /dev/null +++ b/tests/fixtures/invalid.json @@ -0,0 +1 @@ +this is an invalid json file diff --git a/tests/fixtures/nested.json b/tests/fixtures/nested.json new file mode 100644 index 0000000..d56dd09 --- /dev/null +++ b/tests/fixtures/nested.json @@ -0,0 +1,5 @@ +{ + "first_level": { + "second_level": "second_level_value" + } +} diff --git a/tests/fixtures/valid.json b/tests/fixtures/valid.json new file mode 100644 index 0000000..5c10941 --- /dev/null +++ b/tests/fixtures/valid.json @@ -0,0 +1,5 @@ +{ + "JSON_UPPER_KEY": "upper", + "json_lower_key": "lower", + "invalid_value": 1 +} diff --git a/tests/src/EnvironmentTest.php b/tests/src/EnvironmentTest.php index f8c6f67..19013dd 100644 --- a/tests/src/EnvironmentTest.php +++ b/tests/src/EnvironmentTest.php @@ -610,4 +610,54 @@ public static function providerEnvironment(): array ], ]; } + + /** + * Test processing JSON files into environment variables. + * + * @dataProvider providerProcessEnvironmentFileJson + */ + #[DataProvider('providerProcessEnvironmentFileJson')] + public function testProcessEnvironmentFileJson(string $file, array|string $expectedValues): void + { + if (is_string($expectedValues)) { + $this->expectException($expectedValues); + } + Environment::processEnvironmentFileJson($file); + + foreach ($expectedValues as $name => $value) { + $this->assertSame($value, getenv($name)); + $this->assertSame($value, Environment::get($name)); + } + } + + /** + * Data provider for ::testProcessEnvironmentFileJson. + */ + public static function providerProcessEnvironmentFileJson(): array + { + $fixtureDirectory = dirname(__DIR__) . '/fixtures/'; + return [ + 'valid-file' => [ + $fixtureDirectory . 'valid.json', + [ + 'JSON_UPPER_KEY' => 'upper', + 'json_lower_key' => 'lower', + 'JSON_LOWER_KEY' => false, + 'invalid_value' => false, + ], + ], + 'invalid-file' => [ + $fixtureDirectory . 'invalid.json', + \JsonException::class, + ], + 'nested-fail' => [ + $fixtureDirectory . 'nested.json', + \JsonException::class, + ], + 'missing-file' => [ + $fixtureDirectory . 'missing.json', + \RuntimeException::class, + ], + ]; + } }