Skip to content

Refactor: split static resources from per-request context#254

Merged
loks0n merged 2 commits intomainfrom
feat-resources-and-context
May 5, 2026
Merged

Refactor: split static resources from per-request context#254
loks0n merged 2 commits intomainfrom
feat-resources-and-context

Conversation

@loks0n
Copy link
Copy Markdown
Contributor

@loks0n loks0n commented May 5, 2026

Summary

Reworks how DI containers are exposed by the HTTP layer. The previous single getContainer() API conflated two distinct lifetimes — long-lived "boot the server" wiring and short-lived per-request state. This PR makes the split explicit:

  • resources() — static container, shared across every request for the lifetime of the server. Use this at boot to register config, clients, and shared services.
  • context() — per-request child container, created fresh on each incoming request. Lookups fall through to resources(), so static services remain reachable from request handlers. The library writes request, response, route, and error into this container.

The internal coroutine context key is now __utopia__.

Breaking changes

Adapter

Before After
getContainer(): Container removed — replaced by the two methods below
resources(): Container
context(): Container

Both new methods are abstract and documented on Utopia\Http\Adapter. All three bundled adapters (FPM, Swoole, SwooleCoroutine) implement both.

Adapter constructors

The container parameter has been renamed from $container to $resources and is now a constructor-promoted property on every adapter:

// FPM
new FPM\Server(Container $resources)

// Swoole
new Swoole\Server(
    string $host,
    ?string $port = null,
    array $settings = [],
    int $mode = SWOOLE_PROCESS,
    Container $resources = new Container(),
)

// SwooleCoroutine
new SwooleCoroutine\Server(
    string $host,
    ?string $port = null,
    array $settings = [],
    Container $resources = new Container(),
)

If you were passing the container positionally to the Swoole adapters this still works. If you were passing it by name, rename container:resources:.

Http

Before After
new Http(Adapter $server, string $timezone, ?Container $container = null) new Http(Adapter $adapter, string $timezone, Files $files = new Files())
Http::getResource(string $name): mixed removed — use $http->context()->get($name)
Http::getResources(array $list): array removed — call $http->context()->get(...) per name
Http::setResource(string $name, callable $cb, array $injections = []) removed — use $http->resources()->set(...)
Http::resources(): Container (shortcut to $adapter->resources())
Http::context(): Container (shortcut to $adapter->context())

The Http constructor no longer accepts a container — pass it to the adapter instead. The internal $server field has been renamed to $adapter to match the parameter type.

Http::getResource() previously normalized DI errors into Utopia\Http\Exception with a "resource"-flavored message. That normalization is gone; lookups now surface the underlying Utopia\DI exceptions directly.

Migration

// Before
$container = new Container();
$container->set('db', fn() => new Database());

$http = new Http(new Server(), 'UTC', $container);
$http->setResource('cache', fn() => new Cache());
$db = $http->getResource('db');

// After
$resources = new Container();
$resources->set('db', fn() => new Database());

$http = new Http(new Server($resources), 'UTC');
$http->resources()->set('cache', fn() => new Cache());
$db = $http->context()->get('db'); // falls through to resources

Non-breaking cleanup

  • Http constructor uses property promotion for $adapter and $files.
  • Removed the unused Psr\Container\*Exception imports (no more error normalization).
  • README examples updated to the new constructor shapes and $resources naming.
  • Test fixture $this->container renamed to $this->resources.

Test plan

  • composer test passes locally
  • Smoke test FPM adapter against the example app
  • Smoke test Swoole adapter against the example app
  • Verify a downstream consumer (e.g. Appwrite) compiles against the new API and migration steps cover the changes encountered

🤖 Generated with Claude Code

Renames the request-scoped DI container concept to "context" and
splits adapter container access into two methods: resources() (static,
shared across requests) and context() (per-request). Removes the
Http::setResource / getResource / getResources(list) wrappers in favor
of going through resources()/context() directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

k6 benchmark

Throughput Requests Fail rate p50 p95
5667 req/s 396795 0% 7.23 ms 15.13 ms

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

  • Replaces the single getContainer() API with resources() (static, server-lifetime) and context() (per-request child container) across all three adapters and the Http class, correctly making lifetime semantics explicit. The FPM adapter now creates and disposes a genuine child container per onRequest call, addressed cleanly with a finally block.
  • Http::getResource / getResources / setResource / setRequestResource are all removed; callers now use $http->resources()->set(...) and $http->context()->get(...) directly, surfacing raw Utopia\\DI exceptions instead of the previously normalised Http\\Exception wrappers — downstream error-handling code that caught those normalised messages will need updating.
  • Two concerns noted in prior review threads are still present in the merged code: the coroutine context key shortened to __utopia__ (collision risk in multi-component Swoole processes) and a startup-hook exception being written to the long-lived resources() container, where it can leak as a stale error injection into otherwise-successful subsequent requests.

Confidence Score: 4/5

Safe to merge with awareness of two unresolved P1 concerns from prior review threads that are still present in the code

The refactoring is structurally sound and the new lifetime split is correctly implemented in all three adapters. No new P0/P1 issues were found in this pass. The ceiling is 4 because previously-flagged P1 issues — the shortened utopia coroutine context key and the startup error leaking into the long-lived resources container — remain unaddressed in the current diff.

src/Http/Http.php (startup-error path at start() line ~522) and src/Http/Adapter/Swoole/Server.php + src/Http/Adapter/SwooleCoroutine/Server.php (CONTEXT_KEY value)

Important Files Changed

Filename Overview
src/Http/Adapter.php Abstract base class updated: removes getContainer() and adds resources() + context() abstract methods with clear docblock documentation
src/Http/Adapter/FPM/Server.php Correctly introduces a per-request child container in onRequest, cleaned up in finally; context() gracefully falls back to resources() when no request is active
src/Http/Adapter/Swoole/Server.php Splits static resources from per-coroutine request context; CONTEXT_KEY shortened to utopia which increases collision risk vs the previous fully-namespaced key (flagged in a prior thread)
src/Http/Adapter/SwooleCoroutine/Server.php Mirrors Swoole adapter changes; uses same utopia context key; context() lacks the getCid() !== -1 guard present in the Swoole adapter, acceptable because this server is always used inside co\run()
src/Http/Http.php Major refactor: removes getResource/getResources/setResource/setRequestResource, exposes resources() and context() passthroughs; startup-error path still writes to resources() (pre-existing concern flagged in a prior thread)
tests/HttpTest.php Mechanical rename of $container to $resources throughout; tests call run() directly (bypassing onRequest), so per-request values still land on resources() — unchanged from pre-PR behaviour
README.md Documentation updated to reflect new constructor shapes and resources/context naming; examples are consistent with the API changes

Reviews (2): Last reviewed commit: "Chore: apply Rector first-class callable..." | Re-trigger Greptile

Comment thread src/Http/Adapter/FPM/Server.php Outdated
Comment thread src/Http/Adapter/Swoole/Server.php
Comment thread src/Http/Http.php
Comment on lines 519 to 534
@@ -578,7 +534,7 @@ public function start(): void
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Startup error permanently stored in the static resources container

When a server start hook throws, error is set on resources() — the long-lived, shared container. Because context() falls through to resources() on a miss, any subsequent per-request handler that injects error without having encountered its own error will silently receive the stale startup exception. The per-request error handlers (runInternal, execute) do shadow this by calling $this->context()->set('error', ...) on the per-request container, but only when an error actually occurs in that request. A happy-path request that happens to inject error will get the leftover boot failure instead of null or an exception.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@loks0n loks0n force-pushed the feat-resources-and-context branch from cb1912d to d77957d Compare May 5, 2026 14:58
@loks0n loks0n merged commit 3e3b431 into main May 5, 2026
10 checks passed
loks0n added a commit that referenced this pull request May 5, 2026
The Http instance is shared across coroutines, so $this->route and
$this->matchedPath would race the same way Route's mutable fields did.
Store them in the per-request context() container instead, which is
already request-scoped post-#254. getRoute()/setRoute() now read/write
through the context too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant