Summary
Split the current static Di class into two parts:
DiContainer — an instance-based container that holds all dependency state ($dependencies, $container, $resolving)
Di — a static facade that delegates all calls to the "current" DiContainer instance
This enables each application execution to own its own isolated DI container, which is the foundation for true execution isolation.
Parent Issue
Subtask of #373 (Refactor App Bootstrapping & DI Ownership Model).
Depends on: #454 (HttpRequest/HttpResponse must be instance-based first, otherwise static HTTP state still leaks regardless of container isolation).
Problem
Today, Di is entirely static:
class Di
{
private static array $dependencies = [];
private static array $container = [];
private static array $resolving = [];
// all methods are static
}
There is exactly one global container for the entire process. Di::reset() clears it, but you cannot have two containers coexisting. This means:
- Two application executions cannot be alive simultaneously
- The "execution boundary" (
Di::reset()) is destructive — it doesn't create a new scope, it destroys the only one
Proposed Design
DiContainer (new class)
All current Di state and logic moves into an instance-based class:
namespace Quantum\Di;
class DiContainer
{
private array $dependencies = [];
private array $container = [];
private array $resolving = [];
public function register(string $concrete, ?string $abstract = null): void { /* ... */ }
public function get(string $dependency, array $args = []) { /* ... */ }
public function create(string $dependency, array $args = []) { /* ... */ }
public function set(string $abstract, object $instance, bool $override = true): void { /* ... */ }
public function has(string $abstract): bool { /* ... */ }
public function isRegistered(string $abstract): bool { /* ... */ }
public function registerDependencies(array $dependencies): void { /* ... */ }
public function autowire(callable $entry, array $args = []): array { /* ... */ }
public function reset(): void { /* ... */ }
public function resetContainer(): void { /* ... */ }
// all private methods: resolve(), instantiate(), resolveParameters(), etc.
}
Di (refactored to static facade)
Di keeps its existing static API but delegates to a current DiContainer instance:
namespace Quantum\Di;
class Di
{
private static ?DiContainer $current = null;
public static function setCurrent(DiContainer $container): void
{
self::$current = $container;
}
public static function getCurrent(): DiContainer
{
return self::$current;
}
public static function get(string $dependency, array $args = [])
{
return self::$current->get($dependency, $args);
}
public static function set(string $abstract, object $instance, bool $override = true): void
{
self::$current->set($abstract, $instance, $override);
}
// ... all other methods delegate to self::$current
}
Backward Compatibility
Zero breaking changes. Every existing call site (Di::get(), Di::set(), Di::register(), Di::autowire(), etc.) continues to work identically. The static API is preserved — only the internal implementation changes.
Integration with AppContext
After this split, AppFactory creates a fresh DiContainer per execution:
private static function createInstance(string $type, string $baseDir): App
{
$container = new DiContainer();
Di::setCurrent($container);
$context = new AppContext($type, $container);
App::setBaseDir($baseDir);
return new App(new $adapterClass());
}
AppContext holds a reference to its own container:
class AppContext
{
private string $mode;
private DiContainer $container;
public function __construct(string $mode, DiContainer $container)
{
// ...
$this->mode = $mode;
$this->container = $container;
}
public function getContainer(): DiContainer
{
return $this->container;
}
}
Each AppContext owns its DiContainer. Switching the active app means calling Di::setCurrent($context->getContainer()).
Tasks
Acceptance Criteria
- All existing
Di:: static calls work without modification
DiContainer can be instantiated independently (useful for testing)
- Each
AppFactory::createInstance() produces an isolated container
- All 1227+ existing tests pass
- PHPStan passes
Files Affected
src/Di/Di.php — refactored to facade
src/Di/DiContainer.php — new file, extracted logic
src/App/Factories/AppFactory.php — creates and sets container
tests/Unit/Di/DiTest.php — updated/expanded
- No other files should need changes (that's the point of the facade pattern)
Summary
Split the current static
Diclass into two parts:DiContainer— an instance-based container that holds all dependency state ($dependencies,$container,$resolving)Di— a static facade that delegates all calls to the "current"DiContainerinstanceThis enables each application execution to own its own isolated DI container, which is the foundation for true execution isolation.
Parent Issue
Subtask of #373 (Refactor App Bootstrapping & DI Ownership Model).
Depends on: #454 (HttpRequest/HttpResponse must be instance-based first, otherwise static HTTP state still leaks regardless of container isolation).
Problem
Today,
Diis entirely static:There is exactly one global container for the entire process.
Di::reset()clears it, but you cannot have two containers coexisting. This means:Di::reset()) is destructive — it doesn't create a new scope, it destroys the only oneProposed Design
DiContainer (new class)
All current
Distate and logic moves into an instance-based class:Di (refactored to static facade)
Dikeeps its existing static API but delegates to a currentDiContainerinstance:Backward Compatibility
Zero breaking changes. Every existing call site (
Di::get(),Di::set(),Di::register(),Di::autowire(), etc.) continues to work identically. The static API is preserved — only the internal implementation changes.Integration with AppContext
After this split,
AppFactorycreates a freshDiContainerper execution:AppContextholds a reference to its own container:Each
AppContextowns itsDiContainer. Switching the active app means callingDi::setCurrent($context->getContainer()).Tasks
DiContainerclass with all instance-based logic (extracted fromDi)Dito static facade delegating toDiContainer::$currentDiContainerinDi(so existing code works without explicitsetCurrent)AppFactory::createInstance()to create and set a newDiContainerDi::reset()— either resets the current container or creates a new oneDiTestto cover bothDiContainerdirectly andDifacadeAcceptance Criteria
Di::static calls work without modificationDiContainercan be instantiated independently (useful for testing)AppFactory::createInstance()produces an isolated containerFiles Affected
src/Di/Di.php— refactored to facadesrc/Di/DiContainer.php— new file, extracted logicsrc/App/Factories/AppFactory.php— creates and sets containertests/Unit/Di/DiTest.php— updated/expanded