Summary
Expand AppContext from a minimal mode-only object into the true execution state holder. AppContext should represent the full runtime identity of a single application execution, with typed accessors that delegate to its own DiContainer.
Parent Issue
Subtask of #373 (Refactor App Bootstrapping & DI Ownership Model).
Depends on:
Current State
AppContext only holds $mode:
class AppContext
{
private string $mode;
// that's it
}
All execution state is scattered: Config in DI, Request/Response in static properties, Environment in a static singleton, routes in DI, console app on the adapter instance. There's no single object that represents "this execution."
Proposed Design
AppContext delegates to its own DiContainer — no dual registration, no duplicated state:
class AppContext
{
private string $mode;
private string $baseDir;
private DiContainer $container;
public function __construct(string $mode, string $baseDir, DiContainer $container)
{
$this->mode = $mode;
$this->baseDir = $baseDir;
$this->container = $container;
}
public function getMode(): string { return $this->mode; }
public function getBaseDir(): string { return $this->baseDir; }
public function getContainer(): DiContainer { return $this->container; }
public function getEnvironment(): Environment
{
return $this->container->get(Environment::class);
}
public function getConfig(): Config
{
return $this->container->get(Config::class);
}
public function getRequest(): Request
{
return $this->container->get(Request::class);
}
public function getResponse(): Response
{
return $this->container->get(Response::class);
}
public function getRoutes(): RouteCollection
{
return $this->container->get(RouteCollection::class);
}
public function getConsoleApp(): Application
{
return $this->container->get(Application::class);
}
}
Tasks
Core changes
Environment migration
App::$baseDir migration
AppFactory update
Optional: update boot stages
What does NOT change
- Helper functions (
config(), cookie(), server(), etc.) — still use Di::get() internally
- Factories — still use
Di::get(self::class)->resolve()
- Controller autowiring — still works through
Di::autowire()
$_ENV superglobal — stays process-level (but Environment::$envContent is per-execution)
Acceptance Criteria
- AppContext holds mode, baseDir, and a DiContainer reference
- Typed accessors delegate to the container (single source of truth, no dual registration)
Environment is no longer a static singleton — it's DI-managed and execution-scoped
App::$baseDir is no longer static — it's stored in AppContext
- All existing tests pass
- PHPStan passes
Context
This completes the #373 vision: "AppContext represents the runtime identity of a single application execution." After this ticket, each application execution is a self-contained AppContext with its own DiContainer, Environment, Config, Request, Response, and routes. No static state leaks between executions.
Summary
Expand
AppContextfrom a minimal mode-only object into the true execution state holder. AppContext should represent the full runtime identity of a single application execution, with typed accessors that delegate to its ownDiContainer.Parent Issue
Subtask of #373 (Refactor App Bootstrapping & DI Ownership Model).
Depends on:
Current State
AppContextonly holds$mode:All execution state is scattered: Config in DI, Request/Response in static properties, Environment in a static singleton, routes in DI, console app on the adapter instance. There's no single object that represents "this execution."
Proposed Design
AppContext delegates to its own DiContainer — no dual registration, no duplicated state:
Tasks
Core changes
$baseDirto AppContext (move from staticApp::$baseDir)DiContainerreference to AppContext (from Split Di into static facade + instance-based DiContainer #455)getEnvironment(),getConfig(),getRequest(),getResponse(),getRoutes(),getConsoleApp()Di::get(AppContext::class)Environment migration
Environment::$instancestatic singleton andgetInstance().env)LoadEnvironmentStageto create and register Environment in DIenv()helper to resolve from DI instead ofEnvironment::getInstance()App::$baseDir migration
App::$baseDirbaseDirin AppContextApp::getBaseDir()to resolve fromDi::get(AppContext::class)->getBaseDir()base_dir()helper if one existsAppFactory update
DiContainer+AppContexttogether increateInstance()Di::setCurrent()Optional: update boot stages
$context->getConfig()instead ofDi::get(Config::class)for clarityWhat does NOT change
config(),cookie(),server(), etc.) — still useDi::get()internallyDi::get(self::class)->resolve()Di::autowire()$_ENVsuperglobal — stays process-level (butEnvironment::$envContentis per-execution)Acceptance Criteria
Environmentis no longer a static singleton — it's DI-managed and execution-scopedApp::$baseDiris no longer static — it's stored in AppContextContext
This completes the #373 vision: "AppContext represents the runtime identity of a single application execution." After this ticket, each application execution is a self-contained
AppContextwith its ownDiContainer,Environment,Config,Request,Response, and routes. No static state leaks between executions.