Laravel env() Outside config/: Catch Deployment Bugs Before config:cache
The bug only appears after configuration caching
A Laravel application can behave perfectly during development and then fail after deployment because a service class reads an environment variable directly:
// app/Services/AcmeClient.php
$token = env('ACME_TOKEN');
Laravel's configuration cache changes the boot process. Once configuration has been cached, the framework does not load the application's .env file for normal requests or Artisan commands. A direct env() call outside a configuration file may therefore return only an externally supplied system value or its fallback.
The reliable pattern is to read the environment variable once from a configuration file:
// config/services.php
return [
'acme' => [
'token' => env('ACME_TOKEN'),
],
];
Application code should then use Laravel's configuration repository:
// app/Services/AcmeClient.php
$token = config('services.acme.token');
This is the primary fix. A package should never be used to justify keeping unsafe environment access in application code.
Environment drift is a separate problem
Even when every env() call is in the right place, teams still have to keep several environment contracts aligned. A project might contain .env, .env.testing, .env.production, .env.sample, or a template with a company-specific name.
Consider these files:
# .env
APP_NAME=Codegenie
ACME_TOKEN=local-secret
# .env.production
APP_NAME=Codegenie
The values are expected to differ, but the missing ACME_TOKEN key can still indicate an incomplete deployment contract.
Other easy-to-miss cases include:
- a duplicate active assignment in one file;
- keys that differ only by case;
- a key used by the application but declared nowhere;
- an obsolete key that might no longer be consumed;
- direct
getenv(),$_ENV, or$_SERVERaccess; - a dynamic lookup such as
env('SERVICE_'.$driver)that static analysis cannot resolve safely; - a Vite key referenced in frontend code but absent from the environment contract.
A useful manual review
Before adding tooling, a team can already reduce these failures with a small review:
- Search application-owned PHP files for
env(and move valid reads intoconfig/. - Compare the key inventories of the environment files that represent complete environments.
- Treat commented assignments in non-active templates as documentation, not active values.
- Check
phpunit.xmland.env.testingtogether. - Review Vite
import.meta.envandloadEnv()usage. - Run the application with configuration cached before deployment.
Do not compare or print secret values while performing this review. Environment URLs, credentials, application keys, and tokens are supposed to differ between environments.
Automating the development feedback loop
I maintain Laravel Env Guard through Codegenie to automate this review for current Laravel applications. Install it as a development dependency:
composer require --dev codegenie-be/laravel-env-guard
Laravel package discovery registers the guard automatically. By default it runs only during console boots in the local environment. It does not require a separate audit command or a specially named .env.example file.
Each guarded audit reads the current project files directly and checks:
- unsafe
env()andIlluminate\Support\Env::get()calls outsideconfig/; - direct raw environment access for project-owned keys;
- duplicate, case-mismatched, missing, and possibly unused keys;
- drift across automatically discovered plaintext
.envand.env.*files; - explicit reference or comparison files when a project configures them;
- PHPUnit, Vite, Blade, Docker, workflow, and other application-owned usage covered by the scanner.
Blocking findings fail fast by default. Warnings and errors are written to STDERR as a compact key-only report during Artisan commands.
Secret handling is a hard boundary
The guard compares keys, never values. Its reports, exceptions, and logs do not include environment values. It performs no telemetry or network requests, never modifies .env files, and does not synchronize secrets.
Encrypted environment files are intentionally excluded because auditing them would require decryption. Production and normal HTTP scanning are disabled by default. The package is a development guard, not a secret manager or deployment system.
Likely-unused diagnostics also require judgment: a key may be consumed by infrastructure, a package, Docker, a process manager, or code outside the configured scan paths. Projects can explicitly ignore legitimate external keys and patterns.
A gradual rollout works best
For an existing application, I recommend this order:
- Install the package on a branch.
- Fix direct
env()usage outsideconfig/first. - Review duplicate and case-mismatch errors.
- Decide which environment files are complete peers and which are partial Vite-style layers.
- Suppress only intentional external or infrastructure-owned cases.
- Verify the result with
config:cacheand the normal test suite.
The current release, v1.2.1, supports Laravel 12 and 13 across their valid PHP 8.2-8.5 combinations. The package is also available on Packagist.
Disclosure: I maintain this open-source package through Codegenie. I am especially interested in technical feedback about renamed environment templates, partial environment layers, framework keys, and false-positive or false-negative edge cases. Please do not include real environment values or secrets in reports.
Comments
No comments yet. Start the discussion.