PHP

Read variables

Read Envless variables in PHP: coercion to int, float and bool, typed reads for PHPStan, optional reads, the client view, test overrides, and why the views refuse to be serialised.

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.

php
use Envless\Env\Env;

$url = Env::get('DATABASE_URL');
$port = Env::get('PORT');
$debug = Env::get('APP_DEBUG');

Coercion

Declared typeReads back asExample
stringstring'postgres://db/app'
numberint when whole, otherwise float'3000' reads 3000, '0.25' reads 0.25, 'abc' reads NAN
booleanbooltrue 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:

php
use Envless\Env\Env;

$host = Env::string('DB_HOST');
$port = Env::number('DB_PORT');
$secure = Env::boolean('DB_SSL');
ReadReturnsRule
Env::string($name)stringThe value as Envless stores it
Env::number($name)int|floatThe number rule above, whatever the declared type
Env::boolean($name)boolThe 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:

txt
Missing env var: STRIPE_SECRET

For a variable that is genuinely allowed to be absent, read it with Env::optional(), which returns null instead, or ask first with Env::has():

php
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().

php
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.

php
<script>
    window.env = <?= json_encode(Envless\Env\Env::client(), JSON_HEX_TAG | JSON_THROW_ON_ERROR) ?>;
</script>

Secrets stay inside

You writeYou 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 viewAn 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:

php
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.