diff --git a/README.md b/README.md index ba8470f..233bf3a 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,10 @@ * [Overview](#overview) * [Installation](#installation) * [How to use](#how-to-use) - + [Creating an environment variable](#creating-an-environment-variable) - + [Conversions](#conversions) - - [Convert to string](#convert-to-string) - - [Convert to integer](#convert-to-integer) - - [Convert to float](#convert-to-float) - - [Convert to boolean](#convert-to-boolean) - + [Check if the environment variable has a value](#check-if-the-environment-variable-has-a-value) + [Application environment](#application-environment) + + [Reading a variable](#reading-a-variable) + + [Conversions](#conversions) + + [Checking for a value](#checking-for-a-value) + [Exceptions](#exceptions) + [Resolution order](#resolution-order) * [FAQ](#faq) @@ -23,17 +19,13 @@ ## Overview -Provides a type-safe environment variable reader for PHP, wrapping raw values behind a typed accessor with explicit -string, integer, float, and boolean conversion methods. Supports defaults for missing variables and distinguishes -between absent and empty states. Built to surface configuration errors at read time rather than propagate silent -coercions through the system. +A type-safe environment variable reader for PHP. A variable converts to string, integer, float, or boolean, raising a +dedicated exception instead of coercing a value it cannot represent, and tells which environment the application runs +in through predicates such as `isLocal()` and `isProduction()`, so code never compares raw strings. Names in the `HTTP_` namespace are read only from sources an HTTP request cannot reach, so a request header is never mistaken for configuration. See [Resolution order](#resolution-order). -`ApplicationEnvironment` resolves `APP_ENV` into a fixed set of environments, each recognizing several aliases, so code -asks `isLocal()` instead of comparing raw strings. See [Application environment](#application-environment). -
## Installation @@ -46,26 +38,10 @@ 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. -When no source holds the variable, the method raises `EnvironmentVariableMissing`. - -```php -isLocal(); # true +EnvironmentVariable::from(name: 'APP_ENV')->isProduction(); # false ``` -### 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. The raw value is returned unchanged, with no trimming. - -```php -toString(); -``` +| Case | Aliases | Predicate | +|---------------|-----------------------------------------------|-----------------| +| `LOCAL` | `local`, `localhost` | `isLocal` | +| `TEST` | `test`, `testing` | `isTest` | +| `DEVELOPMENT` | `development`, `dev`, `develop` | `isDevelopment` | +| `STAGING` | `staging`, `stage`, `homolog`, `homologation` | `isStaging` | +| `PRODUCTION` | `production`, `prod` | `isProduction` | -#### Convert to integer +A value no environment recognizes, the empty string included, raises `ApplicationEnvironmentUnrecognized` from every +predicate instead of answering `false`. See [FAQ](#faq). -To convert the environment variable to an integer. Values that do not represent an integer, including values above -`PHP_INT_MAX`, raise `EnvironmentValueNotInteger` instead of being silently truncated. +The `ApplicationEnvironment` enum owns the table. Use `fromAlias` when a case is needed, for example to `match` on it, +and `aliases` for the aliases of a case. ```php toInteger(); +ApplicationEnvironment::fromAlias(alias: 'Homolog'); # ApplicationEnvironment::STAGING +ApplicationEnvironment::STAGING->aliases(); # ['staging', 'stage', 'homolog', 'homologation'] ``` -#### Convert to float +### Reading a variable -To convert the environment variable to a float. The decimal separator is `.` and thousands separators are not accepted. -Values that do not represent a float raise `EnvironmentValueNotFloat`. +`from` raises `EnvironmentVariableMissing` when no source holds the variable. `fromOrDefault` carries the default +instead, or an empty string when no default is given. ```php toFloat(); +EnvironmentVariable::from(name: 'MY_VAR'); +EnvironmentVariable::fromOrDefault(name: 'MY_VAR', defaultValueIfNotFound: 'default_value'); ``` -#### Convert to boolean +### Conversions -To convert the environment variable to a boolean. The accepted values are `1`, `true`, `on`, and `yes` for true, and -`0`, `false`, `off`, `no`, and the empty string for false. Anything else raises `EnvironmentValueNotBoolean`. +| Method | Returns | Accepted values | +|-------------|----------|-------------------------------------------------------------------------------------------| +| `toString` | `string` | Any value, returned unchanged with no trimming. | +| `toInteger` | `int` | An integer within the `int` range. Values outside it raise instead of being truncated. | +| `toFloat` | `float` | A number with `.` as the decimal separator, no thousands separators. | +| `toBoolean` | `bool` | `1`, `true`, `on`, `yes` for true. `0`, `false`, `off`, `no`, the empty string for false. | ```php toBoolean(); +# With PORT=8080 and DEBUG=on in the environment. +EnvironmentVariable::from(name: 'PORT')->toInteger(); # 8080 +EnvironmentVariable::from(name: 'DEBUG')->toBoolean(); # true ``` -### Check if the environment variable has a value +### Checking for a value -Checks if the environment variable has a value. Values like `false`, `0`, and `-1` are valid and non-empty. Only an -empty string, a value made of whitespace, and the literal `null` in any casing report no value. +`hasValue` reports `false` only for an empty string, a value made of whitespace, and the literal `null` in any casing. +Values like `false`, `0`, and `-1` have a value. Use it before `toBoolean` when a blank variable must not read as +`false`. ```php -hasValue(); ``` -### Application environment - -`ApplicationEnvironment` is the environment the application runs in. Use `current` to read it from `APP_ENV`. The -variable is read through `EnvironmentVariable::from`, so the [resolution order](#resolution-order) applies and a missing -`APP_ENV` raises `EnvironmentVariableMissing`. Use `fromAlias` when the raw value is already at hand. - -Each environment recognizes a set of aliases, matched regardless of casing and of surrounding whitespace, so `local`, -`LocalHost`, and `LOCALHOST` padded with spaces all resolve to `LOCAL`. A value no environment recognizes raises -`ApplicationEnvironmentUnrecognized`, whose message lists every accepted alias. - -| Case | Aliases | Predicate | -|---------------|-----------------------------------------------|-----------------| -| `LOCAL` | `local`, `localhost` | `isLocal` | -| `TEST` | `test`, `testing` | `isTest` | -| `DEVELOPMENT` | `development`, `dev`, `develop` | `isDevelopment` | -| `STAGING` | `staging`, `stage`, `homolog`, `homologation` | `isStaging` | -| `PRODUCTION` | `production`, `prod` | `isProduction` | - -The backing value of each case is its first alias, and `aliases` returns the full list of a case. - -```php -isLocal(); # true -$environment->isProduction(); # false - -ApplicationEnvironment::fromAlias(alias: 'Homolog'); # ApplicationEnvironment::STAGING -``` - ### Exceptions Every failure raises a dedicated class from `TinyBlocks\EnvironmentVariable\Exceptions`. All of them extend `InvalidArgumentException`, so a consumer can catch the broad type or the precise one. -| Exception | Raised by | Condition | -|--------------------------------------|------------------------|--------------------------------------------------| -| `EnvironmentVariableMissing` | `from`, `current` | No source holds the variable. | -| `EnvironmentValueNotInteger` | `toInteger` | The value does not represent an integer. | -| `EnvironmentValueNotFloat` | `toFloat` | The value does not represent a float. | -| `EnvironmentValueNotBoolean` | `toBoolean` | The value is not a recognized boolean token. | -| `ApplicationEnvironmentUnrecognized` | `fromAlias`, `current` | No application environment recognizes the alias. | +| Exception | Raised by | Condition | +|--------------------------------------|-------------------------------|----------------------------------------------| +| `EnvironmentVariableMissing` | `from` | No source holds the variable. | +| `EnvironmentValueNotInteger` | `toInteger` | The value does not represent an integer. | +| `EnvironmentValueNotFloat` | `toFloat` | The value does not represent a float. | +| `EnvironmentValueNotBoolean` | `toBoolean` | The value is not a recognized boolean token. | +| `ApplicationEnvironmentUnrecognized` | `is*` predicates, `fromAlias` | No environment recognizes the value. | -Messages of the conversion exceptions carry the variable name, never its value, so a failed conversion on a secret does -not leak it into logs or stack traces. `ApplicationEnvironmentUnrecognized` carries the alias it received, because an -environment name is not a secret and the value is what needs fixing. +No message carries the value it received, so a failed read on a secret does not leak it into logs or stack traces. The +conversion exceptions name the variable, and `ApplicationEnvironmentUnrecognized` lists the accepted aliases. ### Resolution order -A value is resolved from the first source that holds it. Non-scalar entries in the superglobals are skipped, so an array -left in `$_ENV` never reaches a conversion method. +A value is resolved from the first source that holds it. Non-scalar entries in the superglobals are skipped. | Order | Source | Read for names in the `HTTP_` namespace | |-------|---------------------|-------------------------------------------------| @@ -221,30 +155,26 @@ left in `$_ENV` never reaches a conversion method. Under CGI-like servers (PHP-FPM, mod_php, FastCGI), request headers are mapped into `$_SERVER` and into the SAPI environment under an `HTTP_` prefix, so a `Proxy` request header surfaces as `HTTP_PROXY`. For names in that namespace -the library skips `$_SERVER` and reads only the real process environment, which a request cannot write. A variable -genuinely set in the environment still resolves. See [httpoxy](https://httpoxy.org) for the background. +the library reads only the real process environment, which a request cannot write. See [httpoxy](https://httpoxy.org). ## FAQ -### 01. Why is a name starting with `HTTP_` not read from `$_SERVER`? - -Because anyone sending a request can write that namespace. Only the request-supplied value is refused, so a variable -genuinely set in the process environment still resolves. See [Resolution order](#resolution-order). - -### 02. Why does `toBoolean` return `false` for an empty value instead of raising? +### 01. Why does an unrecognized environment raise instead of answering `false`? -The empty string is one of the values PHP's boolean filter recognizes as false, alongside `0`, `off`, and `no`. A -variable declared but left blank therefore reads as `false`. Use `hasValue` first when the difference between blank and -false matters. +Answering `false` turns a typo into the wrong environment. With `APP_ENV=prd`, `isProduction()` answering `false` lets +every branch guarded by it run non-production behavior in production. Raising `ApplicationEnvironmentUnrecognized` +surfaces the misconfiguration at read time, with the accepted aliases in the message. -### 03. Why does an unrecognized `APP_ENV` raise instead of falling back to a default? +### 02. What breaks when upgrading from 2.x to 3.0.0? -A silent fallback turns a typo into the wrong environment. Falling back to `PRODUCTION` would run a misconfigured local -setup against production behavior, and falling back to `LOCAL` would relax production. Raising -`ApplicationEnvironmentUnrecognized` at read time surfaces the misconfiguration where it is introduced, with the -accepted aliases in the message. +- `Environment` declares `isLocal`, `isTest`, `isDevelopment`, `isStaging`, and `isProduction`. A class of your own + implementing `Environment` must add them. +- `ApplicationEnvironment::current()` is removed. Replace `ApplicationEnvironment::current()->isLocal()` with + `EnvironmentVariable::from(name: 'APP_ENV')->isLocal()`, or pass the value to `ApplicationEnvironment::fromAlias` + when a case is needed. +- The message of `ApplicationEnvironmentUnrecognized` no longer contains the value received. diff --git a/src/ApplicationEnvironment.php b/src/ApplicationEnvironment.php index ea8370b..921f1bd 100644 --- a/src/ApplicationEnvironment.php +++ b/src/ApplicationEnvironment.php @@ -5,7 +5,6 @@ namespace TinyBlocks\EnvironmentVariable; use TinyBlocks\EnvironmentVariable\Exceptions\ApplicationEnvironmentUnrecognized; -use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentVariableMissing; /** * Environment the application runs in, resolved from the aliases each environment recognizes. @@ -16,26 +15,28 @@ */ enum ApplicationEnvironment: string { + private const array ENVIRONMENTS_BY_ALIAS = [ + 'local' => self::LOCAL, + 'localhost' => self::LOCAL, + 'test' => self::TEST, + 'testing' => self::TEST, + 'development' => self::DEVELOPMENT, + 'dev' => self::DEVELOPMENT, + 'develop' => self::DEVELOPMENT, + 'staging' => self::STAGING, + 'stage' => self::STAGING, + 'homolog' => self::STAGING, + 'homologation' => self::STAGING, + 'production' => self::PRODUCTION, + 'prod' => self::PRODUCTION + ]; + case LOCAL = 'local'; case TEST = 'test'; case DEVELOPMENT = 'development'; case STAGING = 'staging'; case PRODUCTION = 'production'; - /** - * Creates an ApplicationEnvironment from theAPP_ENV environment variable.
- *
- * The variable is read through {@see EnvironmentVariable}, so the same resolution order applies.
- * - * @return ApplicationEnvironment The environment matching the value ofAPP_ENV.
- * @throws EnvironmentVariableMissing If APP_ENV is not set.
- * @throws ApplicationEnvironmentUnrecognized If no environment recognizes the value of APP_ENV.
- */
- public static function current(): ApplicationEnvironment
- {
- return ApplicationEnvironment::fromAlias(alias: EnvironmentVariable::from(name: 'APP_ENV')->toString());
- }
-
/**
* Creates an ApplicationEnvironment from one of its aliases.
*
@@ -47,12 +48,7 @@ public static function fromAlias(string $alias): ApplicationEnvironment
{
$candidate = strtolower(trim($alias));
- $recognized = array_find(
- ApplicationEnvironment::cases(),
- static fn(ApplicationEnvironment $environment): bool => in_array($candidate, $environment->aliases(), true)
- );
-
- return ($recognized ?? throw new ApplicationEnvironmentUnrecognized(alias: $alias));
+ return (self::ENVIRONMENTS_BY_ALIAS[$candidate] ?? throw new ApplicationEnvironmentUnrecognized());
}
/**
@@ -72,13 +68,7 @@ public function isTest(): bool
*/
public function aliases(): array
{
- return match ($this) {
- self::LOCAL => ['local', 'localhost'],
- self::TEST => ['test', 'testing'],
- self::DEVELOPMENT => ['development', 'dev', 'develop'],
- self::STAGING => ['staging', 'stage', 'homolog', 'homologation'],
- self::PRODUCTION => ['production', 'prod']
- };
+ return array_keys(self::ENVIRONMENTS_BY_ALIAS, $this);
}
/**
diff --git a/src/Environment.php b/src/Environment.php
index 733c3b9..4d40e26 100644
--- a/src/Environment.php
+++ b/src/Environment.php
@@ -4,6 +4,7 @@
namespace TinyBlocks\EnvironmentVariable;
+use TinyBlocks\EnvironmentVariable\Exceptions\ApplicationEnvironmentUnrecognized;
use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotBoolean;
use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotFloat;
use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotInteger;
@@ -37,6 +38,28 @@ public static function from(string $name): Environment;
*/
public static function fromOrDefault(string $name, ?string $defaultValueIfNotFound = null): Environment;
+ /**
+ * Tells whether the variable names the test environment.
+ *
+ * The value is matched against the aliases of {@see ApplicationEnvironment::TEST}, regardless of + * casing and of surrounding whitespace.
+ * + * @return bool True if the value is an alias of the test environment, false if it names another one. + * @throws ApplicationEnvironmentUnrecognized If no application environment recognizes the value. + */ + public function isTest(): bool; + + /** + * Tells whether the variable names the local environment. + * + *The value is matched against the aliases of {@see ApplicationEnvironment::LOCAL}, regardless of + * casing and of surrounding whitespace.
+ * + * @return bool True if the value is an alias of the local environment, false if it names another one. + * @throws ApplicationEnvironmentUnrecognized If no application environment recognizes the value. + */ + public function isLocal(): bool; + /** * Converts the environment variable value to a float. * @@ -59,6 +82,17 @@ public function hasValue(): bool; */ public function toString(): string; + /** + * Tells whether the variable names the staging environment. + * + *The value is matched against the aliases of {@see ApplicationEnvironment::STAGING}, regardless of + * casing and of surrounding whitespace.
+ * + * @return bool True if the value is an alias of the staging environment, false if it names another one. + * @throws ApplicationEnvironmentUnrecognized If no application environment recognizes the value. + */ + public function isStaging(): bool; + /** * Converts the environment variable value to a boolean. * @@ -74,4 +108,26 @@ public function toBoolean(): bool; * @throws EnvironmentValueNotInteger If the value cannot be converted to an integer. */ public function toInteger(): int; + + /** + * Tells whether the variable names the production environment. + * + *The value is matched against the aliases of {@see ApplicationEnvironment::PRODUCTION}, regardless of + * casing and of surrounding whitespace.
+ * + * @return bool True if the value is an alias of the production environment, false if it names another one. + * @throws ApplicationEnvironmentUnrecognized If no application environment recognizes the value. + */ + public function isProduction(): bool; + + /** + * Tells whether the variable names the development environment. + * + *The value is matched against the aliases of {@see ApplicationEnvironment::DEVELOPMENT}, regardless of + * casing and of surrounding whitespace.
+ * + * @return bool True if the value is an alias of the development environment, false if it names another one. + * @throws ApplicationEnvironmentUnrecognized If no application environment recognizes the value. + */ + public function isDevelopment(): bool; } diff --git a/src/EnvironmentVariable.php b/src/EnvironmentVariable.php index 05e5618..07292b7 100644 --- a/src/EnvironmentVariable.php +++ b/src/EnvironmentVariable.php @@ -30,6 +30,16 @@ public static function fromOrDefault(string $name, ?string $defaultValueIfNotFou return new EnvironmentVariable(environmentValue: EnvironmentValue::from(value: $value, variable: $name)); } + public function isTest(): bool + { + return $this->environmentValue->toApplicationEnvironment()->isTest(); + } + + public function isLocal(): bool + { + return $this->environmentValue->toApplicationEnvironment()->isLocal(); + } + public function toFloat(): float { return $this->environmentValue->toFloat(); @@ -45,6 +55,11 @@ public function toString(): string return $this->environmentValue->toString(); } + public function isStaging(): bool + { + return $this->environmentValue->toApplicationEnvironment()->isStaging(); + } + public function toBoolean(): bool { return $this->environmentValue->toBoolean(); @@ -54,4 +69,14 @@ public function toInteger(): int { return $this->environmentValue->toInteger(); } + + public function isProduction(): bool + { + return $this->environmentValue->toApplicationEnvironment()->isProduction(); + } + + public function isDevelopment(): bool + { + return $this->environmentValue->toApplicationEnvironment()->isDevelopment(); + } } diff --git a/src/Exceptions/ApplicationEnvironmentUnrecognized.php b/src/Exceptions/ApplicationEnvironmentUnrecognized.php index be8225b..fd89762 100644 --- a/src/Exceptions/ApplicationEnvironmentUnrecognized.php +++ b/src/Exceptions/ApplicationEnvironmentUnrecognized.php @@ -9,17 +9,20 @@ /** * Raised when no application environment recognizes an alias. + * + *The message names the accepted aliases, never the value received, so a secret read by mistake does not leak into + * logs or stack traces.
*/ final class ApplicationEnvironmentUnrecognized extends InvalidArgumentException { - public function __construct(string $alias) + public function __construct() { - $template = 'Application environment <%s> is not recognized. Accepted aliases are <%s>.'; + $template = 'Application environment is not recognized. Accepted aliases are <%s>.'; $acceptedAliases = implode(', ', array_map( static fn(ApplicationEnvironment $environment): string => implode(', ', $environment->aliases()), ApplicationEnvironment::cases() )); - parent::__construct(message: sprintf($template, $alias, $acceptedAliases)); + parent::__construct(message: sprintf($template, $acceptedAliases)); } } diff --git a/src/Internal/EnvironmentValue.php b/src/Internal/EnvironmentValue.php index 0e5516d..5bdfbfb 100644 --- a/src/Internal/EnvironmentValue.php +++ b/src/Internal/EnvironmentValue.php @@ -4,6 +4,7 @@ namespace TinyBlocks\EnvironmentVariable\Internal; +use TinyBlocks\EnvironmentVariable\ApplicationEnvironment; use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotBoolean; use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotFloat; use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentValueNotInteger; @@ -56,4 +57,9 @@ public function toInteger(): int ? $filteredValue : throw new EnvironmentValueNotInteger(variable: $this->variable); } + + public function toApplicationEnvironment(): ApplicationEnvironment + { + return ApplicationEnvironment::fromAlias(alias: $this->value); + } } diff --git a/tests/ApplicationEnvironmentTest.php b/tests/ApplicationEnvironmentTest.php index f8f86c9..27fad89 100644 --- a/tests/ApplicationEnvironmentTest.php +++ b/tests/ApplicationEnvironmentTest.php @@ -8,20 +8,13 @@ use PHPUnit\Framework\TestCase; use TinyBlocks\EnvironmentVariable\ApplicationEnvironment; use TinyBlocks\EnvironmentVariable\Exceptions\ApplicationEnvironmentUnrecognized; -use TinyBlocks\EnvironmentVariable\Exceptions\EnvironmentVariableMissing; final class ApplicationEnvironmentTest extends TestCase { - private const string UNRECOGNIZED_MESSAGE = 'Application environment