diff --git a/README.md b/README.md index 585caad..25a00af 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ use Utopia\Http\Request; use Utopia\Http\Response; use Utopia\Http\Adapter\FPM\Server; -$container = new Container(); +$resources = new Container(); Http::get('/hello-world') // Define Route ->inject('request') @@ -46,7 +46,7 @@ Http::get('/hello-world') // Define Route Http::setMode(Http::MODE_TYPE_PRODUCTION); -$http = new Http(new Server(), 'America/New_York', $container); +$http = new Http(new Server($resources), 'America/New_York'); $http->start(); ``` @@ -74,7 +74,7 @@ use Utopia\Http\Http; use Utopia\Http\Response; use Utopia\Http\Adapter\FPM\Server; -$container = new Container(); +$resources = new Container(); Http::get('/') ->inject('response') @@ -84,7 +84,7 @@ Http::get('/') } ); -$http = new Http(new Server(), 'America/New_York', $container); +$http = new Http(new Server($resources), 'America/New_York'); $http->start(); ``` @@ -99,7 +99,7 @@ use Utopia\Http\Request; use Utopia\Http\Response; use Utopia\Http\Adapter\Swoole\Server; -$container = new Container(); +$resources = new Container(); Http::get('/') ->inject('request') @@ -110,7 +110,7 @@ Http::get('/') } ); -$http = new Http(new Server('0.0.0.0', '80'), 'America/New_York', $container); +$http = new Http(new Server('0.0.0.0', '80', resources: $resources), 'America/New_York'); $http->start(); ``` @@ -217,12 +217,12 @@ Groups are designed to be actions that run during the lifecycle of requests to e ### Resources -Resources allow you to prepare dependencies for requests such as database connections or shared services. Register application dependencies on the DI container with `set()`. Runtime values such as `request`, `response`, `route`, `error`, and `context` are scoped by `Http` for each request. +Resources allow you to prepare dependencies for requests such as database connections or shared services. Register application dependencies on the resources container with `set()`. Runtime values such as `request`, `response`, `route`, and `error` are registered on the per-request `context` container by `Http` for each request. -Define a dependency on the DI container: +Define a dependency on the resources container: ```php -$container->set('bootTime', function () { +$resources->set('bootTime', function () { return \microtime(true); }); ``` @@ -230,7 +230,7 @@ $container->set('bootTime', function () { Inject resource into endpoint action: ```php -$http = new Http(new Server(), 'America/New_York', $container); +$http = new Http(new Server($resources), 'America/New_York'); Http::get('/') ->inject('bootTime') diff --git a/src/Http/Adapter.php b/src/Http/Adapter.php index 0b0e478..aa31b1c 100755 --- a/src/Http/Adapter.php +++ b/src/Http/Adapter.php @@ -11,5 +11,26 @@ abstract class Adapter abstract public function onStart(callable $callback): void; abstract public function onRequest(callable $callback): void; abstract public function start(): void; - abstract public function getContainer(): Container; + + /** + * Static resources container. + * + * Long-lived, shared across every request for the lifetime of the server. + * Use this for things wired up at boot (config, clients, services) that + * should be reused across requests. Available before any request begins, + * inside server start hooks, and as the parent of every request context. + */ + abstract public function resources(): Container; + + /** + * Per-request context container. + * + * A fresh child container created for each incoming request and disposed + * when the request ends. Use it to register or read request-scoped values + * (request, response, route, error, ...). Lookups fall through to + * {@see self::resources()}, so static resources remain reachable from + * within request handlers. Outside of a request, this returns the static + * resources container. + */ + abstract public function context(): Container; } diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index 2b56568..92ebdbe 100755 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -7,17 +7,24 @@ class Server extends Adapter { - public function __construct(private Container $container) {} + private ?Container $context = null; + + public function __construct(private Container $resources) {} public function onRequest(callable $callback): void { $request = new Request(); $response = new Response(); - $this->container->set('fpmRequest', fn() => $request); - $this->container->set('fpmResponse', fn() => $response); + $this->context = new Container($this->resources); + $this->context->set('fpmRequest', fn() => $request); + $this->context->set('fpmResponse', fn() => $response); - \call_user_func($callback, $request, $response); + try { + \call_user_func($callback, $request, $response); + } finally { + $this->context = null; + } } public function onStart(callable $callback): void @@ -25,9 +32,14 @@ public function onStart(callable $callback): void \call_user_func($callback, $this); } - public function getContainer(): Container + public function resources(): Container + { + return $this->resources; + } + + public function context(): Container { - return $this->container; + return $this->context ?? $this->resources; } public function start(): void {} diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index b8cf4f1..350f86d 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -12,39 +12,47 @@ class Server extends Adapter { protected SwooleServer $server; - protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; - protected Container $container; + protected const string CONTEXT_KEY = '__utopia__'; /** * @param array $settings */ - public function __construct(string $host, ?string $port = null, array $settings = [], int $mode = SWOOLE_PROCESS, ?Container $container = null) - { + public function __construct( + string $host, + ?string $port = null, + array $settings = [], + int $mode = SWOOLE_PROCESS, + protected Container $resources = new Container(), + ) { $this->server = new SwooleServer($host, (int) $port, $mode); $this->server->set($settings); - $this->container = $container ?? new Container(); } public function onRequest(callable $callback): void { $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - $requestContainer = new Container($this->container); - $requestContainer->set('swooleRequest', fn() => $request); - $requestContainer->set('swooleResponse', fn() => $response); + $context = new Container($this->resources); + $context->set('swooleRequest', fn() => $request); + $context->set('swooleResponse', fn() => $response); - Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + Coroutine::getContext()[self::CONTEXT_KEY] = $context; \call_user_func($callback, new Request($request), new Response($response)); }); } - public function getContainer(): Container + public function resources(): Container + { + return $this->resources; + } + + public function context(): Container { if (Coroutine::getCid() !== -1) { - return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + return Coroutine::getContext()[self::CONTEXT_KEY] ?? $this->resources; } - return $this->container; + return $this->resources; } public function getServer(): SwooleServer diff --git a/src/Http/Adapter/SwooleCoroutine/Server.php b/src/Http/Adapter/SwooleCoroutine/Server.php index 2b21ab1..b62e121 100644 --- a/src/Http/Adapter/SwooleCoroutine/Server.php +++ b/src/Http/Adapter/SwooleCoroutine/Server.php @@ -11,10 +11,9 @@ class Server extends Adapter { - protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + protected const string CONTEXT_KEY = '__utopia__'; protected SwooleServer $server; - protected Container $container; /** @var callable|null */ protected $onStartCallback; @@ -26,33 +25,37 @@ public function __construct( string $host, ?string $port = null, array $settings = [], - ?Container $container = null, + protected Container $resources = new Container(), ) { $this->server = new SwooleServer($host, $port, false, true); $this->server->set($settings); - $this->container = $container ?? new Container(); } public function onRequest(callable $callback): void { $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - $requestContainer = new Container($this->container); - $requestContainer->set('swooleRequest', fn() => $request); - $requestContainer->set('swooleResponse', fn() => $response); + $context = new Container($this->resources); + $context->set('swooleRequest', fn() => $request); + $context->set('swooleResponse', fn() => $response); - Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + Coroutine::getContext()[self::CONTEXT_KEY] = $context; try { \call_user_func($callback, new Request($request), new Response($response)); } finally { - unset(Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY]); + unset(Coroutine::getContext()[self::CONTEXT_KEY]); } }); } - public function getContainer(): Container + public function resources(): Container { - return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + return $this->resources; + } + + public function context(): Container + { + return Coroutine::getContext()[self::CONTEXT_KEY] ?? $this->resources; } public function getServer(): SwooleServer diff --git a/src/Http/Http.php b/src/Http/Http.php index e059a91..06351fb 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,8 +2,6 @@ namespace Utopia\Http; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; use Utopia\DI\Container; use Utopia\Servers\Hook; use Utopia\Telemetry\Adapter as Telemetry; @@ -44,11 +42,6 @@ class Http public const string MODE_TYPE_PRODUCTION = 'production'; - protected Files $files; - - protected Container $container; - - protected ?Container $requestContainer = null; /** * Current running mode @@ -135,17 +128,15 @@ class Http private Histogram $responseBodySize; - protected Adapter $server; - /** * Http */ - public function __construct(Adapter $server, string $timezone) - { + public function __construct( + protected Adapter $adapter, + string $timezone, + protected Files $files = new Files(), + ) { date_default_timezone_set($timezone); - $this->files = new Files(); - $this->server = $server; - $this->container = $server->getContainer(); $this->setTelemetry(new NoTelemetry()); } @@ -365,63 +356,28 @@ public static function setAllowOverride(bool $value): void } /** - * Get a single resource from the given scope. - * - * @throws Exception - */ - public function getResource(string $name): mixed - { - try { - return $this->server->getContainer()->get($name); - } catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) { - // Normalize DI container errors to the Http layer's "resource" terminology. - $message = str_replace('dependency', 'resource', $e->getMessage()); - - if ($message === $e->getMessage() && !str_contains($message, 'resource')) { - $message = 'Failed to find resource: "' . $name . '"'; - } - - throw new Exception($message, 500, $e); - } - } - - /** - * Get multiple resources from the given scope. - * - * @param string[] $list - * @return array - * - * @throws Exception - */ - public function getResources(array $list): array - { - $resources = []; - - foreach ($list as $name) { - $resources[$name] = $this->getResource($name); - } - - return $resources; - } - - /** - * Set a resource on the given scope. + * Static resources container. * - * @param list $injections + * Shortcut for the underlying adapter's {@see Adapter::resources()}. Use + * `$http->resources()->set(...)` to register app-wide services that are + * shared across every request for the lifetime of the server. */ - public function setResource(string $name, callable $callback, array $injections = []): void + public function resources(): Container { - $this->container->set($name, $callback, $injections); + return $this->adapter->resources(); } /** - * Set a request-scoped resource on the current request's container. + * Per-request context container. * - * @param list $injections + * Shortcut for the underlying adapter's {@see Adapter::context()}. Use + * `$http->context()->set(...)` to register request-scoped resources and + * `$http->context()->get(...)` to read them. Lookups fall through to the + * static resources container, so app-wide services remain accessible. */ - protected function setRequestResource(string $name, callable $callback, array $injections = []): void + public function context(): Container { - $this->server->getContainer()->set($name, $callback, $injections); + return $this->adapter->context(); } /** @@ -550,12 +506,12 @@ public static function onRequest(): Hook public function start(): void { - $this->server->onRequest( + $this->adapter->onRequest( fn(Request $request, Response $response) => $this->run($request, $response), ); - $this->server->onStart(function ($server) { - $this->setResource('server', fn() => $server); + $this->adapter->onStart(function ($server) { + $this->resources()->set('server', fn() => $server); try { foreach (self::$startHooks as $hook) { @@ -563,7 +519,7 @@ public function start(): void \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->setResource('error', fn() => $e); + $this->resources()->set('error', fn() => $e); foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { @@ -578,7 +534,7 @@ public function start(): void } }); - $this->server->start(); + $this->adapter->start(); } /** @@ -657,7 +613,7 @@ public function execute(Route $route, Request $request, Response $response): sta } } } catch (\Throwable $e) { - $this->setRequestResource('error', fn() => $e, []); + $this->context()->set('error', fn() => $e, []); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks @@ -725,7 +681,8 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) $arg = $existsInRequest ? $requestParams[$requestKey] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, array_values($this->getResources($param['injections']))); + $context = $this->adapter->context(); + $arg = \call_user_func_array($arg, array_map($context->get(...), $param['injections'])); } $value = $existsInValues ? $values[$valuesKey] : $arg; @@ -744,7 +701,7 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } foreach ($hook->getInjections() as $injection) { - $arguments[$injection['order']] = $this->getResource($injection['name']); + $arguments[$injection['order']] = $this->adapter->context()->get($injection['name']); } return $arguments; @@ -797,8 +754,8 @@ private function runInternal(Request $request, Response $response): static $response->setCompressionSupported($this->compressionSupported); } - $this->setRequestResource('request', fn() => $request); - $this->setRequestResource('response', fn() => $response); + $this->context()->set('request', fn() => $request); + $this->context()->set('response', fn() => $response); try { foreach (self::$requestHooks as $hook) { @@ -806,7 +763,7 @@ private function runInternal(Request $request, Response $response): static \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->setRequestResource('error', fn() => $e, []); + $this->context()->set('error', fn() => $e, []); foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { @@ -836,7 +793,7 @@ private function runInternal(Request $request, Response $response): static $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->setRequestResource('route', fn() => $route, []); + $this->context()->set('route', fn() => $route, []); if (self::REQUEST_METHOD_HEAD === $method) { $method = self::REQUEST_METHOD_GET; @@ -864,7 +821,7 @@ private function runInternal(Request $request, Response $response): static foreach (self::$errors as $error) { // Global error hooks /** @var Hook $error */ if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => $e, []); + $this->context()->set('error', fn() => $e, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -880,7 +837,7 @@ private function runInternal(Request $request, Response $response): static $path = \is_string($path) ? ($path === '' ? '/' : $path) : '/'; $route->path($path); - $this->setRequestResource('route', fn() => $route, []); + $this->context()->set('route', fn() => $route, []); } if (null !== $route) { return $this->execute($route, $request, $response); @@ -904,7 +861,7 @@ private function runInternal(Request $request, Response $response): static } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => $e, []); + $this->context()->set('error', fn() => $e, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -912,7 +869,7 @@ private function runInternal(Request $request, Response $response): static } else { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setRequestResource('error', fn() => new Exception('Not Found', 404), []); + $this->context()->set('error', fn() => new Exception('Not Found', 404), []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -940,7 +897,8 @@ protected function validate(string $key, array $param, mixed $value): void $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, array_values($this->getResources($param['injections']))); + $context = $this->adapter->context(); + $validator = \call_user_func_array($validator, array_map($context->get(...), $param['injections'])); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 2836f40..e37291b 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -17,7 +17,7 @@ final class HttpTest extends TestCase { protected ?Http $http; - protected ?Container $container; + protected ?Container $resources; protected ?string $method; @@ -26,15 +26,15 @@ final class HttpTest extends TestCase public function setUp(): void { Http::reset(); - $this->container = new Container(); - $this->http = new Http(new Server($this->container), 'Asia/Tel_Aviv'); + $this->resources = new Container(); + $this->http = new Http(new Server($this->resources), 'Asia/Tel_Aviv'); $this->saveRequest(); } public function tearDown(): void { $this->http = null; - $this->container = null; + $this->resources = null; $this->restoreRequest(); } @@ -90,8 +90,8 @@ public function testCanGetEnvironmentVariable(): void public function testCanExecuteRoute(): void { - $this->container->set('rand', fn() => rand()); - $resource = $this->container->get('rand'); + $this->resources->set('rand', fn() => rand()); + $resource = $this->resources->get('rand'); $this->http ->error() @@ -116,7 +116,7 @@ public function testCanExecuteRoute(): void ob_end_clean(); // With Params - $resource = $this->container->get('rand'); + $resource = $this->resources->get('rand'); $route = new Route('GET', '/path'); $route @@ -142,7 +142,7 @@ public function testCanExecuteRoute(): void $this->assertSame($resource . '-param-x-param-y', $result); // With Error - $resource = $this->container->get('rand'); + $resource = $this->resources->get('rand'); $route = new Route('GET', '/path'); $route @@ -162,7 +162,7 @@ public function testCanExecuteRoute(): void $this->assertSame('error: Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); // With Hooks - $resource = $this->container->get('rand'); + $resource = $this->resources->get('rand'); $this->http ->init() ->inject('rand') @@ -233,7 +233,7 @@ public function testCanExecuteRoute(): void $this->assertSame('init-' . $resource . '-(init-api)-param-x-param-y-(shutdown-api)-shutdown', $result); - $resource = $this->container->get('rand'); + $resource = $this->resources->get('rand'); ob_start(); $request = new UtopiaFPMRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); @@ -722,7 +722,7 @@ public function testWildcardRoute(): void Http::init() ->action(function () { $route = $this->http->getRoute(); - $this->container->set('myRoute', fn() => $route); + $this->resources->set('myRoute', fn() => $route); }); @@ -841,7 +841,7 @@ public function testCanInjectResourceAndParamWithSameName(): void { // Register a 'locale' resource returning a Locale instance whose // `name` statically resolves to "en". - $this->container->set('locale', fn() => new Locale()); + $this->resources->set('locale', fn() => new Locale()); $route = new Route('GET', '/path');