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

Detects runtime details of the PHP environment, including version, operating system, and whether running under HHVM, so your code can branch into runtime-specific execution paths.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style