In a Laravel gym-membership app, why does env('PAYMENT_API_KEY') in a controller return null in production while it works locally?
answer
- production boots from a config cache
- .env is skipped when config is cached
- env() only sees real process variables
- env() belongs inside config/*.php
- config('services.payment.key') in code
basics
~20 sProduction runs with cached configuration, and once config is cached Laravel skips loading .env, so env() sees only real server environment variables. Map the key in a config file and read it with config() instead.
solid answer
~40 sLaravel's `LoadEnvironmentVariables` bootstrapper returns early when `$app->configurationIsCached()` is true, so a production server whose deploy cached the configuration never reads `.env`. `env()` is just a lookup in the process environment through phpdotenv's repository, so a value that lived **only** in `.env` is absent and `env('PAYMENT_API_KEY')` returns its default, `null`. Locally nothing is cached, `.env` is loaded on every boot and the same line works, which is why the bug hides until release. The fix is the convention the framework itself follows: call `env()` only inside `config/*.php` (for example `'payment' => ['key' => env('PAYMENT_API_KEY')]` in `config/services.php`) and read `config('services.payment.key')` everywhere else. The cached config file already holds the resolved value, so `config()` works whether or not `.env` was loaded.
code
php · 9 lines<?php
// config/services.php
return [
// ...
'payment' => [
'key' => env('PAYMENT_API_KEY'),
],
];go deeper
Remember the rule: env() goes in config files only, and code everywhere else reads config('file.key'). Be able to write the services.php entry and the config() call.
Explain the boot order: LoadEnvironmentVariables skips .env when configurationIsCached() is true, and LoadConfiguration then reads the cached array. Show why env() in a controller returns its default.
Show you can diagnose it on a live server: compare real process variables with .env-only ones, use config:show to see resolved values, and make required keys fail loudly with Env::getOrFail or Config::string.
Frame it as a team convention: a lint or review rule banning env() outside config/, and required settings validated when configuration loads so a bad deploy fails in the pipeline, not at a member's checkout.
## The symptom A gym-membership app charges monthly fees through a payment provider. A developer reads the provider's key straight from the environment inside a controller: ```php $key = env('PAYMENT_API_KEY'); ``` On a laptop the checkout works. After the production deploy, every charge fails because `$key` is `null`. The `.env` file on the server is present and correct, which makes the bug look impossible. ## How Laravel reads `.env` at boot Two things feed configuration in a Laravel app: - **`.env`** holds per-machine values as plain `KEY=value` lines. Laravel parses it with the **phpdotenv** library and places the values in the process environment. - **`config/*.php`** files return arrays. Their entries call the global `env()` helper, for example `'env' => env('APP_ENV', 'production')` in `config/app.php`, and the resulting values land in the **config repository** that `config()` reads. During boot the HTTP and console kernels run a list of bootstrappers. The order matters here: 1. `LoadEnvironmentVariables` loads `.env` into the environment, **unless** `$app->configurationIsCached()` returns true, in which case it returns immediately. 2. `LoadConfiguration` either `require`s the cached file in `bootstrap/cache/config.php` or, when there is no cache, executes every file in `config/` (where the `env()` calls run). Most production deploys cache the configuration, so step 1 is skipped and step 2 reads the cache. The `env()` calls inside `config/*.php` already ran when the cache was built, and their results are frozen in that file. An `env()` call anywhere else runs at request time, when `.env` was never loaded. ## What `env()` actually returns `env($key, $default = null)` delegates to `Illuminate\Support\Env::get()`, which asks phpdotenv's repository for the variable and falls back to the default when it is missing. So with a cached config: | Where the value lives | `env('PAYMENT_API_KEY')` in a controller | `config('services.payment.key')` | |---|---|---| | Only in `.env` | `null` (or the default you passed) | the value frozen in the cache | | A real server environment variable | the value | the value frozen in the cache | | Nowhere | `null` | `null` | Note the middle row: a variable set by the host or container is still in the process environment, so `env()` returns it. That is why the bug is described as "env() returns null for values that exist only in .env", and why it can appear on one server and not another. ## The fix Declare the variable once, in a config file, and read the config key everywhere else: ```php // config/services.php 'payment' => [ 'key' => env('PAYMENT_API_KEY'), ], // app/Http/Controllers/MembershipController.php $key = config('services.payment.key'); ``` `config/services.php` is the skeleton's conventional home for third-party credentials; a dedicated `config/payments.php` works the same way. ## Rules of thumb interviewers look for - **`env()` only inside `config/*.php`.** Every default Laravel config file follows this rule; application code, providers, jobs and views read `config()`. - **The default argument does not fix it.** `env('PAYMENT_API_KEY', '')` returns an empty string instead of `null`, which still breaks the charge, just less loudly. - **Fail loudly when a value is required.** `Config::string('services.payment.key')` throws `InvalidArgumentException` if the value is `null` or not a string, so a missing key stops the request with a clear message instead of reaching the provider. - **Nothing here depends on the `.env` file being missing.** It exists on the server; it is simply not read once config is cached. - **Tests will not catch it by default**, because the test suite runs without a config cache and loads the environment normally. ## Diagnosing it on a live server When a value is `null` in production and fine elsewhere, a short checklist settles it: 1. Check whether configuration is cached: the file `bootstrap/cache/config.php` exists on the server. 2. Run `php artisan config:show services.payment` to see the value the app resolved. If it is correct there, the config file is right and the bug is in code that bypasses `config()`. 3. Search the codebase for `env(` outside `config/`. Every hit is a candidate for the same failure. 4. Check whether the variable exists in the real process environment. If it does on one server and not on another, that explains why only some servers fail. A useful habit is a test or static-analysis rule that fails the build when `env(` appears outside `config/`, so the mistake never reaches a deploy. ## Why interviewers ask it It is the classic mid-level Laravel trap: the code works in every local and CI run and fails only in production. A good answer names the cause (config caching skips `.env`), the rule (`env()` in config files only) and the replacement (`config()` with a dot-notation key).
- If the host sets PAYMENT_API_KEY as a real environment variable, does env() in the controller still return null?No. phpdotenv's repository reads the process environment, and a variable set by the host or container is there whether or not `.env` was loaded, so `env()` returns it. Only values that existed solely in `.env` disappear once config is cached. It still is not a fix: the code now depends on how each server injects variables, and moving the value back into `.env` silently breaks it again.
- How can the app fail at deploy time rather than at checkout when the payment key is missing?Use `Illuminate\Support\Env::getOrFail('PAYMENT_API_KEY')` in the config file instead of `env()`. It throws a `RuntimeException` ("Environment variable [PAYMENT_API_KEY] has no value.") while configuration is being loaded, so the step that builds the config fails in the pipeline. At read time, `Config::string()` gives the same loud failure for a value that is `null`.
The config cache is a printed menu made the morning the restaurant opens. The kitchen reads the menu (config()); a waiter who tries to check the chef's scribbled notes (the .env file) at dinner finds they were never brought out of the office.
saying these in an interview costs you the question
- env() reads the .env file on every call, so it works anywhere
- The production server must be missing its .env file
- Passing a default like env('PAYMENT_API_KEY', '') fixes the bug
- Caching config makes env() throw an exception outside config files
- config() is slower than env(), so use env() in hot code paths