Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Environment Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Check whether you already have it. Any Laravel app with PHPUnit installed already ships this package transitively — phpunit/phpunit requires sebastian/environment, and it ends up on the coverage/console plumbing path. Confirm with:

composer show sebastian/environment

2. Declare it explicitly. If you're going to call it from your own code, add it yourself so the dependency isn't accidental:

composer require --dev sebastian/environment   # tests, CI scripts, local tooling
composer require sebastian/environment         # only if app/runtime code uses it in production

That --dev distinction matters more than usual here (see Gotchas).

3. Read two classes first. The whole package is essentially:

  • SebastianBergmann\Environment\Runtime — PHP version, SAPI-level facts, coverage drivers, OPcache, INI settings.
  • SebastianBergmann\Environment\Console — TTY detection, terminal width/height, color support.

Everything else (Host, added in 9.1.0) is incidental. Start with those two files in vendor/sebastian/environment/src/.

4. First real use case: stop guessing why --coverage fails in CI. This is the single most common day-to-day win in a Laravel repo:

php artisan test --coverage
# "No code coverage driver available" — or worse, silent 0% output

Instead of hardcoding "make sure Xdebug is on" in a wiki page, ask the package:

use SebastianBergmann\Environment\Runtime;

$runtime = new Runtime();

if (! $runtime->canCollectCodeCoverage()) {
    // fail fast with a useful message instead of a broken report
}

canCollectCodeCoverage() accounts for Xdebug mode (since 7.2.1/8.0.1 — xdebug.mode=off correctly reports false), PCOV, and PHPDBG, which is exactly the set of conditions that trip people up when a CI image "has Xdebug" but nothing gets measured.


Implementation Patterns

Pattern 1: Bind it once, wrap it thinly

Don't sprinkle new Runtime() through controllers and providers. Register a singleton and put your app's policy on top of it:

// app/Providers/AppServiceProvider.php
use SebastianBergmann\Environment\Runtime;

public function register(): void
{
    $this->app->singleton(Runtime::class, fn () => new Runtime());
}
// app/Support/Platform.php
namespace App\Support;

use SebastianBergmann\Environment\Runtime;

final readonly class Platform
{
    public function __construct(private Runtime $runtime) {}

    public function phpVersionId(): int
    {
        return $this->runtime->getVersionId();
    }

    public function atLeast(int $versionId): bool
    {
        return $this->runtime->getVersionId() >= $versionId;
    }

    public function canMeasureCoverage(): bool
    {
        return $this->runtime->canCollectCodeCoverage();
    }
}

The wrapper is where your support policy lives (atLeast(80300) etc.), so the day you drop PHP 8.3 you edit one file — not forty version_compare() calls.

Pattern 2: Runtime-gated tests instead of silently skipped ones

The idiomatic use inside a Laravel test suite is a precondition check that explains itself:

use SebastianBergmann\Environment\Runtime;

protected function requireCoverageDriver(): void
{
    static $runtime = new Runtime();

    if (! $runtime->canCollectCodeCoverage()) {
        $this->markTestSkipped(
            'No coverage driver: enable PCOV, or Xdebug with xdebug.mode=coverage.'
        );
    }
}

Pest equivalent:

it('renders the fiber-based scheduler', function () {
    // ...
})->skip(
    fn () => ! (new Runtime())->isXdebugActive(),
    'Requires an active Xdebug session for tracing.'
);

Use the specific predicate (isXdebugActive() vs hasXdebug()) in the skip reason. hasXdebug() means "extension loaded", isXdebugActive() means "mode is not off" — the distinction explains 90% of "works on my machine" coverage tickets.

Pattern 3: An app:runtime Artisan command as your diagnostic surface

A tiny command pays for itself the first time a teammate or CI runner misbehaves. It's also a good regression canary when you bump PHP versions:

final class RuntimeInfoCommand extends Command
{
    protected $signature = 'app:runtime {--json : Output as JSON}';

    public function handle(Runtime $runtime, Console $console): int
    {
        $data = [
            'name'          => $runtime->getName(),          // "PHP"
            'version'       => $runtime->getVersion(),
            'version_id'    => $runtime->getVersionId(),
            'binary'        => PHP_BINARY,
            'xdebug_loaded' => $runtime->hasXdebug(),
            'xdebug_active' => $runtime->isXdebugActive(),
            'pcov_loaded'   => $runtime->hasPCOV(),
            'pcov_active'   => $runtime->isPCOVActive(),
            'coverage'      => $runtime->canCollectCodeCoverage(),
            'opcache'       => $runtime->isOpcacheActive(),
            'interactive'   => $console->isInteractive(),
            'columns'       => $console->getNumberOfColumns(),
        ];

        $this->option('json')
            ? $this->line(json_encode($data, JSON_PRETTY_PRINT))
            : $this->table(['Key', 'Value'], collect($data)->map(fn ($v, $k) => [$k, var_export($v, true)])->values()->all());

        return self::SUCCESS;
    }
}

Pattern 4: Mirroring INI settings into subprocesses

The genuinely under-used feature. When you spawn a PHP child process (parallel test shards, a sandboxed artisan invocation, a worker using Process), you usually want it to inherit the current runtime configuration — but only the settings that are legal on the command line:

use Symfony\Component\Process\Process;

$runtime   = app(Runtime::class);
$current   = $runtime->getCurrentSettings();               // ['memory_limit' => '-1', ...]
$immutable = $runtime->getSettingsNotChangeableAtRuntime(); // e.g. php.ini-only directives

$flags = [];
foreach ($current as $key => $value) {
    if (in_array($key, $immutable, true)) {
        continue;
    }
    $flags[] = '-d';
    $flags[] = "{$key}={$value}";
}

$process = new Process([PHP_BINARY, ...$flags, base_path('artisan'), 'migrate', '--force']);
$process->mustRun();

This is the same machinery PHPUnit uses internally, and it is why the 8.1.1/9.3.1 quoting fix and the 9.3.2 empty-string-override fix exist: values must survive the php -d key=value round-trip. If you build -d strings by hand, you will hit exactly those bugs.

Pattern 5: Terminal shape for custom output

Inside Artisan commands, prefer Symfony's $this->output->getFormatter() / isDecorated() — you already have it. Reach for Console when you're in a non-Symfony context: a plain bin/ script, a composer script, a pre-commit hook, a custom PHPUnit printer.

$console = new Console();

$width = max(40, $console->getNumberOfColumns() - 4);
$color = $console->hasColorSupport();

hasColorSupport() is aligned with Symfony's StreamOutput::hasColorSupport() since 7.2.0 and honours FORCE_COLOR (9.2.0) and NO_COLOR, so behaviour matches what devs see in artisan output. If you're rendering inside Artisan, don't re-implement it — you'll create two different colour policies.

Pattern 6: CI guard as a Composer script, not a middleware

Runtime validation belongs in the pipeline, not in a request path:

"scripts": {
  "check-runtime": "@php -r \"require 'vendor/autoload.php'; \\$r = new SebastianBergmann\\Environment\\Runtime(); exit(\\$r->getVersionId() >= 80300 ? 0 : 1);\"",
  "test": ["@check-runtime", "@php artisan test"]
}

Warn in local (log a Log::warning from a service provider), fail hard in ci. A middleware that inspects PHP version on every request is pure overhead for a value that can't change mid-process.


Gotchas and Tips

It is not app()->environment(). Laravel's App::environment() answers "am I in local / staging / production". This package answers "which PHP am I, what can it do, and what terminal am I attached to". They're orthogonal; naming a facade Environment (a common suggestion) creates a real collision of meaning. Call the wrapper Platform, PhpRuntime, or RuntimeInfo.

--no-dev will break production use. This is the most common Laravel-specific footgun. Because it usually arrives via PHPUnit, it lands in require-dev. If you start calling it from a controller or provider and deploy with composer install --no-dev, the class vanishes at runtime. Either declare it in require, or keep it strictly to tests/CI/Artisan-locally.

There is no isPhp81() / isPhpVersion('8.1') / isLinux() / hasExtension(). Those helpers appear in a lot of AI-generated advice but are not the API. What actually exists is getVersionId() (int, comparable directly to PHP_VERSION_ID constants like 80300), getName(), getVersion(), plus the coverage/OPcache predicates and getCurrentSettings()/getSettingsNotChangeableAtRuntime(). For extension checks just use extension_loaded(); wrapping it adds nothing. If you want isPhp81()-style ergonomics, define them yourself in the wrapper.

getCurrentSettings() can spawn a child process. Until PHP 8.6 it shells out to determine built-in default values (see 9.3.3). It is fine in a CLI diagnostic command; never call it per-request. Cache it (Cache::rememberForever) if you need it at runtime.

Keep it current if you round-trip settings. 8.1.1 / 9.3.1 fixed unquoted values breaking php -d, and 9.3.2 fixed empty-string overrides being forwarded for php.ini-only extensions. Old versions silently produce child processes with wrong configuration — a nasty, hard-to-trace class of bug.

getBinary() and getRawBinary() are deprecated. Use escapeshellarg(PHP_BINARY) or the PHP_BINARY constant directly. Same values, one less dependency surface.

isOpcacheActive() reflects the current SAPI. opcache.enable_cli is off by default, so a CLI command will report false on a machine where FPM happily has OPcache on. Don't use it to decide "should I purge the opcache" from an Artisan command without also checking that your CLI has it enabled.

canCollectCodeCoverage() is not "coverage will produce a report". It only proves a driver is present and enabled. The other two failure modes — no <source> filter configured, and pcov clashing with xdebug both loaded — are outside this package's scope.

Prefer Symfony Console inside Artisan. Console::hasColorSupport() and $this->output->isDecorated() can diverge in edge cases (piping, NO_COLOR, FORCE_COLOR) and having two colour policies in one command is a support burden. Historically there were also warnings in non-TTY environments (fixed in 8.0.4 / 9.0.1) — another reason to keep the dependency patched if you use it in scripts that run under cron or CI without a TTY.

Version pinning is dictated by your PHP floor. Each major drops the previous PHP minor (8.0.0 dropped 8.2; 9.0.0 dropped 8.3; 9.3.3 already handles PHP 8.6). On a Laravel app pinned to older PHP, Composer will resolve an older major for you — don't fight it, and don't copy snippets that use getSettingsNotChangeableAtRuntime() (9.3.0+) into a project stuck on 8.x.

HHVM is vestigial. The README still says "HHVM/PHP", but 7.2.0 removed code left over from when HHVM was supported. getName() will effectively always return PHP. Don't design a deprecation-warning system around isHHVM() in 2026 — there is no HHVM deployment to warn.

When not to use it. If all you need is a version check, PHP_VERSION_ID is a constant — zero dependency, zero indirection. This package earns its place for the things you'd otherwise get wrong by hand: coverage-driver detection with Xdebug mode awareness, INI-setting round-tripping into subprocesses, and TTY/width detection outside Symfony Console.

Extension points are deliberately absent. There's no plugin surface, no config file, no container hooks — the classes are final-ish, side-effect-free readers of process state. If you need testability, inject the small wrapper interface from Pattern 1 and fake that; don't try to mock Runtime itself, since it reads real globals and constants that PHPUnit-style mocks can't meaningfully control.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table