Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [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)
+ [Exceptions](#exceptions)
+ [Resolution order](#resolution-order)
* [FAQ](#faq)
Expand All @@ -30,6 +31,9 @@ coercions through the system.
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).

<div id='installation'></div>

## Installation
Expand Down Expand Up @@ -151,20 +155,58 @@ use TinyBlocks\EnvironmentVariable\EnvironmentVariable;
EnvironmentVariable::from(name: 'MY_VAR')->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
<?php

declare(strict_types=1);

use TinyBlocks\EnvironmentVariable\ApplicationEnvironment;

# With APP_ENV=localhost in the environment.
$environment = ApplicationEnvironment::current();

$environment->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` | 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 one of the recognized boolean tokens. |
| 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. |

Messages carry the variable name, never its value, so a failed conversion on a secret does not leak it into logs or
stack traces.
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.

### Resolution order

Expand Down Expand Up @@ -197,6 +239,13 @@ The empty string is one of the values PHP's boolean filter recognizes as false,
variable declared but left blank therefore reads as `false`. Use `hasValue` first when the difference between blank and
false matters.

### 03. Why does an unrecognized `APP_ENV` raise instead of falling back to a default?

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.

<div id='license'></div>

## License
Expand Down
123 changes: 123 additions & 0 deletions src/ApplicationEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

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.
*
* <p>An alias matches regardless of casing and of surrounding whitespace, so <code>local</code>,
* <code>LocalHost</code>, and <code>LOCALHOST</code> padded with spaces all resolve to
* {@see ApplicationEnvironment::LOCAL}.</p>
*/
enum ApplicationEnvironment: string
{
case LOCAL = 'local';
case TEST = 'test';
case DEVELOPMENT = 'development';
case STAGING = 'staging';
case PRODUCTION = 'production';

/**
* Creates an ApplicationEnvironment from the <code>APP_ENV</code> environment variable.
*
* <p>The variable is read through {@see EnvironmentVariable}, so the same resolution order applies.</p>
*
* @return ApplicationEnvironment The environment matching the value of <code>APP_ENV</code>.
* @throws EnvironmentVariableMissing If <code>APP_ENV</code> is not set.
* @throws ApplicationEnvironmentUnrecognized If no environment recognizes the value of <code>APP_ENV</code>.
*/
public static function current(): ApplicationEnvironment
{
return ApplicationEnvironment::fromAlias(alias: EnvironmentVariable::from(name: 'APP_ENV')->toString());
}

/**
* Creates an ApplicationEnvironment from one of its aliases.
*
* @param string $alias The alias, matched regardless of casing and of surrounding whitespace.
* @return ApplicationEnvironment The environment that recognizes the alias.
* @throws ApplicationEnvironmentUnrecognized If no environment recognizes the alias.
*/
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));
}

/**
* Tells whether the application runs in the test environment.
*
* @return bool True if the environment is {@see ApplicationEnvironment::TEST}, false otherwise.
*/
public function isTest(): bool
{
return $this === self::TEST;
}

/**
* Returns the aliases the environment recognizes.
*
* @return list<string> The lowercase aliases, starting with the backing value of the case.
*/
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']
};
}

/**
* Tells whether the application runs in the local environment.
*
* @return bool True if the environment is {@see ApplicationEnvironment::LOCAL}, false otherwise.
*/
public function isLocal(): bool
{
return $this === self::LOCAL;
}

/**
* Tells whether the application runs in the staging environment.
*
* @return bool True if the environment is {@see ApplicationEnvironment::STAGING}, false otherwise.
*/
public function isStaging(): bool
{
return $this === self::STAGING;
}

/**
* Tells whether the application runs in the production environment.
*
* @return bool True if the environment is {@see ApplicationEnvironment::PRODUCTION}, false otherwise.
*/
public function isProduction(): bool
{
return $this === self::PRODUCTION;
}

/**
* Tells whether the application runs in the development environment.
*
* @return bool True if the environment is {@see ApplicationEnvironment::DEVELOPMENT}, false otherwise.
*/
public function isDevelopment(): bool
{
return $this === self::DEVELOPMENT;
}
}
25 changes: 25 additions & 0 deletions src/Exceptions/ApplicationEnvironmentUnrecognized.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\EnvironmentVariable\Exceptions;

use InvalidArgumentException;
use TinyBlocks\EnvironmentVariable\ApplicationEnvironment;

/**
* Raised when no application environment recognizes an alias.
*/
final class ApplicationEnvironmentUnrecognized extends InvalidArgumentException
{
public function __construct(string $alias)
{
$template = 'Application environment <%s> 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));
}
}
Loading