Symfony resolves %env(NAME)% parameters at runtime, from $_ENV, $_SERVER and getenv(). envless/env plugs into that in one of two ways. Pick one.
composer require envless/envOption 1: the env var loader
Symfony asks every service tagged container.env_var_loader for a variable it cannot find anywhere else. Envless\Env\Symfony\EnvVarLoader answers with your Envless variables, loading them the first time Symfony asks:
services:
Envless\Env\Symfony\EnvVarLoader:
tags: [container.env_var_loader]use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class Checkout
{
public function __construct(
#[Autowire(env: 'STRIPE_SECRET')] private readonly string $stripeSecret,
#[Autowire(env: 'int:CHECKOUT_TIMEOUT')] private readonly int $timeout,
) {
}
}Nothing changes in public/index.php or bin/console. When Env::isConfigured() is false, as in a composer install without credentials that runs cache:clear, the loader returns nothing and Symfony falls back to its defaults and .env, so the build does not need secrets.
Option 2: load in the front controller
To have every variable in $_ENV, $_SERVER and getenv() before the kernel boots, load inside the closure your front controller returns, in public/index.php and the same way in bin/console:
<?php
use App\Kernel;
use Envless\Env\Env;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return static function (array $context) {
if (Env::isConfigured()) {
Env::load();
}
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};Do not require 'vendor/autoload.php' above autoload_runtime.php to load earlier. The Symfony Runtime includes the front controller a second time and stops at once when it finds the autoloader already included, so the page renders nothing at all.
.env and Envless together
Both options let .env win for a name both define. The Symfony Runtime loads .env into $_ENV and $_SERVER before the closure runs, and envless/env never overwrites a variable that is already set, while the env var loader is only asked about names Symfony found nowhere else. Move a variable to Envless by deleting it from .env and .env.local. The credentials, ENVLESS_TOKEN, ENVLESS_KEY, ENVLESS_PROJECT and ENVLESS_ENV, come from the real environment, or the slugs from a .envless file.
Reading values
use Envless\Env\Env;
$port = Env::number('PORT');
$checkout = Env::boolean('CHECKOUT_ENABLED');%env()% and #[Autowire(env: ...)] keep working as they do today, with Symfony's processors such as int:, bool: and json:. In your own code, Env::string() and its siblings are typed and throw on a missing name. Generate a class with a method per variable with vendor/bin/envless-env types, --output src/Env/EnvlessEnv.php --namespace 'App\Env'.
Messenger workers
bin/console messenger:consume loads once per worker process, through the loader the first time a service needs a variable, or through the front controller closure at boot. messenger:stop-workers makes the next workers load again.
Client variables in Twig
use Envless\Env\Env;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
final class CheckoutController extends AbstractController
{
public function __invoke(): Response
{
return $this->render('checkout.html.twig', ['env' => Env::client()->snapshot()]);
}
}<script>
window.env = {{ env|json_encode|raw }};
</script>Only the variables marked client in Envless are in the snapshot, so a server secret cannot reach the template by accident.