josegonzalez/dotenv
josegonzalez/dotenv is a lightweight PHP library for loading environment variables from .env files into getenv(), $_ENV, and $_SERVER. It supports environment-specific files, sections, and overwrite or immutable modes, making configuration management simple in any PHP or Laravel project.
Install it alongside your existing Laravel app (it does not ship with Laravel — Laravel defaults to vlucas/phpdotenv):
composer require josegonzalez/dotenv
The package centers on a single class, josegonzalez\Dotenv\Loader, with a fluent API. The shortest viable boot is parse-then-apply:
use josegonzalez\Dotenv\Loader;
(new Loader(__DIR__ . '/.env'))
->parse()
->toEnv();
Where to look first: Loader::parse() and Loader::toArray(). Parse-only mode is the safest entry point because it inspects a file without touching $_ENV, $_SERVER, or putenv(). Use it to confirm the package finds and parses your file before you start mutating global state:
$values = (new Loader(base_path()))->parse()->toArray();
// ['APP_NAME' => 'My App', 'DB_HOST' => '127.0.0.1', ...]
First real use case: load an environment-specific file before Laravel boots. Because josegonzalez/dotenv lets you pick the filename explicitly, it fits naturally into bootstrap/env.php or the top of artisan:
// artisan, public/index.php, tests/bootstrap.php
$env = getenv('APP_ENV') ?: 'local';
(new Loader(__DIR__ . '/..'))->setFilename(".env.{$env}")->parse()->toEnv()->toServer();
If that runs cleanly, you have the 80% of the package you'll actually use.
1. Pre-bootstrap env selector. The main reason a Laravel dev reaches for this package over the default loader: choose the file before the container exists. Wrap it as a small bootstrap file required at the top of artisan, public/index.php, and your PHPUnit bootstrap so CLI, web, and test paths all agree.
// bootstrap/env.php
$env = $_SERVER['APP_ENV'] ?? getenv('APP_ENV') ?: 'local';
(new Loader(__DIR__ . '/..'))
->setFilename(".env.{$env}")
->parse()
->toEnv()
->toServer();
2. Disable Laravel's loader. If you're going to own env loading, own it fully. Remove or replace Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables from the bootstrapper list (or short-circuit it in bootstrap/app.php) so two loaders don't race for the same keys.
3. Parse-only for validation. Write a small artisan command that checks required keys exist before deployment:
$values = (new Loader(base_path()))->parse()->toArray();
$missing = array_diff(['APP_KEY', 'DB_HOST', 'DB_DATABASE'], array_keys($values));
if ($missing) {
$this->error('Missing: ' . implode(', ', $missing));
}
4. Redaction before logging or shelling out. Filters mutate the parsed map in place, so you can mask secrets before they leave the process:
(new Loader(base_path()))
->parse()
->filter('DB_PASSWORD', $redacted = '***')
->toEnv();
5. Value transformations. Filters also accept callables, which is handy for normalizing casing, trimming, or coercing types:
->filter('APP_ENV', fn ($v) => strtolower($v))
->filter('DB_PORT', fn ($v) => (int) $v)
6. Constants for legacy code. define() writes parsed values into PHP constants — useful when integrating with older libraries or scripts that expect APP_ENV-style constants rather than env vars:
(new Loader(base_path()))->parse()->define();
7. Multi-tenant or per-connection loading. Loop over tenant dirs and load each .env, being deliberate about overwriting:
foreach ($tenants as $tenant) {
(new Loader(base_path("tenants/{$tenant->slug}")))
->setFilename('.env')
->parse()
->overload() // see gotchas: use only in CLI/scripts
->toEnv();
}
8. Standalone scripts in CI/CD. Any deploy or maintenance script that doesn't want to boot the whole framework but does need env vars can load a specific file in three lines. This is the cleanest fit for the package in day-to-day work.
Integration tips: run loading before Application is built (not inside config files — env() returns null there after config:cache), keep filenames conventional (.env, .env.local, .env.staging, .env.testing), and prefer toEnv()->toServer() over putenv() so values are visible to env() and getenv()-style lookups consistently.
.env via vlucas/phpdotenv. Running both is redundant at best and order-dependent at worst. Confirm which one is active before debugging mysterious missing keys.overload() only in CLI scripts where you explicitly need a file to override the process environment.putenv() is legacy. Laravel deprecated putenv() in 5.8 because it isn't thread-safe and doesn't survive config:cache. Prefer toEnv()/toServer()..env. Once php artisan config:cache runs, none of this matters for the web path. Re-cache after changing env files, and remember env() outside config files returns null at runtime.define() won't overwrite. Pre-defined constants are skipped silently. Same pattern applies to env/server arrays unless you call overload(). Don't expect a second load to "refresh" values.base_path() or __DIR__ — otherwise php artisan from a subdirectory silently looks in the wrong place.try/catch or check file_exists() first, and fail loudly with a clear message so deploys don't ship with an empty environment.filter() mutates the parsed map. Chain it before toEnv()/toServer()/toArray(), and remember later writes to the same key need to come before earlier consumers.->parse()->toArray() with getenv('KEY') at the top of bootstrap/env.php; that isolates "did the file parse correctly" from "did something else overwrite it later."Loader is easy to subclass or wrap in a small service class (EnvironmentLoader) for DI-friendly usage in commands and tests.vlucas/phpdotenv, which is the ecosystem default and receives ongoing maintenance.How can I help you explore Laravel packages today?