vlucas/phpdotenv
PHP dotenv loads environment variables from a .env file into getenv(), $_ENV and $_SERVER, keeping credentials and per-environment config out of code. A PHP port of Ruby dotenv, installable via Composer and works with Apache, Nginx, CLI and PHP's built-in server.
Architecture fit. vlucas/phpdotenv is a bootstrap-time configuration loader, not a runtime service. Its entire surface is a handful of entry points — Dotenv::createImmutable(), createMutable(), createArrayBacked(), Dotenv::parse(), a RepositoryBuilder for custom adapter/allow-list wiring, and a fluent validation API (required(), ifPresent(), notEmpty(), isInteger(), isBoolean(), allowedValues(), allowedRegexValues()). That makes it architecturally inert: it has no contracts to satisfy, no container bindings, no event loop participation, and no persistent state. It either runs once during boot or it doesn't run at all. This is the ideal shape for a third-party dependency in a regulated or long-lived codebase, because it cannot leak abstractions into your domain layer.
The package is framework-agnostic but is de facto infrastructure in Laravel, which ships it transitively via laravel/framework (wired through the LoadEnvironmentVariables bootstrapper). For a Laravel target, the honest framing is: this is not an adoption decision, it is already in the dependency graph. The decision you actually own is governance — version pinning, environment sourcing policy, and whether .env files are permitted outside local/CI.
Integration feasibility. High. composer require vlucas/phpdotenv, then one or two lines in the bootstrap. The package explicitly supports PHP's built-in server, CLI, and FPM, and resolves the "how do I get config into nginx vhosts" problem without touching host files. The main mechanical frictions are: variables_order in php.ini must include E (or S) or the superglobals are silently unpopulated on some shared hosts; the PutenvAdapter/createUnsafeImmutable path is explicitly documented as not thread-safe; and in Laravel, env() is only reliable before config:cache runs.
Technical risk. Low complexity risk, moderate process and supply-chain risk.
.env is a plaintext secret store by design. The README's .env/.env.example + .gitignore discipline is sound but entirely manual and easy to regress.required() validation this fails at first request rather than at deploy.createImmutable (framework default) means real environment variables win over .env; createMutable inverts that. Choosing wrong silently overrides platform-injected secrets — a subtle and expensive mistake.env() footgun. Any env() call outside config/*.php returns null once config is cached. This is a code-review discipline problem, not a package defect, but it produces the single most common Laravel incident tied to this library.${...} nesting, multiline values, BOM, and invalid UTF-8 have all been hardened in the 5.7 line specifically (BOM stripping, memory-safety crash fix, quadratic parsing removal, null-byte-safe putenv adapter). Treat ≥5.7 as a floor, not a preference.Key questions. (1) Is the target stack Laravel or standalone PHP, and is phpdotenv already transitive — what version does the framework pin? (2) Where do production secrets actually live — platform injection, Vault/Secrets Manager, or .env baked into an image? (3) Do we run under Octane/Swoole or parallel extension, where putenv semantics degrade? (4) Which repo files must be added to .gitignore/.dockerignore and scanner exclusions? (5) Who owns the .env.example contract when a new required variable is introduced?
Data caveats: the reported "Dependents: 0" is almost certainly a metadata-population gap rather than a real signal — Packagist download volume for this package is orders of magnitude above that. The "last release: 2026-10-25" date should be verified against Packagist before being used in any dependency-risk register. The composite score of ~69.5 with an opportunity score of ~52 reflects a mature, commoditized dependency: high reliability, low strategic upside.
Stack fit. For Laravel 9/10/11/12, vlucas/phpdotenv ^5.x arrives with the framework. Do not add a direct composer require unless you are (a) on a standalone/Symfony/Slim/legacy codebase, or (b) deliberately constraining the version independent of the frameach. Run composer why vlucas/phpdotenv first and treat the framework's constraint as authoritative to avoid a resolve conflict at the next minor upgrade. For non-Laravel PHP, it is the cleanest available option and pairs naturally with a front-controller bootstrap.
Migration path. Four realistic entry states:
.env.example to .env, call createImmutable(__DIR__)->safeLoad() in bootstrap, done..env.example, commit the example, add a required() assertion per variable so removal is enforced rather than assumed.symfony/dotenv and vlucas/phpdotenv do the same job with different precedence and parsing rules. Never run both; pick one and remove the other's bootstrap call.Dotenv::load() helper is gone; the create* factory plus a repository is now mandatory. Follow UPGRADING.md; the v4→v5 jump also changes the required-PHP floor and internal types.Compatibility. PHP ^7.2.5 || ^8.0, with 8.6 explicitly supported as of 5.7. BSD-3-Clause is permissive and commercially safe (attribution only; no copyleft contamination of proprietary application code). It has no PSR dependency, so it cannot drag in a framework-version conflict.
Sequencing. Order matters here, because step 5 is what unlocks everything downstream:
.env.example as the canonical contract (dummy or blank values only).createImmutable unless there is a documented reason otherwise).required([...]) for hard prerequisites, ->notEmpty() for credentials, ->isBoolean()/->isInteger()/->allowedValues() for anything the code branches on.env() call out of application code and into config/*.php..gitignore, .dockerignore, and secret-scanner allow/deny lists.php artisan config:cache to the deploy pipeline — now safe, and it removes per-request parse cost.5.x line..env under immutable mode.Maintenance. Among the lowest-burden dependencies available. Semantic versioning is followed with published upgrade guides for v2→v3→v4→v5, releases are incremental and well-described, and the 5.7 changelog shows corrective work (performance, UTF-8 safety, comment parsing) rather than feature churn. Budget realistically: one minor-version review per quarter, one major-migration exercise every few years. The absence of a formal SLA is the only structural gap, and Tidelift exists to close it if procurement requires it.
Support. GitHub issues plus Tidelift's commercial subscription for maintainer-backed support. A private security disclosure channel is published. If the application is customer-facing and regulated, either buy the Tidelift tier or accept that this dependency sits under generic open-source risk treatment — which is defensible given the maturity and the fact that the failure modes are loud and immediate.
Scaling. Effectively free. Parsing is a single small-file read at process bootstrap; the 5.7 work explicitly removed quadratic parsing behavior and reduced interpolation copies, so the cost is linear in file size and negligible versus framework boot. There is no shared state, no cache, no network call — horizontal scaling is unconstrained. The scaling caveat is architectural, not performance: under container fleets, .env should not be baked into images. Inject environment variables at the platform layer (Kubernetes envFrom, ECS task definitions, Heroku config vars, Vault agent) and let immutable mode's precedence rules do the right thing.
Failure modes — ranked by operational cost, since the most expensive ones are the quiet ones:
variables_order excludes E/S in php.ini, so the loader appears to succeed while the superglobals stay empty. Detect with a startup assertion, not a support ticket.required() — fail at boot, fail loudly, fail before traffic.env() invoked at runtime after config:cache returns null. Prevented by the step-5 refactor and a static-analysis rule..env..env committed or baked into an image layer. Mitigated by .gitignore/.dockerignore plus a pre-commit scanner.InvalidPathException unless safeLoad() is used. Choose deliberately: load() for local/CI parity, safeLoad() where a platform guarantees injection.Ramp-up. Near-zero. Any PHP developer is productive in under an hour; the API is two lines and the docs cover validation thoroughly. The actual onboarding cost is cultural, not technical: teaching developers that .env is a local-development convenience rather than a deployment mechanism, that .env.example is a contract that must be updated in the same PR that adds a variable, and that secrets belong in a managed store at production scale. Plan a short standards note and a CI check for .env.example coverage; that, plus the required() assertions, converts this from a convenient footgun into a reliable piece of infrastructure.
How can I help you explore Laravel packages today?