Environment Variables

Env vars carry per-machine settings; .env files supply defaults, processors cast them, and the values are read at runtime.

Syntax%env(processor:processor:VAR_NAME)%

An environment variable is a value the operating system hands to your process. Symfony reads configuration from environment variables so one build can run on a laptop and in production with different addresses, keys and flags, and so a credential never sits in a committed file.

The .env files

.env is a development convenience. It sets a variable only when the real environment has not already set it, so a variable exported by the server, the container runtime or the shell always wins. The files load in this order, each overriding the one above:

FileHoldsCommit
.envdefaults for every developeryes
.env.localthis machine's overridesno
.env.<APP_ENV>per-environment defaultsyes
.env.<APP_ENV>.localthis machine, one environmentno

Reading one in configuration

%env(MAILER_DSN)% reads a variable. A value from a .env file is always a string, so Symfony ships processors that cast it: %env(bool:FEATURE_SIGNUPS)%, %env(int:HTTP_PORT)%, %env(json:ALLOWED_LANGUAGES)%, %env(csv:REDIS_NODES)%. Plain %env(FOO)% and %env(string:FOO)% are the same thing. Processors chain, and the one nearest the name runs first: %env(json:file:resolve:AUTH_FILE)% resolves parameters in the value, reads that file, then decodes the JSON. Symfony 8.1 also offers default:, key:, url:, query_string:, enum:, trim:, const:, not:, defined:, base64:, require:, shuffle:, float: and urlencode:.

Why runtime matters

Parameters are frozen into the compiled container. Environment values are not: the container stores a placeholder and resolves it when it is used, so a new value takes effect without rebuilding the cache.

Example

Example · php
// src/Service/SignupGate.php
declare(strict_types=1);

namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class SignupGate
{
    public function __construct(
        // app.signups_enabled is '%env(bool:FEATURE_SIGNUPS)%', so the string
        // from the environment arrives here already cast to a real bool
        #[Autowire(param: 'app.signups_enabled')]
        private bool $signupsEnabled,
        #[Autowire(param: 'app.signup_closed_message')]
        private string $closedMessage,
    ) {
    }

    /**
     * Returns null when signups are open, or the reason they are not.
     */
    public function reasonClosed(): ?string
    {
        return $this->signupsEnabled ? null : $this->closedMessage;
    }
}

// $gate = new SignupGate(false, 'Signups reopen in April.');
// echo $gate->reasonClosed() ?? 'open';
// Output: Signups reopen in April.

When to use it

  • Point the application at a local mail catcher without changing a committed file.
  • Turn a feature off in production by setting one variable, with no cache rebuild.
  • Read a JSON credentials file whose path differs on every host.

More examples

The files themselves

The split that matters: .env documents which variables exist, .env.local holds the values. Add .env.local and .env.*.local to .gitignore, which the Symfony skeleton already does. Check that before putting a real key in one.

Example · bash
# .env  (committed: defaults, never a real credential)
APP_ENV=dev
APP_SECRET=
MAILER_DSN=null://null
FEATURE_SIGNUPS=false
# a default for a variable the code expects but you do not ship
[email protected]

# .env.local  (never committed: this machine only)
APP_SECRET=4f1c0d7b9a2e4c81
MAILER_DSN=smtp://127.0.0.1:1025
FEATURE_SIGNUPS=true

Casting and defaults in configuration

Naming a parameter per setting gives you one place to see the cast and one name to inject. The env(FEATURE_SIGNUPS) parameter is a fallback: without it, a missing variable is an error when the container needs the value.

Example · yaml
# config/services.yaml
parameters:
    # a default used when the variable is not set at all
    env(FEATURE_SIGNUPS): 'false'

    app.signups_enabled: '%env(bool:FEATURE_SIGNUPS)%'
    app.signup_closed_message: 'Signups reopen in April.'
    app.allowed_locales: '%env(csv:ALLOWED_LOCALES)%'
    app.webhook_timeout: '%env(int:WEBHOOK_TIMEOUT)%'

    # runs right to left: resolve %parameters% in the value, read the
    # file at that path, then JSON-decode what it contains
    app.google_auth: '%env(json:file:resolve:AUTH_FILE)%'
    env(AUTH_FILE): '%kernel.project_dir%/config/auth.json'

Checking and freezing values

debug:dotenv answers "which file won", which is the usual question when a value is not what you expected. composer dump-env is a production step: real environment variables still override the dumped file, so it changes performance, not precedence.

Example · bash
# which .env files were read, and the value each variable ended up with
$ php bin/console debug:dotenv

# every env var the configuration refers to, and whether it is set
$ php bin/console debug:container --env-vars

# deploy step: fold the .env files into one PHP file
$ composer dump-env prod
# writes .env.local.php, so the .env files are not parsed on each request

Discussion

  • Be the first to comment on this lesson.