sebastian/environment
A small PHP utility for writing code with runtime-specific execution paths. Detect the PHP or HHVM version, check whether code is running in the console versus the web, and branch accordingly. Handy for test tooling and library internals.
sebastian/environment is a low-level runtime introspection utility, not an application-layer component. That defines where it fits and where it does not.
Fits well:
Runtime::getCurrentSettings(), Runtime::isOpcacheActive(), Runtime::canCollectCodeCoverage(), and Console::hasColorSupport() are genuinely useful for a /health/runtime or artisan about-style command, and the 9.3.x line fixed real round-trip bugs (php -d quoting, empty-string overrides for php.ini-only extensions) that make those outputs trustworthy.hasXdebug(), hasPCOV(), isOpcacheActive(), non-TTY console behavior) without hand-rolled PHP_VERSION parsing.canCollectCodeCoverage() is the canonical answer and already encodes Xdebug mode handling (fixed in 7.2.1/8.0.1), which teams routinely get wrong by hand.Does not fit:
Runtime::isPhp81(), no isLinux(), no hasExtension(). Those appear in earlier draft assessments in this record and are not part of the shipped API. Building product behaviour on a facade like Environment::isPhp80() means we write and maintain that adapter ourselves — the value proposition shifts from "adopt a package" to "adopt a package plus a thin internal abstraction."app()->environment(). Do not let the naming collision confuse the team; enforce that distinction in code review.isHHVM() and the HHVM branches are legacy surface. HHVM is not a realistic deployment target for Laravel 10+. Any migration effort justified by "HHVM support" should be declined.composer require --dev sebastian/environment. No service provider, no config publish, no migrations. It is almost certainly already in our vendor/ tree transitively via PHPUnit, so the real decision is whether to promote it to a declared direct dependency (yes, for anything we import in our own code) and which constraint to pin.RuntimeInfo wrapper binding Runtime and Console as singletons, exposing the four or five methods we standardize on, is a half-day of engineering plus tests. That is the actual delivery item, not the Composer line.Primary risk — transitive version lock with PHPUnit. This is the only risk that can break a build:
^9 directly while PHPUnit pins ^8, Composer resolves to the intersection or fails. The safe default is to match the constraint PHPUnit already imposes rather than to introduce an independent one.composer.lock for the resolved version before proposing any constraint.Secondary risks:
Runtime::getBinary() and getRawBinary() are deprecated in favour of escapeshellarg(PHP_BINARY) and the PHP_BINARY constant. Any adapter must be written against the resolved version's actual method set, not against a blog post.Environment::detect(), Runtime::isLinux(), Runtime::isPhpVersion(), "zero-cost in production" for a package that shells out in getCurrentSettings()). Any plan derived from those drafts must be re-validated against the installed source before estimation.require-dev removal.version_compare calls"? Without one, this is a nice-to-have and should not consume a sprint.RuntimeInfo facade, it becomes internal API with its own versioning and docs. Is that accepted, or do we mandate direct Runtime usage and skip the abstraction?composer.lock already resolve to, and does promoting this to a direct dependency change the resolution graph at all? Answer this before committing to a major.platform constraint instead? For "require PHP 8.2+", Composer's config.platform and require.php already fail the install, which is earlier and cheaper than a runtime exception in CI. We should be clear that this package is for introspection, not for enforcement.sebastian/environment majors in one matrix? If yes, we cannot declare a single wide constraint.about command), this must be a require, not require-dev — and that changes the deployment story.require-dev, consumed by PHPUnit bootstrap, tests/, and CI scripts. The exception is a runtime diagnostics endpoint, which pulls it into require.composer.json platform constraints remain the enforcement mechanism; Laravel's app()->environment() remains the tier mechanism; sebastian/environment is the introspection layer underneath both.PHP_VERSION, version_compare, phpversion(), defined('HHVM_VERSION'), extension_loaded(, ini_get('opcache, getenv('FORCE_COLOR'), posix_isatty. Classify each hit as test, CI script, diagnostics, or request-path. The request-path bucket is the one that determines scope.composer.lock; confirm which major is present; confirm whether promoting to a direct dependency is a no-op. Record the PHP↔PHPUnit↔environment major matrix in the ADR.RuntimeInfo wrapper against the resolved version. This is where fabricated API assumptions surface cheaply.version_compare(PHP_VERSION, ...) in tests/ and app/, with the approved replacement referenced in the message.app()->environment() selects config; RuntimeInfo only describes the runtime."getBinary() and getRawBinary() from the adapter. Use PHP_BINARY and escapeshellarg(PHP_BINARY) directly.Runtime::getCurrentSettings() spawns a child process on older PHP (9.3.3 removed that on PHP 8.6+). Never call it on a request path.| Phase | Scope | Risk | Est. |
|---|---|---|---|
| 0 | ADR: scope decision, version matrix, adapter-vs-direct call | none | 1 day |
| 1 | Promote to declared require-dev with the lock-resolved constraint |
low | 2 hrs |
| 2 | CI precondition step (fail fast, clear message) | low | ½ day |
| 3 | Test guards: coverage policy via canCollectCodeCoverage(), TTY/colour via Console |
low | 1 day |
| 4 | RuntimeInfo adapter + artisan runtime:inspect diagnostics command |
medium | 2–3 days |
| 5 | (Optional, gated) request-path replacement of manual version checks | medium | 2–5 days |
| 6 | Lint rule + docs | low | 1 day |
Phase 0 is the gate. If Phase 0 concludes "no diagnostics need, CI gate can be a Composer platform constraint," the correct outcome is to stop after Phase 2 and record the decision — not to proceed for consistency's sake.
RuntimeInfo wrapper, its tests, and its docs are ours to maintain forever. Budget for it explicitly or don't build it.PHP_BINARY, extension_loaded, and ini_get in an afternoon.getBinary()/getRawBinary() are already deprecated. Our adapter must not use them, or we inherit a future breaking change for no benefit.getCurrentSettings(). It can spawn a child process (pre-8.6) and shells out. At 10 RPS that is 10 subprocesses per second. Hard rule: diagnostics-only, invoked from a CLI command or a rate-limited health endpoint, never from middleware or a controller on a hot route.| Failure | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Composer constraint conflict with PHPUnit | Medium | Build-blocking, blocks all deploys | Match the lock-resolved constraint; never invent a wider one; validate in a throwaway branch first |
Subprocess spawn in a hot path (getCurrentSettings() misused) |
Medium | Latency spike, process-table exhaustion under load | Ban via middleware allowlist; lint rule; code review checklist |
| Adapter built against non-existent methods (per earlier drafts) | Medium-High if drafts are trusted | Wasted sprint, late discovery, credibility damage | Phase 0 prototype against installed source; delete fabricated API from the record |
| PHP major upgrade breaks the constraint silently | Medium | CI green locally, red in prod image | Pin config.platform.php; include the package in the PHP-upgrade runbook |
| Upstream abandonment | Low | Dev-only inconvenience | Documented 1-day replacement path in the ADR |
| Non-TTY warnings on colour detection | Low | Log noise in CI/containers | Fixed upstream (9.0.1/8.0.4); stay on a patched version |
Env-var-driven behaviour surprises (FORCE_COLOR) |
Low | Cosmetic, hard to reproduce | Document that FORCE_COLOR now affects output; set explicitly in CI |
platform. Which config tier? → app()->environment(). What is the runtime actually doing? → RuntimeInfo. Name collision between app()->environment() and this package is the single most likely source of misuse — address it with naming (RuntimeInfo, not Environment) and one handbook paragraph.version_compare(PHP_VERSION in tests/ or app/; no getCurrentSettings() in a request path; no getBinary()/getRawBinary(); adapter methods exist in the locked version.artisan runtime:inspect command. It makes the adoption tangible to the whole team in one review, rather than an invisible composer.json diff.TPM recommendation: approve Phases 0–3 (declared dev dependency, CI gate, test guards) as a small, low-risk hygiene item; treat Phases 4–5 as a separate, evidence-gated decision. Do not approve any plan that depends on the facade or detection methods described in the earlier draft assessments in this record — those must be re-validated against the installed source before estimation, and the HHVM angle should be dropped entirely.
How can I help you explore Laravel packages today?