Skip to content

Expand AppContext as execution state holder #456

@armanist

Description

@armanist

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

  • Add $baseDir to AppContext (move from static App::$baseDir)
  • Add DiContainer reference to AppContext (from Split Di into static facade + instance-based DiContainer #455)
  • Add typed accessor methods: getEnvironment(), getConfig(), getRequest(), getResponse(), getRoutes(), getConsoleApp()
  • Register AppContext itself in DI so it's accessible via Di::get(AppContext::class)

Environment migration

  • Remove Environment::$instance static singleton and getInstance()
  • Make Environment DI-managed (one instance per execution, loaded from that execution's .env)
  • Update LoadEnvironmentStage to create and register Environment in DI
  • Update env() helper to resolve from DI instead of Environment::getInstance()

App::$baseDir migration

  • Remove static App::$baseDir
  • Store baseDir in AppContext
  • Update App::getBaseDir() to resolve from Di::get(AppContext::class)->getBaseDir()
  • Update base_dir() helper if one exists

AppFactory update

  • Create DiContainer + AppContext together in createInstance()
  • Set container as current via Di::setCurrent()
  • Register AppContext in its own container

Optional: update boot stages

  • Boot stages already receive AppContext as parameter — optionally update them to use $context->getConfig() instead of Di::get(Config::class) for clarity

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions