diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22aac70 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/tests export-ignore +/vendor export-ignore + +/LICENSE export-ignore +/Makefile export-ignore +/README.md export-ignore +/phpmd.xml export-ignore +/phpunit.xml export-ignore +/phpstan.neon.dist export-ignore +/infection.json.dist export-ignore + +/.github export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000..6a9bba4 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,22 @@ +name: Auto assign issues + +on: + issues: + types: + - opened + +jobs: + run: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Assign issues + uses: gustavofreze/auto-assign@1.0.0 + with: + assignees: '${{ secrets.ASSIGNEES }}' + github_token: '${{ secrets.GITHUB_TOKEN }}' + allow_self_assign: 'true' + allow_no_assignees: 'true' + assignment_options: 'ISSUE' \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96e4a08 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +env: + PHP_VERSION: '8.3' + +jobs: + auto-review: + name: Auto review + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + + - name: Install dependencies + run: composer update --no-progress --optimize-autoloader + + - name: Run review + run: composer review + + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use PHP ${{ env.PHP_VERSION }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + + - name: Install dependencies + run: composer update --no-progress --optimize-autoloader + + - name: Run tests + run: composer tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3333ef2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea + +/vendor/ +/report +*.lock +.phpunit.* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b68ede6 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.3 + +.PHONY: configure test test-file test-no-coverage review show-reports clean + +configure: + @${DOCKER_RUN} composer update --optimize-autoloader + +test: + @${DOCKER_RUN} composer tests + +test-file: + @${DOCKER_RUN} composer tests-file-no-coverage ${FILE} + +test-no-coverage: + @${DOCKER_RUN} composer tests-no-coverage + +review: + @${DOCKER_RUN} composer review + +show-reports: + @sensible-browser report/coverage/coverage-html/index.html report/coverage/mutation-report.html + +clean: + @sudo chown -R ${USER}:${USER} ${PWD} + @rm -rf report vendor .phpunit.cache .lock diff --git a/README.md b/README.md index fa9cd5b..7b68d7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,89 @@ -# environment-variable -Provides a simple and flexible solution for managing environment variables, with easy access, type conversions, and validation handling. +# Environment variable + +[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) + +* [Overview](#overview) +* [Installation](#installation) +* [How to use](#how-to-use) +* [License](#license) +* [Contributing](#contributing) + +
+ +## Overview + +Provides a simple and flexible solution for managing environment variables, with easy access, type conversions, and +validation handling. + +
+ +## Installation + +```bash +composer require tiny-blocks/environment-variable +``` + +
+ +## How to use + +### Creating an environment variable + +To create and work with environment variables, use the `from` method to get an instance of the environment variable. + +```php +EnvironmentVariable::from(name: 'MY_VAR'); +``` + +### Conversions + +Once you have an instance of the environment variable, you can convert its value into various types. + +#### Convert to string + +To convert the environment variable to a string. + +```php +$environmentVariable = EnvironmentVariable::from(name: 'MY_VAR'); +$environmentVariable->toString(); +``` + +#### Convert to integer + +To convert the environment variable to an integer. + +```php +$environmentVariable = EnvironmentVariable::from(name: 'MY_VAR'); +$environmentVariable->toInteger(); +``` + +#### Convert to boolean + +To convert the environment variable to a boolean. + +```php +$environmentVariable = EnvironmentVariable::from(name: 'MY_VAR'); +$environmentVariable->toBoolean(); +``` + +### Check if the environment variable has a value + +Checks if the environment variable has a value. Values like `false`, `0`, and `-1` are valid and non-empty. + +```php +$environmentVariable = EnvironmentVariable::from(name: 'MY_VAR'); +$environmentVariable->hasValue(); +``` + +
+ +## License + +Environment variable is licensed under [MIT](LICENSE). + +
+ +## Contributing + +Please follow the [contributing guidelines](https://github.com/tiny-blocks/tiny-blocks/blob/main/CONTRIBUTING.md) to +contribute to the project. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..41a104e --- /dev/null +++ b/composer.json @@ -0,0 +1,72 @@ +{ + "name": "tiny-blocks/environment-variable", + "type": "library", + "license": "MIT", + "homepage": "https://github.com/tiny-blocks/environment-variable", + "description": "Provides a simple and flexible solution for managing environment variables, with easy access, type conversions, and validation handling.", + "prefer-stable": true, + "minimum-stability": "stable", + "keywords": [ + "psr", + "env", + "tiny-blocks", + "environment-variable" + ], + "authors": [ + { + "name": "Gustavo Freze de Araujo Santos", + "homepage": "https://github.com/gustavofreze" + } + ], + "support": { + "issues": "https://github.com/tiny-blocks/environment-variable/issues", + "source": "https://github.com/tiny-blocks/environment-variable" + }, + "autoload": { + "psr-4": { + "TinyBlocks\\EnvironmentVariable\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "TinyBlocks\\EnvironmentVariable\\": "tests/" + } + }, + "require": { + "php": "^8.3" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpunit/phpunit": "^11", + "phpstan/phpstan": "^1", + "dg/bypass-finals": "^1.8", + "infection/infection": "^0", + "squizlabs/php_codesniffer": "^3" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "infection/extension-installer": true + } + }, + "scripts": { + "test": "phpunit --configuration phpunit.xml tests", + "phpcs": "phpcs --standard=PSR12 --extensions=php ./src", + "phpmd": "phpmd ./src text phpmd.xml --suffixes php --ignore-violations-on-exit", + "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress", + "mutation-test": "infection --only-covered --threads=max --logger-html=report/coverage/mutation-report.html --coverage=report/coverage", + "test-no-coverage": "phpunit --configuration phpunit.xml --no-coverage tests", + "review": [ + "@phpcs", + "@phpmd", + "@phpstan" + ], + "tests": [ + "@test", + "@mutation-test" + ], + "tests-no-coverage": [ + "@test-no-coverage" + ] + } +} diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..45c49fc --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,23 @@ +{ + "logs": { + "text": "report/infection/logs/infection-text.log", + "summary": "report/infection/logs/infection-summary.log" + }, + "tmpDir": "report/infection/", + "minMsi": 100, + "timeout": 30, + "source": { + "directories": [ + "src" + ] + }, + "phpUnit": { + "configDir": "", + "customPath": "./vendor/bin/phpunit" + }, + "mutators": { + "@default": true + }, + "minCoveredMsi": 100, + "testFramework": "phpunit" +} diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..bb59312 --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,59 @@ + + + PHPMD Custom rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..937f06e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + paths: + - src + level: 9 + tmpDir: report/phpstan + reportUnmatchedIgnoredErrors: false diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..59058a4 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + + src + + + + + + tests + + + + + + + + + + + + + + + + + diff --git a/src/Environment.php b/src/Environment.php new file mode 100644 index 0000000..8c3c7e5 --- /dev/null +++ b/src/Environment.php @@ -0,0 +1,53 @@ +value))) { + '', 'null' => false, + default => true + }; + } + + public function toString(): string + { + return $this->value; + } + + public function toInteger(): int + { + return is_numeric($this->value) + ? (int)$this->value + : throw InvalidEnvironmentValue::fromIntegerConversion(value: $this->value, variable: $this->variable); + } + + public function toBoolean(): bool + { + $filteredValue = filter_var($this->value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + return $filteredValue !== null + ? $filteredValue + : throw InvalidEnvironmentValue::fromBooleanConversion(value: $this->value, variable: $this->variable); + } +} diff --git a/src/Internal/Exceptions/EnvironmentVariableMissing.php b/src/Internal/Exceptions/EnvironmentVariableMissing.php new file mode 100644 index 0000000..7dd4afb --- /dev/null +++ b/src/Internal/Exceptions/EnvironmentVariableMissing.php @@ -0,0 +1,17 @@ + is missing.'; + + parent::__construct(message: sprintf($template, $variable)); + } +} diff --git a/src/Internal/Exceptions/InvalidEnvironmentValue.php b/src/Internal/Exceptions/InvalidEnvironmentValue.php new file mode 100644 index 0000000..64d1d18 --- /dev/null +++ b/src/Internal/Exceptions/InvalidEnvironmentValue.php @@ -0,0 +1,27 @@ + for environment variable <%s> is invalid for conversion to <%s>.'; + + parent::__construct(message: sprintf($template, $value, $variable, $conversionType)); + } + + public static function fromIntegerConversion(string $value, string $variable): InvalidEnvironmentValue + { + return new InvalidEnvironmentValue(value: $value, variable: $variable, conversionType: 'integer'); + } + + public static function fromBooleanConversion(string $value, string $variable): InvalidEnvironmentValue + { + return new InvalidEnvironmentValue(value: $value, variable: $variable, conversionType: 'boolean'); + } +} diff --git a/tests/EnvironmentVariableTest.php b/tests/EnvironmentVariableTest.php new file mode 100644 index 0000000..36d751c --- /dev/null +++ b/tests/EnvironmentVariableTest.php @@ -0,0 +1,246 @@ +toString(); + + /** @Then the result should match the expected string value */ + self::assertEquals($expected, $actual); + } + + #[DataProvider('integerConversionDataProvider')] + public function testConvertToInteger(mixed $value, string $variable, int $expected): void + { + /** @Given the environment variable is set with the given integer value */ + putenv(sprintf('%s=%s', $variable, $value)); + + /** @When I try to convert the environment variable value to an integer */ + $actual = EnvironmentVariable::from(name: $variable)->toInteger(); + + /** @Then the result should match the expected integer value */ + self::assertEquals($expected, $actual); + } + + #[DataProvider('booleanConversionDataProvider')] + public function testConvertToBoolean(mixed $value, string $variable, bool $expected): void + { + /** @Given the environment variable is set with the given boolean value */ + putenv(sprintf('%s=%s', $variable, $value)); + + /** @When I try to convert the environment variable value to a boolean */ + $actual = EnvironmentVariable::from(name: $variable)->toBoolean(); + + /** @Then the result should match the expected boolean value */ + self::assertEquals($expected, $actual); + } + + #[DataProvider('hasValueDataProvider')] + public function testHasValue(mixed $value, string $variable): void + { + /** @Given the environment variable is set with the given value */ + putenv(sprintf('%s=%s', $variable, $value)); + + /** @When I check if the environment variable has a value */ + $actual = EnvironmentVariable::from(name: $variable)->hasValue(); + + /** @Then the result should be true (has value) */ + self::assertTrue($actual); + } + + #[DataProvider('hasNoValueDataProvider')] + public function testHasNoValue(mixed $value, string $variable): void + { + /** @Given the environment variable is set with the given value */ + putenv(sprintf('%s=%s', $variable, $value)); + + /** @When I check if the environment variable has a value */ + $actual = EnvironmentVariable::from(name: $variable)->hasValue(); + + /** @Then the result should be false (no value) */ + self::assertFalse($actual); + } + + public function testExceptionWhenVariableIsMissing(): void + { + /** @Given that the environment variable 'NON_EXISTENT' does not exist */ + $variable = 'NON_EXISTENT'; + + /** @Then an error indicating the variable is missing should occur */ + $this->expectException(EnvironmentVariableMissing::class); + $this->expectExceptionMessage('Environment variable is missing.'); + + /** @When I try to get the value of the missing environment variable */ + EnvironmentVariable::from(name: $variable); + } + + public function testExceptionWhenInvalidIntegerConversion(): void + { + /** @Given that the environment variable 'INVALID_INT' has an invalid integer value */ + putenv(sprintf('%s=%s', 'INVALID_INT', 'invalid-value')); + + /** @Then an error indicating the value cannot be converted to an integer should occur */ + $this->expectException(InvalidEnvironmentValue::class); + $this->expectExceptionMessage( + 'The value for environment variable is invalid for conversion to .' + ); + + /** @When I try to convert the invalid value to an integer */ + EnvironmentVariable::from(name: 'INVALID_INT')->toInteger(); + } + + public function testExceptionWhenInvalidBooleanConversion(): void + { + /** @Given that the environment variable 'INVALID_BOOL' has an invalid boolean value */ + putenv(sprintf('%s=%s', 'INVALID_BOOL', 'invalid-value')); + + /** @Then an error indicating the value cannot be converted to a boolean should occur */ + $this->expectException(InvalidEnvironmentValue::class); + $this->expectExceptionMessage( + 'The value for environment variable is invalid for conversion to .' + ); + + /** @When I try to convert the invalid value to a boolean */ + EnvironmentVariable::from(name: 'INVALID_BOOL')->toBoolean(); + } + + public static function stringConversionDataProvider(): array + { + return [ + 'String value' => [ + 'value' => 'Hello, world!', + 'variable' => 'VALID_STRING', + 'expected' => 'Hello, world!' + ], + 'Numeric string' => [ + 'value' => '123', + 'variable' => 'NUMERIC_STRING', + 'expected' => '123' + ], + 'Boolean true value' => [ + 'value' => true, + 'variable' => 'BOOLEAN_TRUE', + 'expected' => '1' + ], + 'Boolean false value' => [ + 'value' => false, + 'variable' => 'BOOLEAN_FALSE', + 'expected' => '' + ] + ]; + } + + public static function integerConversionDataProvider(): array + { + return [ + 'Float value' => [ + 'value' => '99.99', + 'variable' => 'FLOAT_VALUE', + 'expected' => 99 + ], + 'Integer value' => [ + 'value' => '123', + 'variable' => 'VALID_INT', + 'expected' => 123 + ], + 'Numeric string' => [ + 'value' => '42', + 'variable' => 'NUMERIC_STRING', + 'expected' => 42 + ] + ]; + } + + public static function booleanConversionDataProvider(): array + { + return [ + 'Numeric value one as string' => [ + 'value' => '1', + 'variable' => 'NUMERIC_TRUE', + 'expected' => true + ], + 'Numeric value zero as string' => [ + 'value' => '0', + 'variable' => 'NUMERIC_FALSE', + 'expected' => false + ], + 'Boolean true value as string' => [ + 'value' => 'true', + 'variable' => 'BOOLEAN_TRUE', + 'expected' => true + ], + 'Boolean false value as string' => [ + 'value' => 'false', + 'variable' => 'BOOLEAN_FALSE', + 'expected' => false + ] + ]; + } + + public static function hasValueDataProvider(): array + { + return [ + 'String value' => [ + 'value' => 'Hello, World!', + 'variable' => 'STRING_VALUE' + ], + 'Integer value 0' => [ + 'value' => '0', + 'variable' => 'INTEGER_ZERO' + ], + 'Boolean value true' => [ + 'value' => 'true', + 'variable' => 'BOOLEAN_TRUE' + ], + 'Boolean value false' => [ + 'value' => 'false', + 'variable' => 'BOOLEAN_FALSE' + ], + 'Integer value positive' => [ + 'value' => '123', + 'variable' => 'INTEGER_POSITIVE' + ], + 'Integer value negative' => [ + 'value' => '-1', + 'variable' => 'INTEGER_NEGATIVE' + ] + ]; + } + + public static function hasNoValueDataProvider(): array + { + return [ + 'Null value' => [ + 'value' => null, + 'variable' => 'NULL_VALUE' + ], + 'Empty string' => [ + 'value' => '', + 'variable' => 'EMPTY_STRING' + ], + 'String null value' => [ + 'value' => 'NULL', + 'variable' => 'NULL_VALUE' + ], + 'String with only spaces' => [ + 'value' => ' ', + 'variable' => 'STRING_WITH_SPACES' + ] + ]; + } +}