Once loaded, every variable is read through Envless\Env\Env. Each value is stored as the string Envless decrypted, and coerced to the type you set on the variable in the dashboard every time you read it.
use Envless\Env\Env;
$url = Env::get('DATABASE_URL');
$port = Env::get('PORT');
$debug = Env::get('APP_DEBUG');Coercion
| Declared type | Reads back as | Example |
|---|---|---|
string | string | 'postgres://db/app' |
number | int when whole, otherwise float | '3000' reads 3000, '0.25' reads 0.25, 'abc' reads NAN |
boolean | bool | true for exactly 'true' or '1', false for anything else, 'TRUE' included |
A number is parsed the way JavaScript's Number() parses it, so '0x1A' reads 26, ' 42 ' reads 42 and an empty string reads 0, exactly as in @goenvless/env. A whole number beyond 2^53 stays a float, because that is where the value stops being exact. A variable with no declared type is a string.
Typed reads
Env::get() returns string|int|float|bool, because the type lives in Envless rather than in your code. When you know what a variable is, read it as that type and PHPStan and Psalm know it too:
use Envless\Env\Env;
$host = Env::string('DB_HOST');
$port = Env::number('DB_PORT');
$secure = Env::boolean('DB_SSL');| Read | Returns | Rule |
|---|---|---|
Env::string($name) | string | The value as Envless stores it |
Env::number($name) | int|float | The number rule above, whatever the declared type |
Env::boolean($name) | bool | The boolean rule above, whatever the declared type |
For one method per variable, typed by name, generate a class with vendor/bin/envless-env types.
Missing variables
A name that is not set throws MissingVariableException, whose variable property names it, so a missing secret fails loudly instead of flowing on as null:
Missing env var: STRIPE_SECRETFor a variable that is genuinely allowed to be absent, read it with Env::optional(), which returns null instead, or ask first with Env::has():
use Envless\Env\Env;
$dsn = Env::optional('SENTRY_DSN');
if (Env::has('FEATURE_BETA')) {
$beta = Env::boolean('FEATURE_BETA');
}The views
Env::server() returns every variable as an object, and Env::client() returns only the variables marked client in the dashboard. Both read with [], with -> and with foreach, and both have the same methods as Env: get(), optional(), has(), string(), number(), boolean() and names().
use Envless\Env\Env;
$env = Env::server();
$port = $env['PORT'];
$url = $env->get('DATABASE_URL');
foreach (Env::client() as $name => $value) {
echo $name, PHP_EOL;
}Property reads such as $env->PORT work too. PHPStan and Psalm cannot know your variable names, so they report them as unknown properties: in code they check, read with [] or get(), or generate a class with a typed method per variable.
Reading a server variable through Env::client() throws ServerOnlyException, STRIPE_SECRET is server only. Read it with Envless\Env\Env::get() instead., so a template that is handed the client view cannot reach a secret by mistake. The client view is also the one you may serialise: json_encode(Env::client()) and Env::client()->snapshot() give you the client variables, coerced, and nothing else.
<script>
window.env = <?= json_encode(Envless\Env\Env::client(), JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
</script>Secrets stay inside
| You write | You get |
|---|---|
json_encode(Env::server()) | SerialisationException |
serialize(Env::server()), serialize(Env::client()) | SerialisationException |
var_dump(), print_r(), Laravel's dump() and dd() of Env::server() | The variable names, never a value |
var_export() of a view | An empty object |
$env->PORT = '1', unset($env['PORT']) | ReadOnlyException |
This catches the accidental copy, the log line or the error report that serialises everything it is handed. It is not a wall: Env::load() returns a plain array on purpose, and foreach (Env::server() ...) copies values out the way Object.entries(env) does in Node.
Overrides in tests
Env::override() layers values over the loaded ones until you call restore(). The newest layer wins, coercion still follows the declared type of the loaded variable, and an override works before any load, so a unit test needs no API at all:
use Envless\Env\Env;
use Envless\Env\Override;
use PHPUnit\Framework\TestCase;
final class CheckoutTest extends TestCase
{
private Override $override;
protected function setUp(): void
{
$this->override = Env::override(['STRIPE_SECRET' => 'sk_test_123', 'CHECKOUT_ENABLED' => 'true']);
}
protected function tearDown(): void
{
$this->override->restore();
}
public function testCheckoutIsEnabled(): void
{
self::assertSame('sk_test_123', Env::string('STRIPE_SECRET'));
}
}An override carries strings, like the values Envless stores. It does not touch $_ENV, $_SERVER or getenv(), so code that reads those directly still sees what the last load wrote.