Purpose

A single static accessor for the DI container, so legacy code and bootstrap files that cannot easily be threaded with a constructor argument can still reach the container. In a normal request, exactly one place reaches for it: a theme's _ext.php that needs to instantiate the theme's Site subclass with the container as its first constructor argument.

The class is transitional. It exists so the Phase 14a container migration could ship without having to thread the container through every legacy callsite in one PR. Each subsequent sub-phase (14b–14c) replaces an App::container() call-site with explicit DI, and the locator shrinks.

In your own code, prefer constructor injection. Reach for App::container() only when something genuinely cannot accept a constructor argument (a global include file, a top-level script, a class whose constructor signature is frozen by an upstream framework).

FQCN + file path

When to use

Two legitimate use sites in current Scriptor:

  • A theme's _ext.php when it instantiates a Site subclass. The subclass's constructor takes a Container as its first argument; App::container() is how the include obtains one.
  • A plugin's setup script run from Composer (e.g. composer.json scripts.post-install-cmd) that needs to clear the plugin discovery cache or run a one-off migration. Boot Scriptor, reach for the container, do the work, exit.

Outside those two shapes, prefer constructor injection. Themes that subclass Site already get the container as $this->container because Site::__construct() stores it; modules get it through their factory's first argument; plugins get it through $context->container(). None of those need App::container().

Surface

public static function set(Container $container): void

Stores the container in the static slot. Called once from boot.php during the application bootstrap. You do not call this yourself.

public static function container(): Container

Returns the stored container. Throws RuntimeException if set() has not been called yet (typical message: "iManager container has not been booted yet. Did boot.php run?"). The exception is a defensive guard against calling the locator from a script that bypassed boot.php.

public static function reset(): void

Clears the static slot. Test-only: lets a test suite swap the container between cases without leaking state. Production code has no reason to call this.

private function __construct()

Private constructor: the class is uninstantiable on purpose. Only the static methods are part of the surface.

Lifecycle

Process-wide static state. One slot, set once per request by boot.php (after the container is wired), read by whoever needs the container, cleared only by tests.

The "process-wide" framing is the source of every concern about service locators: it makes the container reachable from any scope, which makes it tempting to use as a hidden dependency instead of an explicit one. The Scriptor codebase treats it as a migration aid, not a primary pattern; new code should not introduce new App::container() callsites.

Common patterns

Theme _ext.php that bootstraps a Site subclass

<?php

declare(strict_types=1);

$site = new \MyTheme\Site(
    \Scriptor\Boot\App::container(),
    $config,
    dirname(__DIR__, 2),
);
$site->execute();

This is the canonical and accepted use. The bundled basic theme's _ext.php does the same thing.

Composer post-install script that clears the plugin cache

// scripts/postinstall.php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

// Whatever your own boot helper is. The point: after boot,
// the container is reachable as App::container().
\Scriptor\Boot\App::set(\MyApp\Bootstrap::container());

\Scriptor\Boot\App::container()
    ->get(\Scriptor\Boot\Plugin\PluginManager::class)
    ->clearCache();

Test-suite cleanup between cases

protected function tearDown(): void
{
    \Scriptor\Boot\App::reset();
    parent::tearDown();
}

What not to do

// Anti-pattern: hiding a dependency behind the locator.
final class MyModule
{
    public function execute(): void
    {
        // Don't. The module factory already received the container
        // explicitly; ignoring it makes the dependency invisible.
        $logger = \Scriptor\Boot\App::container()->get(\Psr\Log\LoggerInterface::class);
        // ...
    }
}

The module's factory signature is function(Container, Editor): Module. Take the container in the constructor (or take the specific service directly) and use that. The locator route works but obscures what MyModule depends on.

See also

  • Site: the constructor that takes the container as its first argument; the place a theme's _ext.php reaches for App::container()
  • Editor: same constructor shape, also called from boot
  • PluginManager: a service most reachable scripts pull out of the container; clearCache() is the canonical post-install action
  • PluginContext: container() here is the per-plugin path to the same container, without the static global