diff --git a/composer.json b/composer.json index 355a326e..a9e275cc 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "require": { "php": ">=8.3", "utopia-php/compression": "0.1.*", + "utopia-php/di": "0.3.*", "utopia-php/telemetry": "0.2.*", "utopia-php/validators": "0.2.*" }, diff --git a/composer.lock b/composer.lock index b9987e23..0557edeb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bdf751c50aa401f0a220fb353295974b", + "content-hash": "91ee4d89826e20fb4efa451e36455aaa", "packages": [ { "name": "brick/math", - "version": "0.14.7", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/07ff363b16ef8aca9692bba3be9e73fe63f34e50", - "reference": "07ff363b16ef8aca9692bba3be9e73fe63f34e50", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.7" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2026-02-07T10:57:35+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "composer/semver", @@ -462,16 +462,16 @@ }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.4", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca" + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/62e680d587beb42e5247aa6ecd89ad1ca406e8ca", - "reference": "62e680d587beb42e5247aa6ecd89ad1ca406e8ca", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/283a0d66522f2adc6d8d7debfd7686be91c282be", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be", "shasum": "" }, "require": { @@ -522,7 +522,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2026-01-15T09:31:34+00:00" + "time": "2026-02-05T09:44:52+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", @@ -589,16 +589,16 @@ }, { "name": "open-telemetry/sdk", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "7f1bd524465c1ca42755a9ef1143ba09913f5be0" + "reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/7f1bd524465c1ca42755a9ef1143ba09913f5be0", - "reference": "7f1bd524465c1ca42755a9ef1143ba09913f5be0", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1", + "reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1", "shasum": "" }, "require": { @@ -682,7 +682,7 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2026-01-21T04:14:03+00:00" + "time": "2026-01-28T11:38:11+00:00" }, { "name": "open-telemetry/sem-conv", @@ -1306,16 +1306,16 @@ }, { "name": "symfony/http-client", - "version": "v7.4.5", + "version": "v7.4.7", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" + "reference": "1010624285470eb60e88ed10035102c75b4ea6af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", - "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af", "shasum": "" }, "require": { @@ -1383,7 +1383,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.4.5" + "source": "https://github.com/symfony/http-client/tree/v7.4.7" }, "funding": [ { @@ -1403,7 +1403,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:16:02+00:00" + "time": "2026-03-05T11:16:58+00:00" }, { "name": "symfony/http-client-contracts", @@ -1869,20 +1869,20 @@ }, { "name": "utopia-php/compression", - "version": "0.1.3", + "version": "0.1.4", "source": { "type": "git", "url": "https://github.com/utopia-php/compression.git", - "reference": "66f093557ba66d98245e562036182016c7dcfe8a" + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/compression/zipball/66f093557ba66d98245e562036182016c7dcfe8a", - "reference": "66f093557ba66d98245e562036182016c7dcfe8a", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/68045cb9d714c1259582d2dfd0e76bd34f83e713", + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713", "shasum": "" }, "require": { - "php": ">=8.0" + "php": ">=8.1" }, "require-dev": { "laravel/pint": "1.2.*", @@ -1909,9 +1909,60 @@ ], "support": { "issues": "https://github.com/utopia-php/compression/issues", - "source": "https://github.com/utopia-php/compression/tree/0.1.3" + "source": "https://github.com/utopia-php/compression/tree/0.1.4" + }, + "time": "2026-02-17T05:53:40+00:00" + }, + { + "name": "utopia-php/di", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/di.git", + "reference": "68873b7267842315d01d82a83b988bae525eab31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/di/zipball/68873b7267842315d01d82a83b988bae525eab31", + "reference": "68873b7267842315d01d82a83b988bae525eab31", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^2.0" + }, + "require-dev": { + "laravel/pint": "^1.27", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" }, - "time": "2025-01-15T15:15:51+00:00" + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "PSR-11", + "container", + "dependency-injection", + "di", + "php", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.3.1" + }, + "time": "2026-03-13T05:47:23+00:00" }, { "name": "utopia-php/telemetry", @@ -2240,16 +2291,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90", - "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -2260,13 +2311,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.92.4", - "illuminate/view": "^12.44.0", - "larastan/larastan": "^3.8.1", - "laravel-zero/framework": "^12.0.4", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.4" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -2303,7 +2355,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-01-05T16:49:17+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "myclabs/deep-copy", @@ -2594,16 +2646,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.4.3", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "b641dde59d969ea42eed70a39f9b51950bc96878" + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878", - "reference": "b641dde59d969ea42eed70a39f9b51950bc96878", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", + "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", "shasum": "" }, "require": { @@ -2614,7 +2666,7 @@ "ext-reflection": "*", "ext-spl": "*", "ext-tokenizer": "*", - "php": "^8.1", + "php": "^8.2", "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", @@ -2634,8 +2686,9 @@ "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^10.4 || ^11.0", + "phpunit/phpunit": "^11.5", "rector/rector": "^1.2", + "sebastian/exporter": "^6.3.2", "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, @@ -2680,7 +2733,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.3" + "source": "https://github.com/phpbench/phpbench/tree/1.5.1" }, "funding": [ { @@ -2688,15 +2741,15 @@ "type": "github" } ], - "time": "2025-11-06T19:07:31+00:00" + "time": "2026-03-05T08:18:58+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.32", + "version": "1.12.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", - "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1", + "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1", "shasum": "" }, "require": { @@ -2741,7 +2794,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4331,16 +4384,16 @@ }, { "name": "symfony/console", - "version": "v8.0.4", + "version": "v8.0.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", - "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", "shasum": "" }, "require": { @@ -4397,7 +4450,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.4" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -4417,20 +4470,20 @@ "type": "tidelift" } ], - "time": "2026-01-13T13:06:50+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/filesystem", - "version": "v8.0.1", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d937d400b980523dc9ee946bb69972b5e619058d" + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", - "reference": "d937d400b980523dc9ee946bb69972b5e619058d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", + "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", "shasum": "" }, "require": { @@ -4467,7 +4520,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.1" + "source": "https://github.com/symfony/filesystem/tree/v8.0.6" }, "funding": [ { @@ -4487,20 +4540,20 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-02-25T16:59:43+00:00" }, { "name": "symfony/finder", - "version": "v8.0.5", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", - "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", "shasum": "" }, "require": { @@ -4535,7 +4588,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.5" + "source": "https://github.com/symfony/finder/tree/v8.0.6" }, "funding": [ { @@ -4555,7 +4608,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:08:38+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { "name": "symfony/options-resolver", @@ -4945,16 +4998,16 @@ }, { "name": "symfony/string", - "version": "v8.0.4", + "version": "v8.0.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "758b372d6882506821ed666032e43020c4f57194" + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", - "reference": "758b372d6882506821ed666032e43020c4f57194", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", "shasum": "" }, "require": { @@ -5011,7 +5064,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -5031,7 +5084,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index 62566379..599b6dd7 100644 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -3,7 +3,6 @@ namespace Utopia\Http\Adapter\FPM; use Utopia\Http\Adapter\Adapter; -use Utopia\Http\Http; class Server extends Adapter { @@ -16,9 +15,6 @@ public function onRequest(callable $callback) $request = new Request(); $response = new Response(); - Http::setResource('fpmRequest', fn () => $request); - Http::setResource('fpmResponse', fn () => $response); - call_user_func($callback, $request, $response); } diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index f39044a6..56a1be67 100644 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -6,7 +6,6 @@ use Swoole\Http\Server as SwooleServer; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use Utopia\Http\Http; class Server extends Adapter { @@ -23,9 +22,6 @@ public function __construct(string $host, ?string $port = null, array $settings public function onRequest(callable $callback) { $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - Http::setResource('swooleRequest', fn () => $request); - Http::setResource('swooleResponse', fn () => $response); - call_user_func($callback, new Request($request), new Response($response)); }); } diff --git a/src/Http/Http.php b/src/Http/Http.php index c0317508..1b586cff 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,6 +2,7 @@ namespace Utopia\Http; +use Utopia\DI\Container; use Utopia\Validator; use Utopia\Telemetry\Adapter as Telemetry; use Utopia\Telemetry\Adapter\None as NoTelemetry; @@ -15,6 +16,7 @@ class Http { + protected const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; public const COMPRESSION_MIN_SIZE_DEFAULT = 1024; /** @@ -43,22 +45,14 @@ class Http public const MODE_TYPE_PRODUCTION = 'production'; - /** - * @var array - */ - protected array $resources = [ - 'error' => null, - ]; - /** * @var Files */ protected Files $files; - /** - * @var array - */ - protected static array $resourcesCallbacks = []; + protected Container $container; + + protected ?Container $requestContainer = null; /** * Current running mode @@ -144,14 +138,47 @@ class Http * Http * * @param string $timezone + * @param ?Container $container */ - public function __construct(string $timezone) + public function __construct(string $timezone, ?Container $container = null) { \date_default_timezone_set($timezone); $this->files = new Files(); + $this->container = $container ?? new Container(); $this->setTelemetry(new NoTelemetry()); } + /** + * Get dependency injection container + * + * @return Container + */ + public function getContainer(): Container + { + return $this->container; + } + + /** + * Get the current request dependency injection container if available. + */ + public function getRequestContainer(): ?Container + { + return $this->requestContainer; + } + + /** + * Set the current request container. + * + * @param ?Container $container + * @return ?Container The previous container + */ + protected function setRequestContainer(?Container $container): ?Container + { + $previous = $this->requestContainer; + $this->requestContainer = $container; + return $previous; + } + /** * Set Compression */ @@ -410,69 +437,53 @@ public static function setAllowOverride(bool $value): void } /** - * If a resource has been created return it, otherwise create it and then return it - * - * @param string $name - * @param bool $fresh - * @return mixed + * Get a single resource from the given scope. * * @throws Exception */ - public function getResource(string $name, bool $fresh = false): mixed + public function getResource(string $name, Container $scope): mixed { - if ($name === 'utopia') { - return $this; - } + try { + return $scope->get($name); + } catch (\Throwable $e) { + // Normalize DI container errors to the Http layer's "resource" terminology. + $message = \str_replace('dependency', 'resource', $e->getMessage()); - if (!\array_key_exists($name, $this->resources) || $fresh || self::$resourcesCallbacks[$name]['reset']) { - if (!\array_key_exists($name, self::$resourcesCallbacks)) { - throw new Exception('Failed to find resource: "' . $name . '"'); + if ($message === $e->getMessage() && !\str_contains($message, 'resource')) { + $message = 'Failed to find resource: "' . $name . '"'; } - $this->resources[$name] = \call_user_func_array( - self::$resourcesCallbacks[$name]['callback'], - $this->getResources(self::$resourcesCallbacks[$name]['injections']) - ); + throw new Exception($message, 500, $e); } - - self::$resourcesCallbacks[$name]['reset'] = false; - - return $this->resources[$name]; } /** - * Get Resources By List + * Get multiple resources from the given scope. * - * @param array $list - * @return array + * @param string[] $list + * @return array + * + * @throws Exception */ - public function getResources(array $list): array + public function getResources(array $list, Container $scope): array { $resources = []; foreach ($list as $name) { - $resources[$name] = $this->getResource($name); + $resources[$name] = $this->getResource($name, $scope); } return $resources; } /** - * Set a new resource callback + * Set a resource on the given scope. * - * @param string $name - * @param callable $callback - * @param array $injections - * @return void - * - * @throws Exception + * @param string[] $injections */ - public static function setResource(string $name, callable $callback, array $injections = []): void + public function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void { - if ($name === 'utopia') { - throw new Exception("'utopia' is a reserved keyword.", 500); - } - self::$resourcesCallbacks[$name] = ['callback' => $callback, 'injections' => $injections, 'reset' => true]; + ($scope ?? $this->container)->set($name, $callback, $injections); } /** @@ -642,8 +653,9 @@ public function match(Request $request, bool $fresh = false): ?Route * * @param Route $route * @param Request $request + * @param ?Container $requestScope */ - public function execute(Route $route, Request $request, Response $response): static + public function execute(Route $route, Request $request, Response $response, ?Container $requestScope = null): static { $arguments = []; $groups = $route->getGroups(); @@ -651,11 +663,14 @@ public function execute(Route $route, Request $request, Response $response): sta $preparedPath = Router::preparePath($route->getMatchedPath()); $pathValues = $route->getPathValues($request, $preparedPath[0]); + $requestScope ??= new Container($this->container); + $previousRequestContainer = $this->setRequestContainer($requestScope); + try { if ($route->getHook()) { foreach (self::$init as $hook) { // Global init hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -664,14 +679,14 @@ public function execute(Route $route, Request $request, Response $response): sta foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } if (!($response->isSent())) { - $arguments = $this->getArguments($route, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $requestScope, $pathValues, $request->getParams()); // Call the action callback with the matched positions as params \call_user_func_array($route->getAction(), $arguments); @@ -681,7 +696,7 @@ public function execute(Route $route, Request $request, Response $response): sta foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -690,19 +705,19 @@ public function execute(Route $route, Request $request, Response $response): sta if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { - self::setResource('error', fn () => $e); + $this->setResource('error', fn () => $e, [], $requestScope); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks if (in_array($group, $error->getGroups())) { try { - $arguments = $this->getArguments($error, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); @@ -714,13 +729,15 @@ public function execute(Route $route, Request $request, Response $response): sta foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); } } } + } finally { + $this->setRequestContainer($previousRequestContainer); } return $this; @@ -730,13 +747,14 @@ public function execute(Route $route, Request $request, Response $response): sta * Get Arguments * * @param Hook $hook + * @param Container $scope * @param array $values * @param array $requestParams * @return array * * @throws Exception */ - protected function getArguments(Hook $hook, array $values, array $requestParams): array + protected function getArguments(Hook $hook, Container $scope, array $values, array $requestParams): array { $arguments = []; foreach ($hook->getParams() as $key => $param) { // Get value from route or request object @@ -748,7 +766,7 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) $arg = $requestParams[$key]; } else { if (!is_string($param['default']) && \is_callable($param['default'])) { - $arg = \call_user_func_array($param['default'], $this->getResources($param['injections'])); + $arg = \call_user_func_array($param['default'], $this->getResources($param['injections'], $scope)); } else { $arg = $param['default']; } @@ -810,7 +828,7 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) !($param['optional'] && $value === null) && $paramExists ) { - $validator = $this->validate($key, $param, $value); + $validator = $this->validate($key, $param, $value, $scope); if ($existsInRequest && $value !== null) { if ($validator instanceof Boolean) { @@ -837,7 +855,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->getResource($injection['name'], $scope); } return $arguments; @@ -891,112 +909,123 @@ private function runInternal(Request $request, Response $response): static $response->setCompressionSupported($this->compressionSupported); } - $this->resources['request'] = $request; - $this->resources['response'] = $response; + $requestScope = new Container($this->container); + $previousRequestContainer = $this->setRequestContainer($requestScope); - self::setResource('request', function () use ($request) { - return $request; - }); + try { + $this->setResource('request', fn () => $request, [], $requestScope); + $this->setResource('response', fn () => $response, [], $requestScope); - self::setResource('response', function () use ($response) { - return $response; - }); + if ($this->isFileLoaded($request->getURI())) { + $time = (60 * 60 * 24 * 365 * 2); // 45 days cache - if ($this->isFileLoaded($request->getURI())) { - $time = (60 * 60 * 24 * 365 * 2); // 45 days cache + $response + ->setContentType($this->getFileMimeType($request->getURI())) + ->addHeader('Cache-Control', 'public, max-age=' . $time) + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache + ->send($this->getFileContents($request->getURI())); - $response - ->setContentType($this->getFileMimeType($request->getURI())) - ->addHeader('Cache-Control', 'public, max-age=' . $time) - ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $time) . ' GMT') // 45 days cache - ->send($this->getFileContents($request->getURI())); + return $this; + } - return $this; - } + $method = $request->getMethod(); + $route = $this->match($request); + $this->matchedRoute = $route; + $groups = ($route instanceof Route) ? $route->getGroups() : []; - $method = $request->getMethod(); - $route = $this->match($request); - $this->matchedRoute = $route; - $groups = ($route instanceof Route) ? $route->getGroups() : []; - - if (null === $route && null !== self::$wildcardRoute) { - $route = self::$wildcardRoute; - $this->route = $route; - $path = \parse_url($request->getURI(), PHP_URL_PATH); - $route->path($path); - } + if (null === $route && null !== self::$wildcardRoute) { + $route = self::$wildcardRoute; + $this->route = $route; + $path = \parse_url($request->getURI(), PHP_URL_PATH); + $route->path($path); + } - self::setResource('route', function () use ($route, $request) { - return $route ?? new Route($request->getMethod(), $request->getURI()); - }); + $this->setResource('route', function () use ($route, $request) { + return $route ?? new Route($request->getMethod(), $request->getURI()); + }, [], $requestScope); - if (self::REQUEST_METHOD_HEAD == $method) { - $method = self::REQUEST_METHOD_GET; - $response->disablePayload(); - } + if (self::REQUEST_METHOD_HEAD == $method) { + $method = self::REQUEST_METHOD_GET; + $response->disablePayload(); + } - if (self::REQUEST_METHOD_OPTIONS == $method) { - try { - foreach ($groups as $group) { - foreach (self::$options as $option) { // Group options hooks - /** @var Hook $option */ - if (in_array($group, $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + if (self::REQUEST_METHOD_OPTIONS == $method) { + try { + foreach ($groups as $group) { + foreach (self::$options as $option) { // Group options hooks + /** @var Hook $option */ + if (in_array($group, $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, $requestScope, [], $request->getParams())); + } } } - } - foreach (self::$options as $option) { // Global options hooks - /** @var Hook $option */ - if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + foreach (self::$options as $option) { // Global options hooks + /** @var Hook $option */ + if (in_array('*', $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, $requestScope, [], $request->getParams())); + } } - } - } catch (\Throwable $e) { - foreach (self::$errors as $error) { // Global error hooks - /** @var Hook $error */ - if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; - }); - try { - $arguments = $this->getArguments($error, [], $request->getParams()); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } catch (\Throwable $e) { + foreach (self::$errors as $error) { // Global error hooks + /** @var Hook $error */ + if (in_array('*', $error->getGroups())) { + $this->setResource('error', function () use ($e) { + return $e; + }, [], $requestScope); + try { + $arguments = $this->getArguments($error, $requestScope, [], $request->getParams()); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } } } } - } - return $this; - } + return $this; + } - if (null !== $route) { - return $this->execute($route, $request, $response); - } elseif (self::REQUEST_METHOD_OPTIONS == $method) { - try { - foreach ($groups as $group) { - foreach (self::$options as $option) { // Group options hooks - if (in_array($group, $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + if (null !== $route) { + return $this->execute($route, $request, $response, $requestScope); + } elseif (self::REQUEST_METHOD_OPTIONS == $method) { + try { + foreach ($groups as $group) { + foreach (self::$options as $option) { // Group options hooks + if (in_array($group, $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, $requestScope, [], $request->getParams())); + } } } - } - foreach (self::$options as $option) { // Global options hooks - if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); + foreach (self::$options as $option) { // Global options hooks + if (in_array('*', $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, $requestScope, [], $request->getParams())); + } + } + } catch (\Throwable $e) { + foreach (self::$errors as $error) { // Global error hooks + if (in_array('*', $error->getGroups())) { + $this->setResource('error', function () use ($e) { + return $e; + }, [], $requestScope); + try { + $arguments = $this->getArguments($error, $requestScope, [], $request->getParams()); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); + } + } } } - } catch (\Throwable $e) { + } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; - }); + $this->setResource('error', function () { + return new Exception('Not Found', 404); + }, [], $requestScope); try { - $arguments = $this->getArguments($error, [], $request->getParams()); + $arguments = $this->getArguments($error, $requestScope, [], $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); @@ -1004,20 +1033,8 @@ private function runInternal(Request $request, Response $response): static } } } - } else { - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - self::setResource('error', function () { - return new Exception('Not Found', 404); - }); - try { - $arguments = $this->getArguments($error, [], $request->getParams()); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 500, $e); - } - } - } + } finally { + $this->setRequestContainer($previousRequestContainer); } return $this; @@ -1031,16 +1048,17 @@ private function runInternal(Request $request, Response $response): static * @param string $key * @param array $param * @param mixed $value + * @param Container $scope * @return Validator * * @throws Exception */ - protected function validate(string $key, array $param, mixed $value): Validator + protected function validate(string $key, array $param, mixed $value, Container $scope): Validator { $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'])); + $validator = \call_user_func_array($validator, $this->getResources($param['injections'], $scope)); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class @@ -1062,7 +1080,6 @@ protected function validate(string $key, array $param, mixed $value): Validator public static function reset(): void { Router::reset(); - self::$resourcesCallbacks = []; self::$mode = ''; self::$errors = []; self::$init = []; diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 6e83993f..64f46a2b 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -3,6 +3,7 @@ namespace Utopia\Http; use PHPUnit\Framework\TestCase; +use Utopia\DI\Container; use Utopia\Http\Adapter\FPM\Request; use Utopia\Http\Adapter\FPM\Response; use Utopia\Http\Tests\UtopiaRequestTest; @@ -13,6 +14,8 @@ class HttpTest extends TestCase { protected ?Http $app; + protected ?Container $container; + protected ?string $method; protected ?string $uri; @@ -21,12 +24,14 @@ public function setUp(): void { Http::reset(); $this->app = new Http('Asia/Tel_Aviv'); + $this->container = $this->app->getContainer(); $this->saveRequest(); } public function tearDown(): void { $this->app = null; + $this->container = null; $this->restoreRequest(); } @@ -82,21 +87,21 @@ public function testCanGetEnvironmentVariable(): void public function testCanGetResources(): void { - Http::setResource('rand', fn () => rand()); - Http::setResource('first', fn ($second) => "first-{$second}", ['second']); - Http::setResource('second', fn () => 'second'); + $this->container->set('rand', fn () => rand()); + $this->container->set('first', fn ($second) => "first-{$second}", ['second']); + $this->container->set('second', fn () => 'second'); - $second = $this->app->getResource('second'); - $first = $this->app->getResource('first'); + $second = $this->container->get('second'); + $first = $this->container->get('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); - $resource = $this->app->getResource('rand'); + $resource = $this->container->get('rand'); $this->assertNotEmpty($resource); - $this->assertEquals($resource, $this->app->getResource('rand')); - $this->assertEquals($resource, $this->app->getResource('rand')); - $this->assertEquals($resource, $this->app->getResource('rand')); + $this->assertEquals($resource, $this->container->get('rand')); + $this->assertEquals($resource, $this->container->get('rand')); + $this->assertEquals($resource, $this->container->get('rand')); // Default Params $route = new Route('GET', '/path'); @@ -119,11 +124,11 @@ public function testCanGetResources(): void public function testCanGetDefaultValueWithFunction(): void { - Http::setResource('first', fn ($second) => "first-{$second}", ['second']); - Http::setResource('second', fn () => 'second'); + $this->container->set('first', fn ($second) => "first-{$second}", ['second']); + $this->container->set('second', fn () => 'second'); - $second = $this->app->getResource('second'); - $first = $this->app->getResource('first'); + $second = $this->container->get('second'); + $first = $this->container->get('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); @@ -165,8 +170,8 @@ public function testCanGetDefaultValueWithFunction(): void public function testCanExecuteRoute(): void { - Http::setResource('rand', fn () => rand()); - $resource = $this->app->getResource('rand'); + $this->container->set('rand', fn () => rand()); + $resource = $this->container->get('rand'); $this->app ->error() @@ -780,7 +785,8 @@ public function testWildcardRoute(): void ->inject('response') ->action(function (Request $request, Response $response) { $route = $this->app->getRoute(); - Http::setResource('myRoute', fn () => $route); + $requestContainer = $this->app->getRequestContainer(); + $requestContainer?->set('myRoute', fn () => $route); if ($request->getURI() === '/init_response') { $response->send('THIS IS RESPONSE FROM INIT!');