sebastian/global-state
Snapshot and restore PHP global state with sebastian/global-state, the standalone component extracted from PHPUnit. Capture globals, superglobals, static properties, and constants for reliable test isolation.
It is almost certainly already installed. sebastian/global-state is a hard dependency of phpunit/phpunit, so check first:
composer show sebastian/global-state # note the resolved version
Only run composer require --dev sebastian/global-state if you intend to own the pin (i.e. you're building a wrapper other teams depend on). Otherwise you inherit it transitively and skip the upgrade-ticket liability.
Two files tell you everything. There is no real docs beyond the README, so read the source:
vendor/sebastian/global-state/src/Snapshot.php — the constructor booleans are the entire configuration surface (constants, functions, classes, interfaces, traits, globalVariables, superGlobalArrays, staticProperties, iniSettings, includedFiles). Toggling these off is how you scope a capture.vendor/sebastian/global-state/src/Restorer.php — the restore methods map 1:1 to those booleans (restoreGlobalVariables(), restoreStaticProperties(), restoreIniSettings(), etc.). You call them explicitly; nothing is automatic.vendor/sebastian/global-state/src/CodeExporter.php — turns values into PHP source. This is your debugging tool, not a runtime feature.The canonical first use case is a leaky legacy test that passes alone and fails in a full run. Prove the leak before adopting anything:
use SebastianBergmann\GlobalState\Snapshot;
use SebastianBergmann\GlobalState\Restorer;
$before = new Snapshot(new CodeExporter, constants: false, functions: false,
classes: false, interfaces: false, traits: false,
globalVariables: true, superGlobalArrays: true,
staticProperties: true, iniSettings: false, includedFiles: false);
// ... run the suspect code ...
(new Restorer($before))->restoreGlobalVariables();
(new Restorer($before))->restoreStaticProperties();
If that fixes the failure, you have a confirmed cause. If it doesn't, close the ticket — you don't have a global-state problem, you have an isolation problem that RefreshDatabase, Facade::clearResolvedInstances(), or a fresh $this->app already covers.
Wrap it, cap it, don't sprinkle it. The package should appear in exactly one file in your repo — a trait in your shared base TestCase, roughly 40–60 lines. Engineers call $this->isolateGlobalState(), never new Snapshot(...).
Use try/finally, not tearDown(). Teardown ordering breaks the moment another team inserts a base class or a trait between you and the framework, and a restore that silently doesn't run is worse than no restore at all:
protected function isolateGlobalState(): void
{
if (! $this->snapshotGlobals ?? true) {
return;
}
$snapshot = new Snapshot(new CodeExporter, /* selective booleans */);
try {
// test body runs in the subclass via setUp/tearDown
} finally {
(new Restorer($snapshot))->restoreGlobalVariables();
(new Restorer($snapshot))->restoreStaticProperties();
}
}
In practice you'll pair this with Laravel's own teardown and pick an explicit order: restore globals after $this->app->flush() and Facade::clearResolvedInstances(), because the container flush itself can write to statics (event dispatcher, facade roots). Snapshot early in setUp — before the test seeds anything — otherwise you're restoring to a polluted baseline.
Default to selective snapshots. Full captures are slow (cost scales with $GLOBALS size × object-graph complexity) and mostly useless in a Laravel suite. The realistic default is globalVariables + superGlobalArrays + staticProperties, everything else off:
new Snapshot(
new CodeExporter,
constants: false, functions: false, classes: false,
interfaces: false, traits: false, iniSettings: false,
includedFiles: false, globalVariables: true,
superGlobalArrays: true, staticProperties: true,
);
includedFiles and functions in particular are almost never what leaked and are the expensive parts.
Exclude by not capturing, not by pruning. Older versions shipped an ExcludeList/Blacklist; recent versions favour the constructor booleans plus opting individual tests out via a property flag (e.g. protected bool $snapshotGlobals = false;) on tests that genuinely need to mutate $_ENV or a static cache. Verify the exclusion mechanism against your resolved version — this is the API that has churned most across majors.
Use CodeExporter as a diagnostic, not a fixture generator. When a test fails and you don't know which variable changed, dump $exporter->export($value) for the suspects and diff it across two snapshots. Handy for a one-off investigation; do not bake exported code into tests.
Non-PHPUnit harness is the strongest fit. If you have a custom CLI harness, an artisan command that loops over jobs, or a long-running worker that needs a deterministic reset between iterations, this is where the package earns its keep — there's no framework teardown to lean on:
while ($job = $queue->pop()) {
$baseline = new Snapshot(new CodeExporter, /* selective */);
try {
$handler->handle($job);
} finally {
(new Restorer($baseline))->restoreGlobalVariables();
(new Restorer($baseline))->restoreStaticProperties();
}
}
Instrument before and after. Run with --order-by=random over repeat runs, record flake rate, adopt, record again. If the delta isn't measurable, revert — this package is not free at the program level once you declare it.
#[AllowDynamicProperties], stdClass casts), closures captured in globals, internal object state, and resources all escape the snapshot. Your test goes green locally and the state leaks in CI. Treat a green run here as evidence, never as proof.$_ENV, $_SERVER, or $_SESSION outside tests/ is a stability and security problem. Enforce with a CI grep or a PHPStan rule if you have src/ in the same repo — a single use SebastianBergmann\GlobalState\Restorer; under app/ is a stop-work signal.$_SESSION, $_COOKIE, $_FILES, and $_REQUEST have semantics beyond array contents; restoring the array doesn't undo what the test did to the session handler or the response headers. Treat them as out of scope rather than assuming a clean rollback.$_ENV/$_SERVER behaviour has changed at patch level. 8.0.1 reworked superglobal snapshot performance and 8.0.2 restored nullable variables in superglobal arrays — so any selective-snapshot array you copy from a blog post needs per-version verification. Same for constant-name escaping, fixed in 8.0.3 / 9.0.1; if you're exporting code for anything containing backslashes or quotes, check the version.Snapshot holds live object references. Long-running suites that snapshot per-test and never release will retain object graphs across runs; scope the snapshot's lifetime to the single test body and let it go out of scope.static readonly properties or typed properties with a null default can throw. If a class has been modernised since the leaky test was written, the restore may fail rather than restore. Wrap restore in a try/catch that fails the test loudly — a swallowed restore error is the worst outcome.tests/.How can I help you explore Laravel packages today?