php-standard-library/env
A lightweight PHP package from the Standard Library for inspecting and modifying environment variables, working directories, and filesystem paths, with simple, consistent functions that are easy to drop into any project.
php-standard-library/env is a focused utility package for reading/writing process environment variables, the current working directory, and path manipulation. In a Laravel codebase, it is not a replacement for the framework's configuration layer — it is a narrowly scoped primitive that sits below config() and alongside env().
Where it fits cleanly:
config() is available but teams have historically reached for raw getenv()/$_ENV.illuminate/support is unacceptable but a consistent env-access contract is required.getcwd()/realpath() behavior varies by platform.Where it does not fit:
env() called outside config/*.php returns null once php artisan config:cached runs. Introducing a parallel, cache-unaware read path (raw getenv) creates a third source of truth alongside .env, config(), and the real process environment. This is an architectural regression, not an improvement.| Dimension | Assessment |
|---|---|
| Dependency footprint | Effectively zero beyond PHP core — no transitive risk, no license friction (MIT). |
| Laravel coupling | None required. No service provider, no facade, no config publishing. Adoption is composer require + a namespace import. |
| Conflicts | None with illuminate/support. Coexists with vlucas/phpdotenv (Laravel's loader) but does not read .env files itself — it reads the already-populated process environment. |
| Reversibility | High. Call sites are one-line functions; rollback is a mechanical revert. |
| Effort | Days, not weeks, for a pilot module. |
The integration ceiling is low in both directions: it is easy to adopt and easy to abandon, which is the right risk profile for a pilot, but also means it delivers limited architectural leverage on its own.
Ecosystem / supply-chain risk — HIGH. This is the dominant concern and must be stated plainly to stakeholders:
php-standard-library/php-standard-library). The README contains no usage examples, no API listing, and no version-compatibility matrix — every release note says only "see the main repository release." Documentation, issue tracking, and roadmap live upstream, which means our support surface is a repository we do not control and cannot file against locally.2026-10-28, which is in the future relative to a normal review cycle. Treat this as either a metadata error or an artifact of the scoring pipeline; verify against Packagist before any dependency is committed. If Packagist metadata disagrees with the repo, that is a governance red flag.php-standard-library project is a small-maintainer effort. A bus-factor of one on a package that mutates process-global state is a real continuity risk.Semantic risk — MEDIUM.
putenv()-style writes leak into child processes, into proc_open invocations, and across test boundaries. In parallel test runs (ParaTest, Pest --parallel) this produces nondeterministic, order-dependent failures that are expensive to diagnose.envBool('FLAG', false) returning "always a boolean" and rejecting 'true'-vs-true confusion. The actual package description says only "inspecting and modifying environment variables." The typed-helper API (envBool/envInt/envString) appears to be aspirational framing in the assessment document, not a confirmed interface. Do not let the pitch deck become the specification. Confirm the real surface (get_var, set_var, unset_var, current_dir, temp_dir-style functions are the plausible shape) before committing to a migration narrative.env() ("true" → true, "null" → null, "empty" → '') and then freezes values at config:cache time. Reading the same variable through two mechanisms with two coercion rules yields inconsistent values in the same request. This is the single most likely source of production surprises.Scope risk — LOW. The package does one thing. Feature creep is unlikely; abandonment is the likelier failure.
php-standard-library.dev and diff them against the internal pitch. If typed getters do not exist, the business case for "type safety" collapses and the value proposition reduces to "wrapper around getenv()" — which is not worth a dependency.getenv() in CLI/queue code," this package is a reasonable, low-cost answer. If the answer is "Laravel config hygiene," the answer is config() + a linting rule, not a new package.php-standard-library releases, and what is the SLA for a security patch?config:cache? What is the expected behavior in production where .env is not loaded at runtime at all? If the package reads only the process environment, our Docker/K8s deployment must inject every variable as a real env var — which Laravel users often do not do.| Layer | Fit | Rationale |
|---|---|---|
| Laravel HTTP / controllers | Avoid | config() is cached, auditable, and testable. Two read paths is a defect generator. |
| Laravel console commands, scheduler | Good | No config:cache semantics to fight; direct env reads are conventional here. |
| Queue workers / jobs | Good | Job timeouts, feature flags, and batch sizes are natural typed-getter targets (if typed getters exist). |
| Deployment / release scripts | Good | Build-time variables (CI_COMMIT_SHA, APP_ENV) are process-level by definition. |
| Shared internal Composer packages | Best fit | The one place where a framework-agnostic contract has genuine value and no illuminate/support dependency. |
| Path/cwd utilities | Conditional | Only if it demonstrably improves on getcwd()/DIRECTORY_SEPARATOR handling; otherwise it is a wrapper for its own sake. |
Governing principle: confine adoption to code that has no Laravel config() equivalent. Do not introduce a second configuration idiom into the framework layer. This keeps the blast radius small and makes the decision cheap to reverse.
| Phase | Duration | Action | Exit Criteria |
|---|---|---|---|
| 0. Due diligence | 1–2 days | Verify Packagist entry, PHP constraint, real API surface, upstream issue activity, last-commit recency of the monorepo. | Written go/no-go memo. If Packagist is stale or the API lacks typed helpers, stop. |
| 1. Inventory | 2–3 days | Enumerate getenv(/$_ENV/$_SERVER usage outside config/. Classify each as env var, path, or cwd. |
Quantified call-site count; identification of the 1–2 modules with the highest churn. |
| 2. Pilot | 1 sprint | Adopt in one module — a single console command or a shared non-Laravel library. Wrap the package behind our own thin interface (App\Support\Environment) rather than importing its functions at every call site. |
Pilot module green in CI; no behavior change in production; wrapper proves sufficient. |
| 3. Constrain | Same sprint | Add a static-analysis rule (PHPStan/Larastan custom rule or a simple grep gate in CI) blocking new getenv() in app/. Add an allowlist for the wrapper. |
CI fails on violation. |
| 4. Evaluate | End of sprint | Compare defect rate and developer feedback in the pilot module vs. a control. | Decision: expand, hold, or revert. |
Key architectural recommendation: wrap the dependency. A one-class Environment facade inside our codebase (a) isolates us from upstream abandonment, (b) lets us swap the implementation without touching call sites, (c) gives us a seam for test overrides, and (d) prevents the package's idiosyncratic coercion rules from leaking into our domain. Given 0 dependents and 0 stars, the cost of this indirection is trivial and the insurance is worth it.
config:cache awareness..env loading: the package does not parse .env files. In environments where variables are only in .env and never exported to the process, the package will return null. This must be validated in a real staging container before the pilot, because it is the most common way this adoption fails silently.env() and this package will not agree on "false", "null", "empty", or "1". Any code path where both are read is a latent bug.putenv are process-global and unsafe under --parallel. The wrapper should expose a scoped override with guaranteed restore (try/finally), or the pilot must run in a non-parallel suite.env()" framing drive a performance-motivated adoption — there is no meaningful performance problem to solve here. Overstating this will damage credibility with engineering.getenv()) rather than coexisting with it. If the team will not enforce the static-analysis guard, do not adopt.| Failure mode | Likelihood | Impact | Detection | Mitigation |
|---|---|---|---|---|
Value read via package disagrees with config() after config:cache |
High | High — silent wrong config in prod | Integration test asserting parity for every variable read by both paths | Restrict package to non-config() code paths; enforce via static analysis |
Variable absent from process env (only in .env) → null returned silently |
High | Medium | Staging smoke test in a container that mimics prod env injection | Never default to null without an explicit fallback; require defaults at call sites |
Untyped/coerced value causes subtle bug ("false" truthy) |
Medium | Medium–High | Unit tests per accessor | Wrapper enforces and tests coercion rules explicitly |
| Parallel tests interfere via process-global mutation | Medium | Medium — flaky CI, eroded trust | Intermittent failures under --parallel |
Scoped override in wrapper with guaranteed restore; exclude from parallel suites |
Secrets leaked to child processes / logs via putenv |
Low–Medium | High — compliance exposure | Log scrubbing; subprocess env audit | Policy: read-only usage of this package. Do not use setter functions for secrets |
| Upstream abandoned / repo archived | Low | Low if wrapped; Medium if imported directly | Quarterly dependency review | Wrapper indirection; pin versions |
| Packagist metadata disagrees with repo (future-dated release) | Unknown — must verify | Medium — supply-chain trust | Phase 0 check | No adoption without verification |
Recommended hard policy: permit read-only, typed accessors with mandatory defaults; prohibit environment mutation outside tests. Mutation is where the risk concentrates (globals, child processes, secrets, test pollution) and where the package adds the least value.
config() equivalent). Codify this in a short ADR and a one-paragraph CONTRIBUTING note, or the distinction will erode within two sprints.config(), env(), $_ENV, and now this). Document the decision tree in the ADR.Conditional pilot, not framework-wide adoption.
getenv(), and consistent Packagist metadata.config() equivalent.How can I help you explore Laravel packages today?