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
- FQCN:
Scriptor\Boot\App - Source:
boot/App.php
When to use
Two legitimate use sites in current Scriptor:
- A theme's
_ext.phpwhen it instantiates aSitesubclass. The subclass's constructor takes aContaineras its first argument;App::container()is how the include obtains one. - A plugin's setup script run from Composer (e.g.
composer.jsonscripts.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.phpreaches forApp::container()Editor: same constructor shape, also called from bootPluginManager: a service most reachable scripts pull out of the container;clearCache()is the canonical post-install actionPluginContext:container()here is the per-plugin path to the same container, without the static global