From 2b500d0798806e0fad62cd9a27dda42fa183f2b7 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 11 Dec 2025 18:30:55 +0530 Subject: [PATCH 01/29] Fix array to string conversion warning in Request::getSize() Headers can now be arrays (after recent changes allowing array headers). The getSize() method was attempting to directly implode headers, causing a warning when a header value was an array. This fix properly handles both string and array header values by joining array values with commas (standard HTTP header format) before calculating the request size. Added test case to verify the fix works correctly with array headers. --- src/Http/Request.php | 11 ++++++++++- tests/RequestTest.php | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Http/Request.php b/src/Http/Request.php index 8332d99d..c9762123 100755 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -332,7 +332,16 @@ abstract public function removeHeader(string $key): static; */ public function getSize(): int { - return \mb_strlen(\implode("\n", $this->generateHeaders()), '8bit') + \mb_strlen(\file_get_contents('php://input'), '8bit'); + $headers = $this->generateHeaders(); + $headerStrings = []; + foreach ($headers as $key => $value) { + if (\is_array($value)) { + $headerStrings[] = $key . ': ' . \implode(', ', $value); + } else { + $headerStrings[] = $key . ': ' . $value; + } + } + return \mb_strlen(\implode("\n", $headerStrings), '8bit') + \mb_strlen(\file_get_contents('php://input'), '8bit'); } /** diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 55d49ee9..02d8930a 100755 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -313,4 +313,23 @@ public function testCanGetRange() $this->assertEquals(null, $this->request->getRangeStart()); $this->assertEquals(null, $this->request->getRangeEnd()); } + + public function testCanGetSizeWithArrayHeaders() + { + $this->request->addHeader('content-type', 'application/json'); + + $reflection = new \ReflectionClass($this->request); + $headersProperty = $reflection->getProperty('headers'); + $headersProperty->setAccessible(true); + + $headers = $headersProperty->getValue($this->request) ?? []; + $headers['accept'] = ['application/json', 'text/html']; + $headers['x-custom'] = ['value1', 'value2', 'value3']; + $headersProperty->setValue($this->request, $headers); + + $size = $this->request->getSize(); + + $this->assertIsInt($size); + $this->assertGreaterThan(0, $size); + } } From b73e9e8500fada78ff4104e9b625be2dbf38c1ca Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 18 Dec 2025 13:39:22 +0530 Subject: [PATCH 02/29] feat: remove validators and use utopia validators lib --- composer.json | 3 +- composer.lock | 361 ++++++++++++++----------- docs/Getting-Starting-Guide.md | 2 +- example/src/server.php | 2 +- src/Http/Hook.php | 2 + src/Http/Http.php | 2 + src/Http/Validator.php | 57 ---- src/Http/Validator/AllOf.php | 86 ------ src/Http/Validator/AnyOf.php | 87 ------ src/Http/Validator/ArrayList.php | 116 -------- src/Http/Validator/Assoc.php | 88 ------ src/Http/Validator/Boolean.php | 94 ------- src/Http/Validator/Domain.php | 78 ------ src/Http/Validator/FloatValidator.php | 88 ------ src/Http/Validator/HexColor.php | 53 ---- src/Http/Validator/Host.php | 82 ------ src/Http/Validator/Hostname.php | 114 -------- src/Http/Validator/IP.php | 113 -------- src/Http/Validator/Integer.php | 88 ------ src/Http/Validator/JSON.php | 59 ---- src/Http/Validator/NoneOf.php | 88 ------ src/Http/Validator/Nullable.php | 73 ----- src/Http/Validator/Numeric.php | 66 ----- src/Http/Validator/Range.php | 149 ---------- src/Http/Validator/Text.php | 138 ---------- src/Http/Validator/URL.php | 86 ------ src/Http/Validator/WhiteList.php | 119 -------- src/Http/Validator/Wildcard.php | 62 ----- tests/HookTest.php | 4 +- tests/HttpTest.php | 2 +- tests/RouteTest.php | 2 +- tests/Validator/ArrayListTest.php | 52 ---- tests/Validator/AssocTest.php | 43 --- tests/Validator/BooleanTest.php | 46 ---- tests/Validator/DomainTest.php | 37 --- tests/Validator/FloatValidatorTest.php | 39 --- tests/Validator/HexColorTest.php | 26 -- tests/Validator/HostTest.php | 30 -- tests/Validator/HostnameTest.php | 106 -------- tests/Validator/IPTest.php | 67 ----- tests/Validator/IntegerTest.php | 36 --- tests/Validator/JSONTest.php | 28 -- tests/Validator/MultipleOfTest.php | 61 ----- tests/Validator/NullableTest.php | 22 -- tests/Validator/NumericTest.php | 24 -- tests/Validator/RangeTest.php | 60 ---- tests/Validator/TextTest.php | 88 ------ tests/Validator/URLTest.php | 43 --- tests/Validator/WhiteListTest.php | 58 ---- tests/Validator/WildcardTest.php | 21 -- tests/e2e/init.php | 2 +- 51 files changed, 219 insertions(+), 3034 deletions(-) delete mode 100755 src/Http/Validator.php delete mode 100644 src/Http/Validator/AllOf.php delete mode 100644 src/Http/Validator/AnyOf.php delete mode 100644 src/Http/Validator/ArrayList.php delete mode 100644 src/Http/Validator/Assoc.php delete mode 100644 src/Http/Validator/Boolean.php delete mode 100644 src/Http/Validator/Domain.php delete mode 100755 src/Http/Validator/FloatValidator.php delete mode 100644 src/Http/Validator/HexColor.php delete mode 100755 src/Http/Validator/Host.php delete mode 100644 src/Http/Validator/Hostname.php delete mode 100644 src/Http/Validator/IP.php delete mode 100755 src/Http/Validator/Integer.php delete mode 100644 src/Http/Validator/JSON.php delete mode 100644 src/Http/Validator/NoneOf.php delete mode 100644 src/Http/Validator/Nullable.php delete mode 100755 src/Http/Validator/Numeric.php delete mode 100755 src/Http/Validator/Range.php delete mode 100644 src/Http/Validator/Text.php delete mode 100644 src/Http/Validator/URL.php delete mode 100755 src/Http/Validator/WhiteList.php delete mode 100644 src/Http/Validator/Wildcard.php delete mode 100755 tests/Validator/ArrayListTest.php delete mode 100755 tests/Validator/AssocTest.php delete mode 100755 tests/Validator/BooleanTest.php delete mode 100644 tests/Validator/DomainTest.php delete mode 100755 tests/Validator/FloatValidatorTest.php delete mode 100755 tests/Validator/HexColorTest.php delete mode 100644 tests/Validator/HostTest.php delete mode 100755 tests/Validator/HostnameTest.php delete mode 100644 tests/Validator/IPTest.php delete mode 100755 tests/Validator/IntegerTest.php delete mode 100755 tests/Validator/JSONTest.php delete mode 100644 tests/Validator/MultipleOfTest.php delete mode 100755 tests/Validator/NullableTest.php delete mode 100755 tests/Validator/NumericTest.php delete mode 100755 tests/Validator/RangeTest.php delete mode 100755 tests/Validator/TextTest.php delete mode 100644 tests/Validator/URLTest.php delete mode 100755 tests/Validator/WhiteListTest.php delete mode 100644 tests/Validator/WildcardTest.php diff --git a/composer.json b/composer.json index 377787e6..e5953bc4 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ }, "require": { "php": ">=8.0", - "ext-swoole": "*" + "ext-swoole": "*", + "utopia-php/validators": "0.1.*" }, "require-dev": { "phpunit/phpunit": "^9.5.25", diff --git a/composer.lock b/composer.lock index abee3f7b..b68b4ff5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,54 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "47587a7a55803f5b0fdf225267c03f03", - "packages": [], + "content-hash": "3851bd0696173988209c33d95ad3fd35", + "packages": [ + { + "name": "utopia-php/validators", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/validators.git", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.*", + "phpstan/phpstan": "1.*", + "phpunit/phpunit": "11.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight collection of reusable validators for Utopia projects", + "keywords": [ + "php", + "utopia", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/utopia-php/validators/issues", + "source": "https://github.com/utopia-php/validators/tree/0.1.0" + }, + "time": "2025-11-18T11:05:46+00:00" + } + ], "packages-dev": [ { "name": "doctrine/annotations", @@ -81,6 +127,7 @@ "issues": "https://github.com/doctrine/annotations/issues", "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, + "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, { @@ -232,16 +279,16 @@ }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -252,22 +299,19 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -287,6 +331,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -297,7 +342,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "myclabs/deep-copy", @@ -361,16 +406,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -413,9 +458,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -537,24 +582,24 @@ }, { "name": "phpbench/container", - "version": "2.2.2", + "version": "2.2.3", "source": { "type": "git", "url": "https://github.com/phpbench/container.git", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33" + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/container/zipball/a59b929e00b87b532ca6d0edd8eca0967655af33", - "reference": "a59b929e00b87b532ca6d0edd8eca0967655af33", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", "shasum": "" }, "require": { "psr/container": "^1.0|^2.0", - "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0" + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^0.12.52", "phpunit/phpunit": "^8" }, @@ -582,22 +627,22 @@ "description": "Simple, configurable, service container.", "support": { "issues": "https://github.com/phpbench/container/issues", - "source": "https://github.com/phpbench/container/tree/2.2.2" + "source": "https://github.com/phpbench/container/tree/2.2.3" }, - "time": "2023-10-30T13:38:26+00:00" + "time": "2025-11-06T09:05:13+00:00" }, { "name": "phpbench/phpbench", - "version": "1.4.1", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "78cd98a9aa34e0f8f80ca01972a8b88d2c30194b" + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/78cd98a9aa34e0f8f80ca01972a8b88d2c30194b", - "reference": "78cd98a9aa34e0f8f80ca01972a8b88d2c30194b", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/b641dde59d969ea42eed70a39f9b51950bc96878", + "reference": "b641dde59d969ea42eed70a39f9b51950bc96878", "shasum": "" }, "require": { @@ -612,26 +657,26 @@ "phpbench/container": "^2.2", "psr/log": "^1.1 || ^2.0 || ^3.0", "seld/jsonlint": "^1.1", - "symfony/console": "^6.1 || ^7.0", - "symfony/filesystem": "^6.1 || ^7.0", - "symfony/finder": "^6.1 || ^7.0", - "symfony/options-resolver": "^6.1 || ^7.0", - "symfony/process": "^6.1 || ^7.0", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", "webmozart/glob": "^4.6" }, "require-dev": { "dantleech/invoke": "^2.0", "ergebnis/composer-normalize": "^2.39", - "friendsofphp/php-cs-fixer": "^3.0", "jangregor/phpstan-prophecy": "^1.0", - "phpspec/prophecy": "dev-master", + "php-cs-fixer/shim": "^3.9", + "phpspec/prophecy": "^1.22", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^10.4 || ^11.0", "rector/rector": "^1.2", - "symfony/error-handler": "^6.1 || ^7.0", - "symfony/var-dumper": "^6.1 || ^7.0" + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" }, "suggest": { "ext-xdebug": "For Xdebug profiling extension." @@ -674,7 +719,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.4.1" + "source": "https://github.com/phpbench/phpbench/tree/1.4.3" }, "funding": [ { @@ -682,20 +727,15 @@ "type": "github" } ], - "time": "2025-03-12T08:01:40+00:00" + "time": "2025-11-06T19:07:31+00:00" }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -740,7 +780,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1063,16 +1103,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.25", + "version": "9.6.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7" + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", "shasum": "" }, "require": { @@ -1097,7 +1137,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -1146,7 +1186,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" }, "funding": [ { @@ -1170,7 +1210,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:38:31+00:00" + "time": "2025-12-06T07:45:52+00:00" }, { "name": "psr/cache", @@ -1765,16 +1805,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -1830,15 +1870,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -2431,47 +2483,39 @@ }, { "name": "symfony/console", - "version": "v7.3.2", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1" + "reference": "fcb73f69d655b48fcb894a262f074218df08bd58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1", - "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1", + "url": "https://api.github.com/repos/symfony/console/zipball/fcb73f69d655b48fcb894a262f074218df08bd58", + "reference": "fcb73f69d655b48fcb894a262f074218df08bd58", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" + "symfony/string": "^7.4|^8.0" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2505,7 +2549,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.2" + "source": "https://github.com/symfony/console/tree/v8.0.1" }, "funding": [ { @@ -2525,7 +2569,7 @@ "type": "tidelift" } ], - "time": "2025-07-30T17:13:41+00:00" + "time": "2025-12-05T15:25:33+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2596,25 +2640,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "d937d400b980523dc9ee946bb69972b5e619058d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d", + "reference": "d937d400b980523dc9ee946bb69972b5e619058d", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2642,7 +2686,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v8.0.1" }, "funding": [ { @@ -2662,27 +2706,27 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -2710,7 +2754,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v8.0.0" }, "funding": [ { @@ -2730,24 +2774,24 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-11-05T14:36:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.2", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37", - "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -2781,7 +2825,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.2" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -2801,7 +2845,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:36:08+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3140,20 +3184,20 @@ }, { "name": "symfony/process", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "type": "library", "autoload": { @@ -3181,7 +3225,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" + "source": "https://github.com/symfony/process/tree/v8.0.0" }, "funding": [ { @@ -3192,25 +3236,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-10-16T16:25:44+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -3264,7 +3312,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3275,44 +3323,47 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.2", + "version": "v8.0.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", - "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", + "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -3351,7 +3402,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.2" + "source": "https://github.com/symfony/string/tree/v8.0.1" }, "funding": [ { @@ -3371,20 +3422,20 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:47:49+00:00" + "time": "2025-12-01T09:13:36+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -3413,7 +3464,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -3421,7 +3472,7 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/glob", @@ -3475,13 +3526,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.0", "ext-swoole": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/docs/Getting-Starting-Guide.md b/docs/Getting-Starting-Guide.md index b0f46a4b..831d0687 100644 --- a/docs/Getting-Starting-Guide.md +++ b/docs/Getting-Starting-Guide.md @@ -144,7 +144,7 @@ use Utopia\Http\Swoole\Response; use Swoole\Http\Server; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use Utopia\Http\Validator\Wildcard; +use Utopia\Validator\Wildcard; $http = new Server("0.0.0.0", 8080); diff --git a/example/src/server.php b/example/src/server.php index 4eb07f71..d05f80f2 100644 --- a/example/src/server.php +++ b/example/src/server.php @@ -5,7 +5,7 @@ use Utopia\Http\Http; use Utopia\Http\Response; use Utopia\Http\Adapter\Swoole\Server; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Text; Http::get('/') ->param('name', 'World', new Text(256), 'Name to greet. Optional, max length 256.', true) diff --git a/src/Http/Hook.php b/src/Http/Hook.php index 2266da26..0f177dc3 100644 --- a/src/Http/Hook.php +++ b/src/Http/Hook.php @@ -2,6 +2,8 @@ namespace Utopia\Http; +use Utopia\Validator; + class Hook { /** diff --git a/src/Http/Http.php b/src/Http/Http.php index 8fe02785..61434842 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,6 +2,8 @@ namespace Utopia\Http; +use Utopia\Validator; + class Http { /** diff --git a/src/Http/Validator.php b/src/Http/Validator.php deleted file mode 100755 index 6a8304ea..00000000 --- a/src/Http/Validator.php +++ /dev/null @@ -1,57 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) - { - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - if (!(\is_null($this->failedRule))) { - $description = $this->failedRule->getDescription(); - } else { - $description = $this->validators[0]->getDescription(); - } - - return $description; - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - if (!$valid) { - $this->failedRule = $rule; - return false; - } - } - - return true; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } -} diff --git a/src/Http/Validator/AnyOf.php b/src/Http/Validator/AnyOf.php deleted file mode 100644 index ff96a54c..00000000 --- a/src/Http/Validator/AnyOf.php +++ /dev/null @@ -1,87 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) - { - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - if (!(\is_null($this->failedRule))) { - $description = $this->failedRule->getDescription(); - } else { - $description = $this->validators[0]->getDescription(); - } - - return $description; - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - $this->failedRule = $rule; - - if ($valid) { - return true; - } - } - - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } -} diff --git a/src/Http/Validator/ArrayList.php b/src/Http/Validator/ArrayList.php deleted file mode 100644 index c2c7e5ab..00000000 --- a/src/Http/Validator/ArrayList.php +++ /dev/null @@ -1,116 +0,0 @@ -validator = $validator; - $this->length = $length; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - $msg = 'Value must a valid array'; - - if ($this->length > 0) { - $msg .= ' no longer than ' . $this->length . ' items'; - } - - return $msg . ' and ' . $this->validator->getDescription(); - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->validator->getType(); - } - - /** - * Get Nested Validator - * - * @return Validator - */ - public function getValidator(): Validator - { - return $this->validator; - } - - /** - * Is valid - * - * Validation will pass when $value is valid array and validator is valid. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if (!\is_array($value)) { - return false; - } - - if ($this->length && \count($value) > $this->length) { - return false; - } - - foreach ($value as $element) { - if (!$this->validator->isValid($element)) { - return false; - } - } - - return true; - } -} diff --git a/src/Http/Validator/Assoc.php b/src/Http/Validator/Assoc.php deleted file mode 100644 index 9b7697e4..00000000 --- a/src/Http/Validator/Assoc.php +++ /dev/null @@ -1,88 +0,0 @@ -length = $length; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid object.'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_ARRAY; - } - - /** - * Is valid - * - * Validation will pass when $value is valid assoc array. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - if (!\is_array($value)) { - return false; - } - - $jsonString = \json_encode($value); - $jsonStringSize = \strlen($jsonString); - - if ($jsonStringSize > $this->length) { - return false; - } - - return \array_keys($value) !== \range(0, \count($value) - 1); - } -} diff --git a/src/Http/Validator/Boolean.php b/src/Http/Validator/Boolean.php deleted file mode 100644 index 38b498ec..00000000 --- a/src/Http/Validator/Boolean.php +++ /dev/null @@ -1,94 +0,0 @@ -loose = $loose; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid boolean'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_BOOLEAN; - } - - /** - * Is valid - * - * Validation will pass when $value has a boolean value. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - if ($this->loose && ($value === 'true' || $value === 'false')) { // Accept strings - return true; - } - - if ($this->loose && ($value === '1' || $value === '0')) { // Accept numeric strings - return true; - } - - if ($this->loose && ($value === 1 || $value === 0)) { // Accept integers - return true; - } - - if (\is_bool($value)) { - return true; - } - - return false; - } -} diff --git a/src/Http/Validator/Domain.php b/src/Http/Validator/Domain.php deleted file mode 100644 index dd213191..00000000 --- a/src/Http/Validator/Domain.php +++ /dev/null @@ -1,78 +0,0 @@ -loose = $loose; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid float'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_FLOAT; - } - - /** - * Is valid - * - * Validation will pass when $value is float. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if ($this->loose) { - if (!\is_numeric($value)) { - return false; - } - $value = $value + 0; - } - if (!\is_float($value) && !\is_int($value)) { - return false; - } - - return true; - } -} diff --git a/src/Http/Validator/HexColor.php b/src/Http/Validator/HexColor.php deleted file mode 100644 index fe9d5926..00000000 --- a/src/Http/Validator/HexColor.php +++ /dev/null @@ -1,53 +0,0 @@ -whitelist = $whitelist; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'URL host must be one of: ' . \implode(', ', $this->whitelist); - } - - /** - * Is valid - * - * Validation will pass when $value starts with one of the given hosts - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - $urlValidator = new URL(); - - if (!$urlValidator->isValid($value)) { - return false; - } - - $hostnameValidator = new Hostname($this->whitelist); - - return $hostnameValidator->isValid(\parse_url($value, PHP_URL_HOST)); - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Http/Validator/Hostname.php b/src/Http/Validator/Hostname.php deleted file mode 100644 index d3231f10..00000000 --- a/src/Http/Validator/Hostname.php +++ /dev/null @@ -1,114 +0,0 @@ -allowList = $allowList; - } - - /** - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid hostname without path, port and protocol.'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } - - /** - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - // Validate proper format - if (!\is_string($value) || empty($value)) { - return false; - } - - // Max length 253 chars: https://en.wikipedia.org/wiki/Hostname#:~:text=The%20entire%20hostname%2C%20including%20the,maximum%20of%20253%20ASCII%20characters - if (\mb_strlen($value) > 253) { - return false; - } - - // This tests: 'http://', 'https://', and 'myapp.com/route' - if (\str_contains($value, '/')) { - return false; - } - - // This tests for: 'myapp.com:3000' - if (\str_contains($value, ':')) { - return false; - } - - // Logic #1: Empty allowList means everything is allowed - if (empty($this->allowList)) { - return true; - } - - // Logic #2: Allow List not empty, there are rules to check - // Loop through all allowed hostnames until match is found - foreach ($this->allowList as $allowedHostname) { - // If exact match; allow - // If *, allow everything - if ($value === $allowedHostname || $allowedHostname === '*') { - return true; - } - - // If wildcard symbol used - if (\str_starts_with($allowedHostname, '*')) { - // Remove starting * symbol before comparing - $allowedHostname = substr($allowedHostname, 1); - - // If rest of hostname match; allow - // Notice allowedHostname still includes starting dot. Root domain is NOT allowed by wildcard. - if (\str_ends_with($value, $allowedHostname)) { - return true; - } - } - } - - // If finished loop above without result, match is not found - return false; - } -} diff --git a/src/Http/Validator/IP.php b/src/Http/Validator/IP.php deleted file mode 100644 index e0ffef49..00000000 --- a/src/Http/Validator/IP.php +++ /dev/null @@ -1,113 +0,0 @@ -type = $type; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid IP address'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid IP address. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - switch ($this->type) { - case self::ALL: - if (\filter_var($value, FILTER_VALIDATE_IP)) { - return true; - } - break; - - case self::V4: - if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { - return true; - } - break; - - case self::V6: - if (\filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { - return true; - } - break; - - default: - return false; - } - - return false; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Http/Validator/Integer.php b/src/Http/Validator/Integer.php deleted file mode 100755 index 76af2aa4..00000000 --- a/src/Http/Validator/Integer.php +++ /dev/null @@ -1,88 +0,0 @@ -loose = $loose; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid integer'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_INTEGER; - } - - /** - * Is valid - * - * Validation will pass when $value is integer. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if ($this->loose) { - if (!\is_numeric($value)) { - return false; - } - $value = $value + 0; - } - if (!\is_int($value)) { - return false; - } - - return true; - } -} diff --git a/src/Http/Validator/JSON.php b/src/Http/Validator/JSON.php deleted file mode 100644 index 5bb734f6..00000000 --- a/src/Http/Validator/JSON.php +++ /dev/null @@ -1,59 +0,0 @@ - $validators - */ - public function __construct(protected array $validators, protected string $type = self::TYPE_MIXED) - { - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - $description = ''; - - if (!(\is_null($this->failedRule))) { - $description = $this->failedRule->getDescription(); - } else { - $description = $this->validators[0]->getDescription(); - } - - return $description; - } - - /** - * Is valid - * - * Validation will pass when all rules are valid if only one of the rules is invalid validation will fail. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - foreach ($this->validators as $rule) { - $valid = $rule->isValid($value); - - if ($valid) { - $this->failedRule = $rule; - return false; - } - } - - return true; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return true; - } -} diff --git a/src/Http/Validator/Nullable.php b/src/Http/Validator/Nullable.php deleted file mode 100644 index b6c0007b..00000000 --- a/src/Http/Validator/Nullable.php +++ /dev/null @@ -1,73 +0,0 @@ -validator->getDescription() . ' or null'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->validator->getType(); - } - - /** - * @return Validator - */ - public function getValidator(): Validator - { - return $this->validator; - } - - /** - * Is valid - * - * Validation will pass when $value is text with valid length. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if (\is_null($value)) { - return true; - } - - return $this->validator->isValid($value); - } -} diff --git a/src/Http/Validator/Numeric.php b/src/Http/Validator/Numeric.php deleted file mode 100755 index 13c88610..00000000 --- a/src/Http/Validator/Numeric.php +++ /dev/null @@ -1,66 +0,0 @@ -min = $min; - $this->max = $max; - $this->format = $format; - } - - /** - * Get Range Minimum Value - * - * @return int|float - */ - public function getMin(): int|float - { - return $this->min; - } - - /** - * Get Range Maximum Value - * - * @return int|float - */ - public function getMax(): int|float - { - return $this->max; - } - - /** - * Get Range Format - * - * @return string - */ - public function getFormat(): string - { - return $this->format; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be a valid range between '.\number_format($this->min).' and '.\number_format($this->max); - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->format; - } - - /** - * Is valid - * - * Validation will pass when $value number is bigger or equal than $min number and lower or equal than $max. - * Not strict, considers any valid integer to be a valid float - * Considers infinity to be a valid integer - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if (!parent::isValid($value)) { - return false; - } - - switch ($this->format) { - case self::TYPE_INTEGER: - // Accept infinity as an integer - // Since gettype(INF) === TYPE_FLOAT - if ($value === INF || $value === -INF) { - break; // move to check if value is within range - } - $value = $value + 0; - if (!is_int($value)) { - return false; - } - break; - case self::TYPE_FLOAT: - if (!is_numeric($value)) { - return false; - } - $value = $value + 0.0; - break; - default: - return false; - } - - if ($this->min <= $value && $this->max >= $value) { - return true; - } - - return false; - } -} diff --git a/src/Http/Validator/Text.php b/src/Http/Validator/Text.php deleted file mode 100644 index 4df5d7e4..00000000 --- a/src/Http/Validator/Text.php +++ /dev/null @@ -1,138 +0,0 @@ -length = $length; - $this->min = $min; - $this->allowList = $allowList; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - $message = 'Value must be a valid string'; - - if ($this->min === $this->length) { - $message .= ' and exactly '.$this->length.' chars'; - } else { - if ($this->min) { - $message .= ' and at least '.$this->min.' chars'; - } - - if ($this->length) { - $message .= ' and no longer than '.$this->length.' chars'; - } - } - - if ($this->allowList) { - $message .= ' and only consist of \''.\implode(', ', $this->allowList).'\' chars'; - } - - return $message; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } - - /** - * Is valid - * - * Validation will pass when $value is text with valid length. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if (!\is_string($value)) { - return false; - } - - if (\mb_strlen($value) < $this->min) { - return false; - } - - if (\mb_strlen($value) > $this->length && $this->length !== 0) { - return false; - } - - if (\count($this->allowList) > 0) { - foreach (\str_split($value) as $char) { - if (!\in_array($char, $this->allowList)) { - return false; - } - } - } - - return true; - } -} diff --git a/src/Http/Validator/URL.php b/src/Http/Validator/URL.php deleted file mode 100644 index f6434e98..00000000 --- a/src/Http/Validator/URL.php +++ /dev/null @@ -1,86 +0,0 @@ -allowedSchemes = $allowedSchemes; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - if (!empty($this->allowedSchemes)) { - return 'Value must be a valid URL with following schemes (' . \implode(', ', $this->allowedSchemes) . ')'; - } - - return 'Value must be a valid URL'; - } - - /** - * Is valid - * - * Validation will pass when $value is valid URL. - * - * @param mixed $value - * @return bool - */ - public function isValid($value): bool - { - if (\filter_var($value, FILTER_VALIDATE_URL) === false) { - return false; - } - - if (!empty($this->allowedSchemes) && !\in_array(\parse_url($value, PHP_URL_SCHEME), $this->allowedSchemes)) { - return false; - } - - return true; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return self::TYPE_STRING; - } -} diff --git a/src/Http/Validator/WhiteList.php b/src/Http/Validator/WhiteList.php deleted file mode 100755 index 53a6bb16..00000000 --- a/src/Http/Validator/WhiteList.php +++ /dev/null @@ -1,119 +0,0 @@ -list = $list; - $this->strict = $strict; - $this->type = $type; - - if (!$this->strict) { - foreach ($this->list as $key => &$value) { - $this->list[$key] = \strtolower($value); - } - } - } - - /** - * Get List of All Allowed Values - * - * @return array - */ - public function getList(): array - { - return $this->list; - } - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - public function getDescription(): string - { - return 'Value must be one of ('.\implode(', ', $this->list).')'; - } - - /** - * Is array - * - * Function will return true if object is array. - * - * @return bool - */ - public function isArray(): bool - { - return false; - } - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * Is valid - * - * Validation will pass if $value is in the white list array. - * - * @param mixed $value - * @return bool - */ - public function isValid(mixed $value): bool - { - if (\is_array($value)) { - return false; - } - - $value = ($this->strict) ? $value : \strtolower($value); - - if (!\in_array($value, $this->list, $this->strict)) { - return false; - } - - return true; - } -} diff --git a/src/Http/Validator/Wildcard.php b/src/Http/Validator/Wildcard.php deleted file mode 100644 index 1e7314a8..00000000 --- a/src/Http/Validator/Wildcard.php +++ /dev/null @@ -1,62 +0,0 @@ -assertFalse($arrayList->isValid(['text'])); - $this->assertEquals('Value must a valid array and Value must be a valid integer', $arrayList->getDescription()); - - $arrayList = new ArrayList(new Integer(), 3); - $this->assertFalse($arrayList->isValid(['a', 'b', 'c', 'd'])); - $this->assertEquals('Value must a valid array no longer than 3 items and Value must be a valid integer', $arrayList->getDescription()); - } - - public function testCanValidateTextValues(): void - { - $arrayList = new ArrayList(new Text(100)); - $this->assertTrue($arrayList->isArray(), 'true'); - $this->assertTrue($arrayList->isValid([0 => 'string', 1 => 'string'])); - $this->assertTrue($arrayList->isValid(['string', 'string'])); - $this->assertFalse($arrayList->isValid(['string', 'string', 3])); - $this->assertFalse($arrayList->isValid('string')); - $this->assertFalse($arrayList->isValid('string')); - $this->assertEquals(\Utopia\Http\Validator::TYPE_STRING, $arrayList->getType()); - $this->assertInstanceOf(Text::class, $arrayList->getValidator()); - } - - public function testCanValidateNumericValues(): void - { - $arrayList = new ArrayList(new Numeric()); - $this->assertTrue($arrayList->isValid([1, 2, 3])); - $this->assertFalse($arrayList->isValid(1)); - $this->assertFalse($arrayList->isValid('string')); - $this->assertEquals(\Utopia\Http\Validator::TYPE_MIXED, $arrayList->getType()); - $this->assertInstanceOf(Numeric::class, $arrayList->getValidator()); - } - - public function testCanValidateNumericValuesWithBoundaries(): void - { - $arrayList = new ArrayList(new Numeric(), 2); - $this->assertTrue($arrayList->isValid([1])); - $this->assertTrue($arrayList->isValid([1, 2])); - $this->assertFalse($arrayList->isValid([1, 2, 3])); - $this->assertEquals($arrayList->getType(), \Utopia\Http\Validator::TYPE_MIXED); - $this->assertInstanceOf(Numeric::class, $arrayList->getValidator()); - } -} diff --git a/tests/Validator/AssocTest.php b/tests/Validator/AssocTest.php deleted file mode 100755 index 2036998b..00000000 --- a/tests/Validator/AssocTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assoc = new Assoc(); - } - - public function tearDown(): void - { - $this->assoc = null; - } - - public function testCanValidateAssocArray(): void - { - $this->assertTrue($this->assoc->isValid(['1' => 'a', '0' => 'b', '2' => 'c'])); - $this->assertTrue($this->assoc->isValid(['a' => 'a', 'b' => 'b', 'c' => 'c'])); - $this->assertTrue($this->assoc->isValid([])); - $this->assertTrue($this->assoc->isValid(['value' => str_repeat('-', 62000)])); - $this->assertTrue($this->assoc->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_ARRAY, $this->assoc->getType()); - } - - public function testCantValidateSequentialArray(): void - { - $this->assertFalse($this->assoc->isValid([0 => 'string', 1 => 'string'])); - $this->assertFalse($this->assoc->isValid(['a'])); - $this->assertFalse($this->assoc->isValid(['a', 'b', 'c'])); - $this->assertFalse($this->assoc->isValid(['0' => 'a', '1' => 'b', '2' => 'c'])); - } - - public function testCantValidateAssocArrayWithOver65kCharacters(): void - { - $this->assertFalse($this->assoc->isValid(['value' => str_repeat('-', 66000)])); - } -} diff --git a/tests/Validator/BooleanTest.php b/tests/Validator/BooleanTest.php deleted file mode 100755 index bda71580..00000000 --- a/tests/Validator/BooleanTest.php +++ /dev/null @@ -1,46 +0,0 @@ -assertTrue($boolean->isValid(true)); - $this->assertTrue($boolean->isValid(false)); - $this->assertFalse($boolean->isValid('false')); - $this->assertFalse($boolean->isValid('true')); - $this->assertFalse($boolean->isValid('0')); - $this->assertFalse($boolean->isValid('1')); - $this->assertFalse($boolean->isValid(0)); - $this->assertFalse($boolean->isValid(1)); - $this->assertFalse($boolean->isValid(['string', 'string'])); - $this->assertFalse($boolean->isValid('string')); - $this->assertFalse($boolean->isValid(1.2)); - $this->assertFalse($boolean->isArray()); - $this->assertEquals($boolean->getType(), \Utopia\Http\Validator::TYPE_BOOLEAN); - } - - public function testCanValidateLoosely() - { - $boolean = new Boolean(true); - - $this->assertTrue($boolean->isValid(true)); - $this->assertTrue($boolean->isValid(false)); - $this->assertTrue($boolean->isValid('false')); - $this->assertTrue($boolean->isValid('true')); - $this->assertTrue($boolean->isValid('0')); - $this->assertTrue($boolean->isValid('1')); - $this->assertTrue($boolean->isValid(0)); - $this->assertTrue($boolean->isValid(1)); - $this->assertFalse($boolean->isValid(['string', 'string'])); - $this->assertFalse($boolean->isValid('string')); - $this->assertFalse($boolean->isValid(1.2)); - $this->assertFalse($boolean->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_BOOLEAN, $boolean->getType()); - } -} diff --git a/tests/Validator/DomainTest.php b/tests/Validator/DomainTest.php deleted file mode 100644 index 85a346e1..00000000 --- a/tests/Validator/DomainTest.php +++ /dev/null @@ -1,37 +0,0 @@ -domain = new Domain(); - } - - public function testIsValid() - { - // Assertions - $this->assertEquals(true, $this->domain->isValid('example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example-app.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain.example_app.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain-new.example.com')); - $this->assertEquals(true, $this->domain->isValid('subdomain_new.example.com')); - $this->assertEquals(true, $this->domain->isValid('localhost')); - $this->assertEquals(true, $this->domain->isValid('example.io')); - $this->assertEquals(true, $this->domain->isValid('example.org')); - $this->assertEquals(true, $this->domain->isValid('example.org')); - $this->assertEquals(false, $this->domain->isValid(false)); - $this->assertEquals(false, $this->domain->isValid('.')); - $this->assertEquals(false, $this->domain->isValid('..')); - $this->assertEquals(false, $this->domain->isValid('')); - $this->assertEquals(false, $this->domain->isValid(['string', 'string'])); - $this->assertEquals(false, $this->domain->isValid(1)); - $this->assertEquals(false, $this->domain->isValid(1.2)); - } -} diff --git a/tests/Validator/FloatValidatorTest.php b/tests/Validator/FloatValidatorTest.php deleted file mode 100755 index be905b68..00000000 --- a/tests/Validator/FloatValidatorTest.php +++ /dev/null @@ -1,39 +0,0 @@ -assertTrue($validator->isValid(27.25)); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid(23.5)); - $this->assertTrue($validator->isValid(1e7)); - $this->assertFalse($validator->isValid('abc')); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid('23')); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_FLOAT, $validator->getType()); - } - - public function testCanValidateLoosely(): void - { - $validator = new FloatValidator(true); - - $this->assertTrue($validator->isValid(27.25)); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid(23.5)); - $this->assertTrue($validator->isValid(1e7)); - $this->assertTrue($validator->isValid('23.5')); - $this->assertTrue($validator->isValid('23')); - $this->assertFalse($validator->isValid('abc')); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_FLOAT, $validator->getType()); - } -} diff --git a/tests/Validator/HexColorTest.php b/tests/Validator/HexColorTest.php deleted file mode 100755 index f90d92fe..00000000 --- a/tests/Validator/HexColorTest.php +++ /dev/null @@ -1,26 +0,0 @@ -assertTrue($hexColor->isValid('000')); - $this->assertTrue($hexColor->isValid('ffffff')); - $this->assertTrue($hexColor->isValid('fff')); - $this->assertTrue($hexColor->isValid('000000')); - - $this->assertFalse($hexColor->isValid('AB10BC99')); - $this->assertFalse($hexColor->isValid('AR1012')); - $this->assertFalse($hexColor->isValid('ab12bc99')); - $this->assertFalse($hexColor->isValid('00')); - $this->assertFalse($hexColor->isValid('ffff')); - $this->assertFalse($hexColor->isArray()); - - $this->assertEquals(\Utopia\Http\Validator::TYPE_STRING, $hexColor->getType()); - } -} diff --git a/tests/Validator/HostTest.php b/tests/Validator/HostTest.php deleted file mode 100644 index 4162a5b3..00000000 --- a/tests/Validator/HostTest.php +++ /dev/null @@ -1,30 +0,0 @@ -host = new Host(['example.io', 'subdomain.example.test', 'localhost', '*.appwrite.io']); - } - - public function testIsValid() - { - // Assertions - $this->assertEquals($this->host->isValid('https://example.io/link'), true); - $this->assertEquals($this->host->isValid('https://localhost'), true); - $this->assertEquals($this->host->isValid('localhost'), false); - $this->assertEquals($this->host->isValid('http://subdomain.example.test/path'), true); - $this->assertEquals($this->host->isValid('http://test.subdomain.example.test/path'), false); - $this->assertEquals($this->host->isValid('http://appwrite.io/path'), false); - $this->assertEquals($this->host->isValid('http://me.appwrite.io/path'), true); - $this->assertEquals($this->host->isValid('http://you.appwrite.io/path'), true); - $this->assertEquals($this->host->isValid('http://us.together.appwrite.io/path'), true); - $this->assertEquals($this->host->getType(), 'string'); - } -} diff --git a/tests/Validator/HostnameTest.php b/tests/Validator/HostnameTest.php deleted file mode 100755 index 2760648d..00000000 --- a/tests/Validator/HostnameTest.php +++ /dev/null @@ -1,106 +0,0 @@ -assertEquals(\Utopia\Http\Validator::TYPE_STRING, $validator->getType()); - $this->assertFalse($validator->isArray()); - - $this->assertTrue($validator->isValid('myweb.com')); - $this->assertTrue($validator->isValid('httpmyweb.com')); - $this->assertTrue($validator->isValid('httpsmyweb.com')); - $this->assertTrue($validator->isValid('wsmyweb.com')); - $this->assertTrue($validator->isValid('wssmyweb.com')); - $this->assertTrue($validator->isValid('vercel.app')); - $this->assertTrue($validator->isValid('web.vercel.app')); - $this->assertTrue($validator->isValid('my-web.vercel.app')); - $this->assertTrue($validator->isValid('my-project.my-web.vercel.app')); - $this->assertTrue($validator->isValid('my-commit.my-project.my-web.vercel.app')); - $this->assertTrue($validator->isValid('myapp.co.uk')); - $this->assertTrue($validator->isValid('*.myapp.com')); - $this->assertTrue($validator->isValid('myapp.*')); - - $this->assertFalse($validator->isValid('https://myweb.com')); - $this->assertFalse($validator->isValid('ws://myweb.com')); - $this->assertFalse($validator->isValid('wss://myweb.com')); - $this->assertFalse($validator->isValid('http://myweb.com')); - $this->assertFalse($validator->isValid('http://myweb.com:3000')); - $this->assertFalse($validator->isValid('http://myweb.com/blog')); - $this->assertFalse($validator->isValid('myweb.com:80')); - $this->assertFalse($validator->isValid('myweb.com:3000')); - $this->assertFalse($validator->isValid('myweb.com/blog')); - $this->assertFalse($validator->isValid('myweb.com/blog/article1')); - - // Max length test - $domain = \str_repeat('bestdomain', 25); // 250 chars total - - $domain .= '.sk'; // Exactly at the limit - $this->assertTrue($validator->isValid($domain)); - - $domain .= 'a'; // Exactly over the limit - $this->assertFalse($validator->isValid($domain)); - } - - public function testCanValidateHostnamesWithAllowList(): void - { - // allowList tests - $validator = new Hostname([ - 'myweb.vercel.app', - 'myweb.com', - '*.myapp.com', - ]); - - $this->assertTrue($validator->isValid('myweb.vercel.app')); - $this->assertFalse($validator->isValid('myweb.vercel.com')); - $this->assertFalse($validator->isValid('myweb2.vercel.app')); - $this->assertFalse($validator->isValid('vercel.app')); - $this->assertFalse($validator->isValid('mycommit.myweb.vercel.app')); - - $this->assertTrue($validator->isValid('myweb.com')); - $this->assertFalse($validator->isValid('myweb.eu')); - $this->assertFalse($validator->isValid('project.myweb.eu')); - $this->assertFalse($validator->isValid('commit.project.myweb.eu')); - - $this->assertTrue($validator->isValid('project1.myapp.com')); - $this->assertTrue($validator->isValid('project2.myapp.com')); - $this->assertTrue($validator->isValid('project-with-dash.myapp.com')); - $this->assertTrue($validator->isValid('anything.myapp.com')); - $this->assertTrue($validator->isValid('commit.anything.myapp.com')); - $this->assertFalse($validator->isValid('anything.myapp.eu')); - $this->assertFalse($validator->isValid('myapp.com')); - - $validator = new Hostname(['localhost']); - $this->assertTrue($validator->isValid('localhost')); - } - - public function testCanValidateHostnamesWithWildcard(): void - { - $validator = new Hostname(); - $this->assertTrue($validator->isValid('*')); - - $validator = new Hostname(['netlify.*']); - $this->assertFalse($validator->isValid('netlify.com')); - $this->assertFalse($validator->isValid('netlify.eu')); - $this->assertFalse($validator->isValid('netlify.app')); - - $validator = new Hostname(['*.*.app.io']); - $this->assertFalse($validator->isValid('app.io')); - $this->assertFalse($validator->isValid('project.app.io')); - $this->assertFalse($validator->isValid('commit.project.app.io')); - $this->assertFalse($validator->isValid('api.commit.project.app.io')); - - $validator = new Hostname(['*']); - $this->assertTrue($validator->isValid('*')); - $this->assertTrue($validator->isValid('localhost')); - $this->assertTrue($validator->isValid('anything')); // Like localhost - $this->assertTrue($validator->isValid('anything.com')); - $this->assertTrue($validator->isValid('anything.with.subdomains.eu')); - } -} diff --git a/tests/Validator/IPTest.php b/tests/Validator/IPTest.php deleted file mode 100644 index 074a8f68..00000000 --- a/tests/Validator/IPTest.php +++ /dev/null @@ -1,67 +0,0 @@ -assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); - $this->assertEquals($validator->getType(), 'string'); - } - - public function testIsValidIPALL() - { - $validator = new IP(IP::ALL); - - // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); - } - - public function testIsValidIPV4() - { - $validator = new IP(IP::V4); - - // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), false); - $this->assertEquals($validator->isValid('109.67.204.101'), true); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); - } - - public function testIsValidIPV6() - { - $validator = new IP(IP::V6); - - // Assertions - $this->assertEquals($validator->isValid('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), true); - $this->assertEquals($validator->isValid('109.67.204.101'), false); - $this->assertEquals($validator->isValid(23.5), false); - $this->assertEquals($validator->isValid('23.5'), false); - $this->assertEquals($validator->isValid(null), false); - $this->assertEquals($validator->isValid(true), false); - $this->assertEquals($validator->isValid(false), false); - } -} diff --git a/tests/Validator/IntegerTest.php b/tests/Validator/IntegerTest.php deleted file mode 100755 index faddca2c..00000000 --- a/tests/Validator/IntegerTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertTrue($validator->isValid(23)); - $this->assertFalse($validator->isValid('23')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_INTEGER, $validator->getType()); - } - - public function testCanValidateLoosely() - { - $validator = new Integer(true); - $this->assertTrue($validator->isValid(23)); - $this->assertTrue($validator->isValid('23')); - $this->assertFalse($validator->isValid(23.5)); - $this->assertFalse($validator->isValid('23.5')); - $this->assertFalse($validator->isValid(null)); - $this->assertFalse($validator->isValid(true)); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_INTEGER, $validator->getType()); - } -} diff --git a/tests/Validator/JSONTest.php b/tests/Validator/JSONTest.php deleted file mode 100755 index d4a4e358..00000000 --- a/tests/Validator/JSONTest.php +++ /dev/null @@ -1,28 +0,0 @@ -assertTrue($json->isValid('{}')); - $this->assertTrue($json->isValid([])); - $this->assertTrue($json->isValid(['test'])); - $this->assertTrue($json->isValid(['test' => 'demo'])); - $this->assertTrue($json->isValid('{"test": "demo"}')); - - $this->assertFalse($json->isValid('')); - $this->assertFalse($json->isValid(false)); - $this->assertFalse($json->isValid(null)); - $this->assertFalse($json->isValid('string')); - $this->assertFalse($json->isValid(1)); - $this->assertFalse($json->isValid(1.2)); - $this->assertFalse($json->isValid("{'test': 'demo'}")); - $this->assertFalse($json->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_OBJECT, $json->getType()); - } -} diff --git a/tests/Validator/MultipleOfTest.php b/tests/Validator/MultipleOfTest.php deleted file mode 100644 index 06435aa8..00000000 --- a/tests/Validator/MultipleOfTest.php +++ /dev/null @@ -1,61 +0,0 @@ -assertEquals('string', $validator->getType()); - $this->assertEquals("Value must be a valid string and at least 1 chars and no longer than 20 chars", $validator->getDescription()); - - // Valid URL but invalid text length - $this->assertFalse($validator->isValid('http://example.com/very-long-url')); - - // Valid text within length, but invalid URL - $this->assertFalse($validator->isValid('hello world')); - - // Both conditions satisfied - $this->assertTrue($validator->isValid('http://example.com')); - $this->assertTrue($validator->isValid('https://google.com')); - - // Neither condition satisfied - $this->assertFalse($validator->isValid('example.com/hello-world')); - $this->assertFalse($validator->isValid('')); - } - - public function testRules() - { - $validTextValidUrl = 'http://example.com'; - $validTextInvalidUrl = 'hello world'; - $invalidTextValidUrl = 'http://example.com/very-long-url'; - $invalidTextInvalidUrl = 'Some very long text that is also not an URL'; - - $vaidator = new AllOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertTrue($vaidator->isValid($validTextValidUrl)); - $this->assertFalse($vaidator->isValid($validTextInvalidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextValidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextInvalidUrl)); - - $vaidator = new AnyOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertTrue($vaidator->isValid($validTextValidUrl)); - $this->assertTrue($vaidator->isValid($validTextInvalidUrl)); - $this->assertTrue($vaidator->isValid($invalidTextValidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextInvalidUrl)); - - $vaidator = new NoneOf([new Text(20), new URL()], Validator::TYPE_STRING); - $this->assertFalse($vaidator->isValid($validTextValidUrl)); - $this->assertFalse($vaidator->isValid($validTextInvalidUrl)); - $this->assertFalse($vaidator->isValid($invalidTextValidUrl)); - $this->assertTrue($vaidator->isValid($invalidTextInvalidUrl)); - } -} diff --git a/tests/Validator/NullableTest.php b/tests/Validator/NullableTest.php deleted file mode 100755 index ebd24834..00000000 --- a/tests/Validator/NullableTest.php +++ /dev/null @@ -1,22 +0,0 @@ -assertTrue($validator->isValid('text')); - $this->assertTrue($validator->isValid(null)); - $this->assertFalse($validator->isValid(123)); - } - - public function testCanReturnValidator(): void - { - $validator = new Nullable(new Text(0)); - $this->assertTrue($validator->getValidator() instanceof Text); - } -} diff --git a/tests/Validator/NumericTest.php b/tests/Validator/NumericTest.php deleted file mode 100755 index 88ea5030..00000000 --- a/tests/Validator/NumericTest.php +++ /dev/null @@ -1,24 +0,0 @@ -assertTrue($numeric->isValid('42')); - $this->assertTrue($numeric->isValid(1337)); - $this->assertTrue($numeric->isValid(0x539)); - $this->assertTrue($numeric->isValid(02471)); - $this->assertTrue($numeric->isValid(1337e0)); - $this->assertTrue($numeric->isValid(9.1)); - $this->assertFalse($numeric->isValid('not numeric')); - $this->assertFalse($numeric->isValid([])); - $this->assertFalse($numeric->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_MIXED, $numeric->getType()); - } -} diff --git a/tests/Validator/RangeTest.php b/tests/Validator/RangeTest.php deleted file mode 100755 index aafbab4a..00000000 --- a/tests/Validator/RangeTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertTrue($range->isValid(0)); - $this->assertTrue($range->isValid(1)); - $this->assertTrue($range->isValid(4)); - $this->assertTrue($range->isValid(5)); - $this->assertTrue($range->isValid('5')); - $this->assertFalse($range->isValid('1.5')); - $this->assertFalse($range->isValid(6)); - $this->assertFalse($range->isValid(-1)); - $this->assertEquals(0, $range->getMin()); - $this->assertEquals(5, $range->getMax()); - $this->assertFalse($range->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_INTEGER, $range->getFormat()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_INTEGER, $range->getType()); - } - - public function testCanValidateFloatRange() - { - $range = new Range(0, 1, \Utopia\Http\Validator::TYPE_FLOAT); - - $this->assertTrue($range->isValid(0.0)); - $this->assertTrue($range->isValid(1.0)); - $this->assertTrue($range->isValid(0.5)); - $this->assertTrue($range->isValid('0.5')); - $this->assertTrue($range->isValid('0.6')); - $this->assertFalse($range->isValid(4)); - $this->assertFalse($range->isValid(1.5)); - $this->assertFalse($range->isValid(-1)); - $this->assertEquals(0, $range->getMin()); - $this->assertEquals(1, $range->getMax()); - $this->assertFalse($range->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_FLOAT, $range->getFormat()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_FLOAT, $range->getType(), \Utopia\Http\Validator::TYPE_FLOAT); - } - - public function canValidateInfinityRange() - { - $integer = new Range(5, INF, \Utopia\Http\Validator::TYPE_INTEGER); - $float = new Range(-INF, 45.6, \Utopia\Http\Validator::TYPE_FLOAT); - - $this->assertTrue($integer->isValid(25)); - $this->assertFalse($integer->isValid(3)); - $this->assertTrue($integer->isValid(INF)); - $this->assertTrue($float->isValid(32.1)); - $this->assertFalse($float->isValid(97.6)); - $this->assertTrue($float->isValid(-INF)); - } -} diff --git a/tests/Validator/TextTest.php b/tests/Validator/TextTest.php deleted file mode 100755 index 476cfd8a..00000000 --- a/tests/Validator/TextTest.php +++ /dev/null @@ -1,88 +0,0 @@ -assertTrue($validator->isValid('text')); - $this->assertTrue($validator->isValid('7')); - $this->assertTrue($validator->isValid('7.9')); - $this->assertTrue($validator->isValid('["seven"]')); - $this->assertFalse($validator->isValid(['seven'])); - $this->assertFalse($validator->isValid(['seven', 8, 9.0])); - $this->assertFalse($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_STRING, $validator->getType()); - } - - public function testCanValidateBoundaries(): void - { - $validator = new Text(5); - $this->assertTrue($validator->isValid('hell')); - $this->assertTrue($validator->isValid('hello')); - $this->assertFalse($validator->isValid('hellow')); - $this->assertFalse($validator->isValid('')); - - $validator = new Text(5, 3); - $this->assertTrue($validator->isValid('hel')); - $this->assertTrue($validator->isValid('hell')); - $this->assertTrue($validator->isValid('hello')); - $this->assertFalse($validator->isValid('hellow')); - $this->assertFalse($validator->isValid('he')); - $this->assertFalse($validator->isValid('h')); - } - - public function testCanValidateTextWithAllowList(): void - { - // Test lowercase alphabet - $validator = new Text(100, allowList: Text::ALPHABET_LOWER); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('qwertzuiopasdfghjklyxcvbnm')); - $this->assertTrue($validator->isValid('hello')); - $this->assertTrue($validator->isValid('world')); - $this->assertFalse($validator->isValid('hello world')); - $this->assertFalse($validator->isValid('Hello')); - $this->assertFalse($validator->isValid('worlD')); - $this->assertFalse($validator->isValid('hello123')); - - // Test uppercase alphabet - $validator = new Text(100, allowList: Text::ALPHABET_UPPER); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNM')); - $this->assertTrue($validator->isValid('HELLO')); - $this->assertTrue($validator->isValid('WORLD')); - $this->assertFalse($validator->isValid('HELLO WORLD')); - $this->assertFalse($validator->isValid('hELLO')); - $this->assertFalse($validator->isValid('WORLd')); - $this->assertFalse($validator->isValid('HELLO123')); - - // Test numbers - $validator = new Text(100, allowList: Text::NUMBERS); - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('1234567890')); - $this->assertTrue($validator->isValid('123')); - $this->assertFalse($validator->isValid('123 456')); - $this->assertFalse($validator->isValid('hello123')); - - // Test combination of allowLists - $validator = new Text(100, allowList: [ - ...Text::ALPHABET_LOWER, - ...Text::ALPHABET_UPPER, - ...Text::NUMBERS, - ]); - - $this->assertFalse($validator->isArray()); - $this->assertTrue($validator->isValid('1234567890')); - $this->assertTrue($validator->isValid('qwertzuiopasdfghjklyxcvbnm')); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNM')); - $this->assertTrue($validator->isValid('QWERTZUIOPASDFGHJKLYXCVBNMqwertzuiopasdfghjklyxcvbnm1234567890')); - $this->assertFalse($validator->isValid('hello-world')); - $this->assertFalse($validator->isValid('hello_world')); - $this->assertFalse($validator->isValid('hello/world')); - } -} diff --git a/tests/Validator/URLTest.php b/tests/Validator/URLTest.php deleted file mode 100644 index de530cd1..00000000 --- a/tests/Validator/URLTest.php +++ /dev/null @@ -1,43 +0,0 @@ -url = new URL(); - } - - public function tearDown(): void - { - $this->url = null; - } - - public function testIsValid(): void - { - $this->assertEquals('Value must be a valid URL', $this->url->getDescription()); - $this->assertEquals(true, $this->url->isValid('http://example.com')); - $this->assertEquals(true, $this->url->isValid('https://example.com')); - $this->assertEquals(true, $this->url->isValid('htts://example.com')); // does not validate protocol - $this->assertEquals(false, $this->url->isValid('example.com')); // though, requires some kind of protocol - $this->assertEquals(false, $this->url->isValid('http:/example.com')); - $this->assertEquals(true, $this->url->isValid('http://exa-mple.com')); - $this->assertEquals(false, $this->url->isValid('htt@s://example.com')); - $this->assertEquals(true, $this->url->isValid('http://www.example.com/foo%2\u00c2\u00a9zbar')); - $this->assertEquals(true, $this->url->isValid('http://www.example.com/?q=%3Casdf%3E')); - } - - public function testIsValidAllowedSchemes(): void - { - $this->url = new URL(['http', 'https']); - $this->assertEquals('Value must be a valid URL with following schemes (http, https)', $this->url->getDescription()); - $this->assertEquals(true, $this->url->isValid('http://example.com')); - $this->assertEquals(true, $this->url->isValid('https://example.com')); - $this->assertEquals(false, $this->url->isValid('gopher://www.example.com')); - } -} diff --git a/tests/Validator/WhiteListTest.php b/tests/Validator/WhiteListTest.php deleted file mode 100755 index f024908c..00000000 --- a/tests/Validator/WhiteListTest.php +++ /dev/null @@ -1,58 +0,0 @@ -assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid('STRING1')); - $this->assertFalse($whiteList->isValid('strIng1')); - $this->assertFalse($whiteList->isValid('3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertFalse($whiteList->isArray()); - $this->assertEquals($whiteList->getList(), ['string1', 'string2', 3, 4]); - $this->assertEquals(\Utopia\Http\Validator::TYPE_STRING, $whiteList->getType()); - } - - public function testCanValidateLoosely(): void - { - $whiteList = new WhiteList(['string1', 'string2', 3, 4]); - - $this->assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - $this->assertTrue($whiteList->isValid('STRING1')); - $this->assertTrue($whiteList->isValid('strIng1')); - $this->assertTrue($whiteList->isValid('3')); - $this->assertTrue($whiteList->isValid('4')); - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertEquals($whiteList->getList(), ['string1', 'string2', 3, 4]); - - $whiteList = new WhiteList(['STRING1', 'STRING2', 3, 4]); - - $this->assertTrue($whiteList->isValid(3)); - $this->assertTrue($whiteList->isValid(4)); - $this->assertTrue($whiteList->isValid('string1')); - $this->assertTrue($whiteList->isValid('string2')); - $this->assertTrue($whiteList->isValid('STRING1')); - $this->assertTrue($whiteList->isValid('strIng1')); - $this->assertTrue($whiteList->isValid('3')); - $this->assertTrue($whiteList->isValid('4')); - $this->assertFalse($whiteList->isValid('string3')); - $this->assertFalse($whiteList->isValid(5)); - $this->assertEquals($whiteList->getList(), ['string1', 'string2', 3, 4]); - } -} diff --git a/tests/Validator/WildcardTest.php b/tests/Validator/WildcardTest.php deleted file mode 100644 index 4de71475..00000000 --- a/tests/Validator/WildcardTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertTrue($validator->isValid([0 => 'string', 1 => 'string'])); - $this->assertTrue($validator->isValid('')); - $this->assertTrue($validator->isValid([])); - $this->assertTrue($validator->isValid(1)); - $this->assertTrue($validator->isValid(true)); - $this->assertTrue($validator->isValid(false)); - $this->assertFalse($validator->isArray()); - $this->assertEquals(\Utopia\Http\Validator::TYPE_STRING, $validator->getType()); - } -} diff --git a/tests/e2e/init.php b/tests/e2e/init.php index 54638fd9..fb0d9d59 100644 --- a/tests/e2e/init.php +++ b/tests/e2e/init.php @@ -5,7 +5,7 @@ use Utopia\Http\Http; use Utopia\Http\Request; use Utopia\Http\Response; -use Utopia\Http\Validator\Text; +use Utopia\Validator\Text; ini_set('memory_limit', '1024M'); ini_set('display_errors', '1'); From 396a8e3872a42f67c645994e512f132419a8ca8e Mon Sep 17 00:00:00 2001 From: Hemachandar <132386067+hmacr@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:30:14 +0530 Subject: [PATCH 03/29] Convert hostname to lowercase (#214) --- src/Http/Adapter/FPM/Request.php | 3 ++- src/Http/Adapter/Swoole/Request.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Http/Adapter/FPM/Request.php b/src/Http/Adapter/FPM/Request.php index 41452440..d095445f 100644 --- a/src/Http/Adapter/FPM/Request.php +++ b/src/Http/Adapter/FPM/Request.php @@ -108,7 +108,8 @@ public function getPort(): string */ public function getHostname(): string { - return (string) \parse_url($this->getProtocol().'://'.$this->getServer('HTTP_HOST', ''), PHP_URL_HOST); + $hostname = \parse_url($this->getProtocol().'://'.$this->getServer('HTTP_HOST', ''), PHP_URL_HOST); + return strtolower((string) ($hostname)); } /** diff --git a/src/Http/Adapter/Swoole/Request.php b/src/Http/Adapter/Swoole/Request.php index b07a5fdb..d5bd799c 100644 --- a/src/Http/Adapter/Swoole/Request.php +++ b/src/Http/Adapter/Swoole/Request.php @@ -122,7 +122,8 @@ public function getPort(): string */ public function getHostname(): string { - return strval(\parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_HOST)); + $hostname = \parse_url($this->getProtocol().'://'.$this->getHeader('x-forwarded-host', $this->getHeader('host')), PHP_URL_HOST); + return strtolower(strval($hostname)); } /** From d60c9ce7eea3e4f89599d995e0058e3729a1ef7c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 25 Feb 2026 23:01:12 +0530 Subject: [PATCH 04/29] chore: bump validators lib (#218) --- composer.json | 2 +- composer.lock | 133 +++++++++++++++++++++++++------------------------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/composer.json b/composer.json index e5953bc4..2c5bf2c6 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "require": { "php": ">=8.0", "ext-swoole": "*", - "utopia-php/validators": "0.1.*" + "utopia-php/validators": "0.2.*" }, "require-dev": { "phpunit/phpunit": "^9.5.25", diff --git a/composer.lock b/composer.lock index b68b4ff5..3d5b401a 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": "3851bd0696173988209c33d95ad3fd35", + "content-hash": "e7104035855b324e167057fd44e2a875", "packages": [ { "name": "utopia-php/validators", - "version": "0.1.0", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080" + "reference": "30b6030a5b100fc1dff34506e5053759594b2a20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080", - "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/30b6030a5b100fc1dff34506e5053759594b2a20", + "reference": "30b6030a5b100fc1dff34506e5053759594b2a20", "shasum": "" }, "require": { @@ -25,7 +25,7 @@ }, "require-dev": { "laravel/pint": "1.*", - "phpstan/phpstan": "1.*", + "phpstan/phpstan": "2.*", "phpunit/phpunit": "11.*" }, "type": "library", @@ -47,9 +47,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.1.0" + "source": "https://github.com/utopia-php/validators/tree/0.2.0" }, - "time": "2025-11-18T11:05:46+00:00" + "time": "2026-01-13T09:16:51+00:00" } ], "packages-dev": [ @@ -132,30 +132,29 @@ }, { "name": "doctrine/instantiator", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", "shasum": "" }, "require": { - "php": "^8.1" + "php": "^8.4" }, "require-dev": { - "doctrine/coding-standard": "^11", + "doctrine/coding-standard": "^14", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, "type": "library", "autoload": { @@ -182,7 +181,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" }, "funding": [ { @@ -198,7 +197,7 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2026-01-05T06:47:08+00:00" }, { "name": "doctrine/lexer", @@ -279,16 +278,16 @@ }, { "name": "laravel/pint", - "version": "v1.26.0", + "version": "v1.27.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", - "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", + "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", "shasum": "" }, "require": { @@ -299,13 +298,13 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.90.0", - "illuminate/view": "^12.40.1", - "larastan/larastan": "^3.8.0", - "laravel-zero/framework": "^12.0.4", + "friendsofphp/php-cs-fixer": "^3.93.1", + "illuminate/view": "^12.51.0", + "larastan/larastan": "^3.9.2", + "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.4" + "pestphp/pest": "^3.8.5" }, "bin": [ "builds/pint" @@ -342,7 +341,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-11-25T21:15:52+00:00" + "time": "2026-02-10T20:00:20+00:00" }, { "name": "myclabs/deep-copy", @@ -1103,16 +1102,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.31", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", - "reference": "945d0b7f346a084ce5549e95289962972c4272e5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -1134,7 +1133,7 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", "sebastian/exporter": "^4.0.8", @@ -1186,7 +1185,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -1210,7 +1209,7 @@ "type": "tidelift" } ], - "time": "2025-12-06T07:45:52+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/cache", @@ -1533,16 +1532,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -1595,7 +1594,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { @@ -1615,7 +1614,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -2483,16 +2482,16 @@ }, { "name": "symfony/console", - "version": "v8.0.1", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fcb73f69d655b48fcb894a262f074218df08bd58" + "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fcb73f69d655b48fcb894a262f074218df08bd58", - "reference": "fcb73f69d655b48fcb894a262f074218df08bd58", + "url": "https://api.github.com/repos/symfony/console/zipball/ace03c4cf9805080ff40cbeec69fca180c339a3b", + "reference": "ace03c4cf9805080ff40cbeec69fca180c339a3b", "shasum": "" }, "require": { @@ -2549,7 +2548,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.1" + "source": "https://github.com/symfony/console/tree/v8.0.4" }, "funding": [ { @@ -2569,7 +2568,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:25:33+00:00" + "time": "2026-01-13T13:06:50+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2710,16 +2709,16 @@ }, { "name": "symfony/finder", - "version": "v8.0.0", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291" + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/7598dd5770580fa3517ec83e8da0c9b9e01f4291", - "reference": "7598dd5770580fa3517ec83e8da0c9b9e01f4291", + "url": "https://api.github.com/repos/symfony/finder/zipball/8bd576e97c67d45941365bf824e18dc8538e6eb0", + "reference": "8bd576e97c67d45941365bf824e18dc8538e6eb0", "shasum": "" }, "require": { @@ -2754,7 +2753,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.0" + "source": "https://github.com/symfony/finder/tree/v8.0.5" }, "funding": [ { @@ -2774,7 +2773,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T14:36:47+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/options-resolver", @@ -3184,16 +3183,16 @@ }, { "name": "symfony/process", - "version": "v8.0.0", + "version": "v8.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149" + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/a0a750500c4ce900d69ba4e9faf16f82c10ee149", - "reference": "a0a750500c4ce900d69ba4e9faf16f82c10ee149", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", "shasum": "" }, "require": { @@ -3225,7 +3224,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.0" + "source": "https://github.com/symfony/process/tree/v8.0.5" }, "funding": [ { @@ -3245,7 +3244,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T16:25:44+00:00" + "time": "2026-01-26T15:08:38+00:00" }, { "name": "symfony/service-contracts", @@ -3336,16 +3335,16 @@ }, { "name": "symfony/string", - "version": "v8.0.1", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" + "reference": "758b372d6882506821ed666032e43020c4f57194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", "shasum": "" }, "require": { @@ -3402,7 +3401,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.1" + "source": "https://github.com/symfony/string/tree/v8.0.4" }, "funding": [ { @@ -3422,7 +3421,7 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-01-12T12:37:40+00:00" }, { "name": "theseer/tokenizer", From d2db9f3dca7194ce41590533d171d302561e5338 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:16:12 +0530 Subject: [PATCH 05/29] Use utopia-php/di for resource injection --- composer.json | 9 ++++- composer.lock | 69 ++++++++++++++++++++++++++++++++++++-- src/Http/Http.php | 84 ++++++++++++++++++++++++++--------------------- 3 files changed, 120 insertions(+), 42 deletions(-) diff --git a/composer.json b/composer.json index 2c5bf2c6..63fadaa6 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,12 @@ ], "license": "MIT", "minimum-stability": "stable", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/di" + } + ], "autoload": { "psr-4": { "Utopia\\": "src/", @@ -29,8 +35,9 @@ "bench": "vendor/bin/phpbench run --report=benchmark" }, "require": { - "php": ">=8.0", + "php": ">=8.2", "ext-swoole": "*", + "utopia-php/di": "dev-chore/extract-http-di-resources", "utopia-php/validators": "0.2.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 3d5b401a..e4d98970 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,69 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e7104035855b324e167057fd44e2a875", + "content-hash": "0b2a167dd76d9f5860a3028ac4ce13db", "packages": [ + { + "name": "utopia-php/di", + "version": "dev-chore/extract-http-di-resources", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/di.git", + "reference": "3310616570444198cbc2a2a9567cb68e64510e38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/di/zipball/3310616570444198cbc2a2a9567cb68e64510e38", + "reference": "3310616570444198cbc2a2a9567cb68e64510e38", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "laravel/pint": "^1.2", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/", + "Tests\\E2E\\": "tests/e2e" + } + }, + "scripts": { + "lint": [ + "vendor/bin/pint --test" + ], + "format": [ + "vendor/bin/pint" + ], + "check": [ + "vendor/bin/phpstan analyse -c phpstan.neon" + ], + "test": [ + "vendor/bin/phpunit --configuration phpunit.xml" + ] + }, + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", + "issues": "https://github.com/utopia-php/di/issues" + }, + "time": "2026-03-10T05:33:55+00:00" + }, { "name": "utopia-php/validators", "version": "0.2.0", @@ -3525,11 +3586,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": { + "utopia-php/di": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.0", + "php": ">=8.2", "ext-swoole": "*" }, "platform-dev": {}, diff --git a/src/Http/Http.php b/src/Http/Http.php index 61434842..c71bad81 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,6 +2,7 @@ namespace Utopia\Http; +use Utopia\DI\Container as DIContainer; use Utopia\Validator; class Http @@ -33,21 +34,19 @@ class Http public const MODE_TYPE_PRODUCTION = 'production'; /** - * @var array + * @var Files */ - protected array $resources = [ - 'error' => null, - ]; + protected Files $files; /** - * @var Files + * @var DIContainer|null */ - protected Files $files; + protected static ?DIContainer $resourceContainer = null; /** - * @var array + * @var array> */ - protected static array $resourcesCallbacks = []; + protected static array $resourceNames = []; /** * Current running mode @@ -139,6 +138,7 @@ public function __construct(Adapter $server, string $timezone) \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; + self::$resourceContainer ??= new DIContainer(); } /** @@ -365,26 +365,17 @@ public function getResource(string $name, string $context = 'utopia', bool $fres return $this; } - $this->resources[$context] ??= []; - - $resourcesCallback = &self::$resourcesCallbacks[$context] ?? []; - if (empty($resourcesCallback) || !\array_key_exists($name, $resourcesCallback)) { - $resourcesCallback = &self::$resourcesCallbacks['utopia']; - } + try { + return self::getResourceContainer()->getResource($name, $context, $fresh); + } catch (\Throwable $e) { + $message = \str_replace('dependency', 'resource', $e->getMessage()); - if (!\array_key_exists($name, $this->resources[$context]) || $fresh || ($resourcesCallback[$name]['reset'][$context] ?? true)) { - if (!\array_key_exists($name, $resourcesCallback)) { - throw new Exception('Failed to find resource: "' . $name . '"'); + if ($message === $e->getMessage() && !\str_contains($message, 'resource')) { + $message = 'Failed to find resource: "' . $name . '"'; } - $this->resources[$context][$name] = \call_user_func_array( - $resourcesCallback[$name]['callback'], - $this->getResources($resourcesCallback[$name]['injections'], $context) - ); + throw new Exception($message, 500, $e); } - - $resourcesCallback[$name]['reset'][$context] = false; - return $this->resources[$context][$name]; } /** @@ -420,9 +411,8 @@ public static function setResource(string $name, callable $callback, array $inje throw new Exception("'utopia' is a reserved keyword.", 500); } - self::$resourcesCallbacks[$context] ??= []; - - self::$resourcesCallbacks[$context][$name] = ['callback' => $callback, 'injections' => $injections, 'resets' => []]; + self::getResourceContainer()->setResource($name, $callback, $injections, $context); + self::$resourceNames[$context][$name] = true; } /** @@ -578,14 +568,10 @@ public function start() try { $this->run($request, $response, $context); } finally { - if (isset(self::$resourcesCallbacks[$context])) { - unset(self::$resourcesCallbacks[$context]); - } + self::purgeResources($context); } }); $this->server->onStart(function ($server) { - $this->resources['utopia'] ??= []; - $this->resources['utopia']['server'] = $server; self::setResource('server', function () use ($server) { return $server; }); @@ -718,7 +704,7 @@ public function execute(Route $route, Request $request, string $context): static } // Reset resources for the context - $this->resources[$context] = []; + self::refreshResources($context); return $this; } @@ -779,10 +765,6 @@ protected function getArguments(Hook $hook, string $context, array $values, arra */ public function run(Request $request, Response $response, string $context): static { - $this->resources[$context] = []; - $this->resources[$context]['request'] = $request; - $this->resources[$context]['response'] = $response; - self::setResource('context', fn () => $context, [], $context); self::setResource('request', fn () => $request, [], $context); @@ -955,7 +937,8 @@ protected function validate(string $key, array $param, mixed $value, $context): public static function reset(): void { Router::reset(); - self::$resourcesCallbacks = []; + self::$resourceContainer = new DIContainer(); + self::$resourceNames = []; self::$mode = ''; self::$errors = []; self::$init = []; @@ -963,4 +946,29 @@ public static function reset(): void self::$options = []; self::$startHooks = []; } + + protected static function getResourceContainer(): DIContainer + { + self::$resourceContainer ??= new DIContainer(); + + return self::$resourceContainer; + } + + protected static function refreshResources(string $context): void + { + $resources = \array_unique(\array_merge( + \array_keys(self::$resourceNames['utopia'] ?? []), + \array_keys(self::$resourceNames[$context] ?? []) + )); + + foreach ($resources as $resource) { + self::getResourceContainer()->refresh($resource, $context); + } + } + + protected static function purgeResources(string $context): void + { + self::getResourceContainer()->purge($context); + unset(self::$resourceNames[$context]); + } } From a5868633c0698a2606dc7279ca8270ecbe53e39c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:26:08 +0530 Subject: [PATCH 06/29] Move resource ownership into utopia-php/di --- README.md | 25 ++-- docs/Getting-Starting-Guide.md | 5 +- src/Http/Adapter/FPM/Server.php | 9 +- src/Http/Adapter/Swoole/Server.php | 9 +- src/Http/Http.php | 185 +++++++++++++---------------- tests/HttpTest.php | 47 ++++---- 6 files changed, 135 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 137e384c..92a58ac0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Utopia HTTP is a PHP MVC based framework with minimal must-have features for professional, simple, advanced and secure web development. This library is maintained by the [Appwrite team](https://appwrite.io). -Utopia HTTP is dependency-free. Any extra features, such as authentication or caching are available as standalone models in order to keep the framework core clean, light, and easy to learn. +Utopia HTTP keeps routing and request lifecycle concerns separate from resource wiring by relying on the standalone Utopia DI package for dependency injection. ## Getting Started @@ -23,11 +23,14 @@ Init your first application in `src/server.php`: ```php require_once __DIR__.'/../vendor/autoload.php'; +use Utopia\DI\Container; use Utopia\Http\Http; use Utopia\Http\Request; use Utopia\Http\Response; use Utopia\Http\Adapter\FPM\Server; +$container = new Container(); + Http::get('/hello-world') // Define Route ->inject('request') ->inject('response') @@ -43,7 +46,7 @@ Http::get('/hello-world') // Define Route Http::setMode(Http::MODE_TYPE_PRODUCTION); -$http = new Http(new Server(), 'America/New_York'); +$http = new Http(new Server(), 'America/New_York', $container); $http->start(); ``` @@ -66,10 +69,13 @@ The library supports server adapters to be able to run on any PHP setup. You cou #### Use PHP FPM server ```php +use Utopia\DI\Container; use Utopia\Http\Http; use Utopia\Http\Response; use Utopia\Http\Adapter\FPM\Server; +$container = new Container(); + Http::get('/') ->inject('response') ->action( @@ -78,7 +84,7 @@ Http::get('/') } ); -$http = new Http(new Server(), 'America/New_York'); +$http = new Http(new Server(), 'America/New_York', $container); $http->start(); ``` @@ -87,11 +93,14 @@ $http->start(); #### Using Swoole server ```php +use Utopia\DI\Container; use Utopia\Http\Http; use Utopia\Http\Request; use Utopia\Http\Response; use Utopia\Http\Adapter\Swoole\Server; +$container = new Container(); + Http::get('/') ->inject('request') ->inject('response') @@ -101,7 +110,7 @@ Http::get('/') } ); -$http = new Http(new Server('0.0.0.0', '80'), 'America/New_York'); +$http = new Http(new Server('0.0.0.0', '80'), 'America/New_York', $container); $http->start(); ``` @@ -210,10 +219,10 @@ Groups are designed to be actions that run during the lifecycle of requests to e Resources allow you to prepare dependencies for requests such as database connection or the user who sent the request. A new instance of a resource is created for every request. -Define a resource: +Define a resource on the DI container: ```php -Http::setResource('timing', function() { +$container->setResource('timing', function() { return \microtime(true); }); ``` @@ -221,6 +230,8 @@ Http::setResource('timing', function() { Inject resource into endpoint action: ```php +$http = new Http(new Server(), 'America/New_York', $container); + Http::get('/') ->inject('timing') ->inject('response') @@ -248,7 +259,7 @@ To learn more about architecture and features for this library, check out more i ## System Requirements -Utopia HTTP requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible. +Utopia HTTP requires PHP 8.2 or later. We recommend using the latest PHP version whenever possible. ## More from Utopia diff --git a/docs/Getting-Starting-Guide.md b/docs/Getting-Starting-Guide.md index 831d0687..a81d20b9 100644 --- a/docs/Getting-Starting-Guide.md +++ b/docs/Getting-Starting-Guide.md @@ -33,7 +33,7 @@ Http::get('/') $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { $request = new Request($swooleRequest); $response = new Response($swooleResponse); - $http = new Http('America/Toronto'); + $http = new Http($server, 'America/Toronto'); $http->run($request, $response); }); @@ -281,7 +281,6 @@ Http::shutdown(function($request) { # Running Locally If you have PHP and Composer installed on your device, you can run Utopia apps locally by downloading the `utopia-php/http` dependency using `composer require utopia-php/http` command. -> Utopia HTTP requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible. +> Utopia HTTP requires PHP 8.2 or later. We recommend using the latest PHP version whenever possible. Wonderful! 😄 You’re all set to create a basic demo app using the Utopia HTTP. If you have any issues or questions feel free to reach out to us on our [Discord Server](https://appwrite.io/discord). - diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index e90f0cd7..4db7c3bc 100755 --- 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; -use Utopia\Http\Http; class Server extends Adapter { @@ -16,10 +15,10 @@ 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, 'fpm'); + call_user_func($callback, $request, $response, 'fpm', [ + 'fpmRequest' => $request, + 'fpmResponse' => $response, + ]); } public function onStart(callable $callback) diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index bb3b87d8..fa475dd6 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -7,7 +7,6 @@ use Swoole\Coroutine\Http\Server as SwooleServer; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use Utopia\Http\Http; use function Swoole\Coroutine\run; @@ -29,10 +28,10 @@ public function onRequest(callable $callback) $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { $context = \strval(Coroutine::getCid()); - Http::setResource('swooleRequest', fn () => $request, [], $context); - Http::setResource('swooleResponse', fn () => $response, [], $context); - - call_user_func($callback, new Request($request), new Response($response), $context); + call_user_func($callback, new Request($request), new Response($response), $context, [ + 'swooleRequest' => $request, + 'swooleResponse' => $response, + ]); }); } diff --git a/src/Http/Http.php b/src/Http/Http.php index c71bad81..4eeeddd3 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -38,15 +38,12 @@ class Http */ protected Files $files; - /** - * @var DIContainer|null - */ - protected static ?DIContainer $resourceContainer = null; + protected DIContainer $resourceContainer; /** * @var array> */ - protected static array $resourceNames = []; + protected array $resourceNames = []; /** * Current running mode @@ -132,13 +129,14 @@ class Http * * @param Adapter $server * @param string $timezone + * @param DIContainer|null $resourceContainer */ - public function __construct(Adapter $server, string $timezone) + public function __construct(Adapter $server, string $timezone, ?DIContainer $resourceContainer = null) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - self::$resourceContainer ??= new DIContainer(); + $this->resourceContainer = $resourceContainer ?? new DIContainer(); } /** @@ -350,71 +348,6 @@ public static function setAllowOverride(bool $value): void Router::setAllowOverride($value); } - /** - * If a resource has been created return it, otherwise create it and then return it - * - * @param string $name - * @param bool $fresh - * @return mixed - * - * @throws Exception - */ - public function getResource(string $name, string $context = 'utopia', bool $fresh = false): mixed - { - if ($name === 'utopia') { - return $this; - } - - try { - return self::getResourceContainer()->getResource($name, $context, $fresh); - } catch (\Throwable $e) { - $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 Resources By List - * - * @param array $list - * @return array - */ - public function getResources(array $list, string $context = 'utopia'): array - { - $resources = []; - - foreach ($list as $name) { - $resources[$name] = $this->getResource($name, $context); - } - - return $resources; - } - - /** - * Set a new resource callback - * - * @param string $name - * @param callable $callback - * @param array $injections - * @return void - * - * @throws Exception - */ - public static function setResource(string $name, callable $callback, array $injections = [], string $context = 'utopia'): void - { - if ($name === 'utopia') { - throw new Exception("'utopia' is a reserved keyword.", 500); - } - - self::getResourceContainer()->setResource($name, $callback, $injections, $context); - self::$resourceNames[$context][$name] = true; - } - /** * Is http in production mode? * @@ -564,15 +497,16 @@ public static function onRequest(): Hook public function start() { - $this->server->onRequest(function ($request, $response, $context) { + $this->server->onRequest(function ($request, $response, $context, array $resources = []) { try { + $this->registerRequestResources($request, $response, $context, $resources); $this->run($request, $response, $context); } finally { - self::purgeResources($context); + $this->purgeResources($context); } }); $this->server->onStart(function ($server) { - self::setResource('server', function () use ($server) { + $this->registerResource('server', function () use ($server) { return $server; }); try { @@ -582,7 +516,7 @@ public function start() \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - self::setResource('error', fn () => $e); + $this->registerResource('error', fn () => $e); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { @@ -676,7 +610,7 @@ public function execute(Route $route, Request $request, string $context): static } } } catch (\Throwable $e) { - self::setResource('error', fn () => $e, [], $context); + $this->registerResource('error', fn () => $e, [], $context); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks @@ -704,7 +638,7 @@ public function execute(Route $route, Request $request, string $context): static } // Reset resources for the context - self::refreshResources($context); + $this->refreshResources($context); return $this; } @@ -729,7 +663,7 @@ protected function getArguments(Hook $hook, string $context, array $values, arra $arg = $existsInRequest ? $requestParams[$key] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, $this->getResources($param['injections'])); + $arg = \call_user_func_array($arg, $this->resolveResources($param['injections'], $context)); } $value = $existsInValues ? $values[$key] : $arg; @@ -748,7 +682,7 @@ protected function getArguments(Hook $hook, string $context, array $values, arra } foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->getResource($injection['name'], $context); + $arguments[$injection['order']] = $this->resolveResource($injection['name'], $context); } return $arguments; @@ -765,11 +699,9 @@ protected function getArguments(Hook $hook, string $context, array $values, arra */ public function run(Request $request, Response $response, string $context): static { - self::setResource('context', fn () => $context, [], $context); - - self::setResource('request', fn () => $request, [], $context); - - self::setResource('response', fn () => $response, [], $context); + $this->registerResource('context', fn () => $context, [], $context); + $this->registerResource('request', fn () => $request, [], $context); + $this->registerResource('response', fn () => $response, [], $context); try { @@ -778,7 +710,7 @@ public function run(Request $request, Response $response, string $context): stat \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - self::setResource('error', fn () => $e, [], $context); + $this->registerResource('error', fn () => $e, [], $context); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { @@ -807,7 +739,7 @@ public function run(Request $request, Response $response, string $context): stat $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - self::setResource('route', fn () => $route, [], $context); + $this->registerResource('route', fn () => $route, [], $context); if (self::REQUEST_METHOD_HEAD == $method) { $method = self::REQUEST_METHOD_GET; @@ -835,7 +767,7 @@ public function run(Request $request, Response $response, string $context): stat foreach (self::$errors as $error) { // Global error hooks /** @var Hook $error */ if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { + $this->registerResource('error', function () use ($e) { return $e; }, [], $context); \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); @@ -852,7 +784,7 @@ public function run(Request $request, Response $response, string $context): stat $path = \parse_url($request->getURI(), PHP_URL_PATH); $route->path($path); - self::setResource('route', fn () => $route, [], $context); + $this->registerResource('route', fn () => $route, [], $context); } if (null !== $route) { @@ -875,7 +807,7 @@ public function run(Request $request, Response $response, string $context): stat } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { + $this->registerResource('error', function () use ($e) { return $e; }, [], $context); \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); @@ -885,7 +817,7 @@ public function run(Request $request, Response $response, string $context): stat } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () { + $this->registerResource('error', function () { return new Exception('Not Found', 404); }, [], $context); \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); @@ -917,7 +849,7 @@ protected function validate(string $key, array $param, mixed $value, $context): $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'], $context)); + $validator = \call_user_func_array($validator, $this->resolveResources($param['injections'], $context)); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class @@ -937,38 +869,83 @@ protected function validate(string $key, array $param, mixed $value, $context): public static function reset(): void { Router::reset(); - self::$resourceContainer = new DIContainer(); - self::$resourceNames = []; self::$mode = ''; self::$errors = []; self::$init = []; self::$shutdown = []; self::$options = []; self::$startHooks = []; + self::$requestHooks = []; + self::$wildcardRoute = null; + } + + public function getResourceContainer(): DIContainer + { + return $this->resourceContainer; + } + + protected function resolveResource(string $name, string $context = 'utopia', bool $fresh = false): mixed + { + if ($name === 'utopia') { + return $this; + } + + try { + return $this->resourceContainer->getResource($name, $context, $fresh); + } catch (\Throwable $e) { + $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); + } + } + + protected function resolveResources(array $list, string $context = 'utopia'): array + { + $resources = []; + + foreach ($list as $name) { + $resources[$name] = $this->resolveResource($name, $context); + } + + return $resources; } - protected static function getResourceContainer(): DIContainer + protected function registerResource(string $name, callable $callback, array $injections = [], string $context = 'utopia'): void { - self::$resourceContainer ??= new DIContainer(); + if ($name === 'utopia') { + throw new Exception("'utopia' is a reserved keyword.", 500); + } + + $this->resourceContainer->setResource($name, $callback, $injections, $context); + $this->resourceNames[$context][$name] = true; + } - return self::$resourceContainer; + protected function registerRequestResources(Request $request, Response $response, string $context, array $resources = []): void + { + foreach ($resources as $name => $resource) { + $this->registerResource($name, fn () => $resource, [], $context); + } } - protected static function refreshResources(string $context): void + protected function refreshResources(string $context): void { $resources = \array_unique(\array_merge( - \array_keys(self::$resourceNames['utopia'] ?? []), - \array_keys(self::$resourceNames[$context] ?? []) + \array_keys($this->resourceNames['utopia'] ?? []), + \array_keys($this->resourceNames[$context] ?? []) )); foreach ($resources as $resource) { - self::getResourceContainer()->refresh($resource, $context); + $this->resourceContainer->refresh($resource, $context); } } - protected static function purgeResources(string $context): void + protected function purgeResources(string $context): void { - self::getResourceContainer()->purge($context); - unset(self::$resourceNames[$context]); + $this->resourceContainer->purge($context); + unset($this->resourceNames[$context]); } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index a735a41f..b1c63157 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\Tests\UtopiaFPMRequestTest; use Utopia\Validator\Text; use Utopia\Http\Adapter\FPM\Request; @@ -13,6 +14,8 @@ class HttpTest extends TestCase { protected ?Http $http; + protected ?Container $container; + protected ?string $method; protected ?string $uri; @@ -20,13 +23,15 @@ class HttpTest extends TestCase public function setUp(): void { Http::reset(); - $this->http = new Http(new Server(), 'Asia/Tel_Aviv'); + $this->container = new Container(); + $this->http = new Http(new Server(), 'Asia/Tel_Aviv', $this->container); $this->saveRequest(); } public function tearDown(): void { $this->http = 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->setResource('rand', fn () => rand()); + $this->container->setResource('first', fn ($second) => "first-{$second}", ['second']); + $this->container->setResource('second', fn () => 'second'); - $second = $this->http->getResource('second', '1'); - $first = $this->http->getResource('first', '1'); + $second = $this->container->getResource('second', '1'); + $first = $this->container->getResource('first', '1'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); - $resource = $this->http->getResource('rand', '1'); + $resource = $this->container->getResource('rand', '1'); $this->assertNotEmpty($resource); - $this->assertEquals($resource, $this->http->getResource('rand', '1')); - $this->assertEquals($resource, $this->http->getResource('rand', '1')); - $this->assertEquals($resource, $this->http->getResource('rand', '1')); + $this->assertEquals($resource, $this->container->getResource('rand', '1')); + $this->assertEquals($resource, $this->container->getResource('rand', '1')); + $this->assertEquals($resource, $this->container->getResource('rand', '1')); // 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->setResource('first', fn ($second) => "first-{$second}", ['second']); + $this->container->setResource('second', fn () => 'second'); - $second = $this->http->getResource('second'); - $first = $this->http->getResource('first'); + $second = $this->container->getResource('second'); + $first = $this->container->getResource('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); @@ -148,8 +153,8 @@ public function testCanGetDefaultValueWithFunction(): void public function testCanExecuteRoute(): void { - Http::setResource('rand', fn () => rand()); - $resource = $this->http->getResource('rand', '1'); + $this->container->setResource('rand', fn () => rand()); + $resource = $this->container->getResource('rand', '1'); $this->http ->error() @@ -174,7 +179,7 @@ public function testCanExecuteRoute(): void \ob_end_clean(); // With Params - $resource = $this->http->getResource('rand', '1'); + $resource = $this->container->getResource('rand', '1'); $route = new Route('GET', '/path'); $route @@ -200,7 +205,7 @@ public function testCanExecuteRoute(): void $this->assertEquals($resource . '-param-x-param-y', $result); // With Error - $resource = $this->http->getResource('rand', '1'); + $resource = $this->container->getResource('rand', '1'); $route = new Route('GET', '/path'); $route @@ -220,7 +225,7 @@ public function testCanExecuteRoute(): void $this->assertEquals('error: Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); // With Hooks - $resource = $this->http->getResource('rand', '1'); + $resource = $this->container->getResource('rand', '1'); $this->http ->init() ->inject('rand') @@ -291,7 +296,7 @@ public function testCanExecuteRoute(): void $this->assertEquals('init-' . $resource . '-(init-api)-param-x-param-y-(shutdown-api)-shutdown', $result); - $resource = $this->http->getResource('rand', '1'); + $resource = $this->container->getResource('rand', '1'); \ob_start(); $request = new UtopiaFPMRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); @@ -582,7 +587,7 @@ public function testWildcardRoute(): void Http::init() ->action(function () { $route = $this->http->getRoute(); - Http::setResource('myRoute', fn () => $route); + $this->container->setResource('myRoute', fn () => $route); }); From 0991a260a3dce874a64367186c9391951b7c1a0f Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:48:22 +0530 Subject: [PATCH 07/29] Update DI branch dependency --- composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index e4d98970..4313c68a 100644 --- a/composer.lock +++ b/composer.lock @@ -12,21 +12,21 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "3310616570444198cbc2a2a9567cb68e64510e38" + "reference": "a477152b0c428f55c9dc4b07a8e4478f756432b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/3310616570444198cbc2a2a9567cb68e64510e38", - "reference": "3310616570444198cbc2a2a9567cb68e64510e38", + "url": "https://api.github.com/repos/utopia-php/di/zipball/a477152b0c428f55c9dc4b07a8e4478f756432b2", + "reference": "a477152b0c428f55c9dc4b07a8e4478f756432b2", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "laravel/pint": "^1.2", + "laravel/pint": "^1.27", "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5.25", "swoole/ide-helper": "4.8.3" }, @@ -38,13 +38,13 @@ } }, "scripts": { - "lint": [ - "vendor/bin/pint --test" - ], "format": [ "vendor/bin/pint" ], - "check": [ + "format:check": [ + "vendor/bin/pint --test" + ], + "analyze": [ "vendor/bin/phpstan analyse -c phpstan.neon" ], "test": [ @@ -59,13 +59,13 @@ "framework", "http", "php", - "upf" + "utopia" ], "support": { "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-10T05:33:55+00:00" + "time": "2026-03-10T06:15:58+00:00" }, { "name": "utopia-php/validators", From bf94cd26e7cd76ad0b5b6ef83efbc8ca41795164 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 10 Mar 2026 11:50:15 +0530 Subject: [PATCH 08/29] update getting started --- docs/Getting-Starting-Guide.md | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/Getting-Starting-Guide.md b/docs/Getting-Starting-Guide.md index a81d20b9..86455959 100644 --- a/docs/Getting-Starting-Guide.md +++ b/docs/Getting-Starting-Guide.md @@ -10,33 +10,21 @@ If you’re new to Utopia, let’s get started by looking at an example of a bas ```php use Utopia\Http\Http; -use Utopia\Http\Swoole\Request; -use Utopia\Http\Swoole\Response; -use Swoole\Http\Server; -use Swoole\Http\Request as SwooleRequest; -use Swoole\Http\Response as SwooleResponse; - -$http = new Server("0.0.0.0", 8080); +use Utopia\Http\Request; +use Utopia\Http\Response; +use Utopia\Http\Adapter\Swoole\Server; Http::get('/') ->inject('request') ->inject('response') ->action( - function($request, $response) { + function(Request $request, Response $response) { // Return raw HTML $response->send("
Hello World!
"); } -/* - Configure your HTTP server to respond with the Utopia http. -*/ - -$http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { - $request = new Request($swooleRequest); - $response = new Response($swooleResponse); - $http = new Http($server, 'America/Toronto'); - $http->run($request, $response); -}); + ); +$http = new Http(new Server("0.0.0.0", "8080"), 'America/Toronto'); $http->start(); ``` From 82d74810b34f3b6faa82071c06068edd4e70b4e9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 11 Mar 2026 15:54:11 +0530 Subject: [PATCH 09/29] update --- composer.lock | 102 +++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/composer.lock b/composer.lock index 4313c68a..b536f14e 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "a477152b0c428f55c9dc4b07a8e4478f756432b2" + "reference": "3d8a454963a1db81cd63bc4f0f74c1488716e6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/a477152b0c428f55c9dc4b07a8e4478f756432b2", - "reference": "a477152b0c428f55c9dc4b07a8e4478f756432b2", + "url": "https://api.github.com/repos/utopia-php/di/zipball/3d8a454963a1db81cd63bc4f0f74c1488716e6b8", + "reference": "3d8a454963a1db81cd63bc4f0f74c1488716e6b8", "shasum": "" }, "require": { @@ -65,7 +65,7 @@ "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-10T06:15:58+00:00" + "time": "2026-03-11T10:23:32+00:00" }, { "name": "utopia-php/validators", @@ -339,16 +339,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", "shasum": "" }, "require": { @@ -359,13 +359,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", + "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.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.5", + "shipfastlabs/agent-detector": "^1.0.2" }, "bin": [ "builds/pint" @@ -402,7 +403,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-03-10T20:37:18+00:00" }, { "name": "myclabs/deep-copy", @@ -693,16 +694,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": { @@ -713,7 +714,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", @@ -733,8 +734,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" }, @@ -779,7 +781,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": [ { @@ -787,15 +789,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": { @@ -840,7 +842,7 @@ "type": "github" } ], - "time": "2025-09-30T10:16:31+00:00" + "time": "2026-02-28T20:30:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2543,16 +2545,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": { @@ -2609,7 +2611,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.4" + "source": "https://github.com/symfony/console/tree/v8.0.7" }, "funding": [ { @@ -2629,7 +2631,7 @@ "type": "tidelift" } ], - "time": "2026-01-13T13:06:50+00:00" + "time": "2026-03-06T14:06:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2700,16 +2702,16 @@ }, { "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": { @@ -2746,7 +2748,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": [ { @@ -2766,20 +2768,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": { @@ -2814,7 +2816,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": [ { @@ -2834,7 +2836,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:08:38+00:00" + "time": "2026-01-29T09:41:02+00:00" }, { "name": "symfony/options-resolver", @@ -3396,16 +3398,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": { @@ -3462,7 +3464,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.4" + "source": "https://github.com/symfony/string/tree/v8.0.6" }, "funding": [ { @@ -3482,7 +3484,7 @@ "type": "tidelift" } ], - "time": "2026-01-12T12:37:40+00:00" + "time": "2026-02-09T10:14:57+00:00" }, { "name": "theseer/tokenizer", From 7996ee121e0d3a90400ecdc78389ebcd9240a639 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 10:37:02 +0530 Subject: [PATCH 10/29] update --- README.md | 24 ++++---- composer.lock | 136 +++++++++++++++++++++++++-------------------- src/Http/Http.php | 53 +++++++++++------- tests/HttpTest.php | 41 +++++++------- 4 files changed, 142 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index 92a58ac0..d930037e 100644 --- a/README.md +++ b/README.md @@ -217,14 +217,16 @@ 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 connection or the user who sent the request. A new instance of a resource is created for every request. +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. -Define a resource on the DI container: +Define a dependency on the DI container: ```php -$container->setResource('timing', function() { +use Utopia\DI\Dependency; + +$container->set('bootTime', new Dependency([], function () { return \microtime(true); -}); +})); ``` Inject resource into endpoint action: @@ -233,10 +235,10 @@ Inject resource into endpoint action: $http = new Http(new Server(), 'America/New_York', $container); Http::get('/') - ->inject('timing') + ->inject('bootTime') ->inject('response') - ->action(function(float $timing, Response $response) { - $response->send('Request Unix timestamp: ' . \strval($timing)); + ->action(function(float $bootTime, Response $response) { + $response->send('Process started at: ' . \strval($bootTime)); }); ``` @@ -244,10 +246,10 @@ Inject resource into a hook: ```php Http::shutdown() - ->inject('timing') - ->action(function(float $timing) { - $difference = \microtime(true) - $timing; - \var_dump("Request took: " . $difference . " seconds"); + ->inject('bootTime') + ->action(function(float $bootTime) { + $uptime = \microtime(true) - $bootTime; + \var_dump("Process uptime: " . $uptime . " seconds"); }); ``` diff --git a/composer.lock b/composer.lock index b536f14e..9e96fc46 100644 --- a/composer.lock +++ b/composer.lock @@ -6,22 +6,76 @@ ], "content-hash": "0b2a167dd76d9f5860a3028ac4ce13db", "packages": [ + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, { "name": "utopia-php/di", "version": "dev-chore/extract-http-di-resources", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "3d8a454963a1db81cd63bc4f0f74c1488716e6b8" + "reference": "66f3091bc89db14995215a6e09e9332a2434a83b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/3d8a454963a1db81cd63bc4f0f74c1488716e6b8", - "reference": "3d8a454963a1db81cd63bc4f0f74c1488716e6b8", + "url": "https://api.github.com/repos/utopia-php/di/zipball/66f3091bc89db14995215a6e09e9332a2434a83b", + "reference": "66f3091bc89db14995215a6e09e9332a2434a83b", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.2", + "psr/container": "^2.0" }, "require-dev": { "laravel/pint": "^1.27", @@ -45,7 +99,18 @@ "vendor/bin/pint --test" ], "analyze": [ - "vendor/bin/phpstan analyse -c phpstan.neon" + "vendor/bin/phpstan analyse --memory-limit=512M" + ], + "refactor": [ + "tools/rector/vendor/bin/rector process --config=rector.php" + ], + "refactor:check": [ + "tools/rector/vendor/bin/rector process --dry-run --config=rector.php" + ], + "fix": [ + "@refactor", + "@analyze", + "@format" ], "test": [ "vendor/bin/phpunit --configuration phpunit.xml" @@ -56,16 +121,18 @@ ], "description": "A simple and lite library for managing dependency injections", "keywords": [ - "framework", - "http", + "container", + "dependency-injection", + "di", "php", + "psr-11", "utopia" ], "support": { "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-11T10:23:32+00:00" + "time": "2026-03-12T04:46:22+00:00" }, { "name": "utopia-php/validators", @@ -1323,59 +1390,6 @@ }, "time": "2021-02-03T23:26:27+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, { "name": "psr/log", "version": "3.0.2", diff --git a/src/Http/Http.php b/src/Http/Http.php index 4eeeddd3..cafa5611 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,7 +2,8 @@ namespace Utopia\Http; -use Utopia\DI\Container as DIContainer; +use Utopia\DI\Container; +use Utopia\DI\Dependency; use Utopia\Validator; class Http @@ -38,12 +39,12 @@ class Http */ protected Files $files; - protected DIContainer $resourceContainer; + protected Container $resourceContainer; /** - * @var array> + * @var array */ - protected array $resourceNames = []; + protected array $scopedContainers = []; /** * Current running mode @@ -129,14 +130,14 @@ class Http * * @param Adapter $server * @param string $timezone - * @param DIContainer|null $resourceContainer + * @param Container|null $resourceContainer */ - public function __construct(Adapter $server, string $timezone, ?DIContainer $resourceContainer = null) + public function __construct(Adapter $server, string $timezone, ?Container $resourceContainer = null) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - $this->resourceContainer = $resourceContainer ?? new DIContainer(); + $this->resourceContainer = $resourceContainer ?? new Container(); } /** @@ -879,7 +880,7 @@ public static function reset(): void self::$wildcardRoute = null; } - public function getResourceContainer(): DIContainer + public function getResourceContainer(): Container { return $this->resourceContainer; } @@ -891,7 +892,11 @@ protected function resolveResource(string $name, string $context = 'utopia', boo } try { - return $this->resourceContainer->getResource($name, $context, $fresh); + if ($fresh && $context !== 'utopia') { + unset($this->scopedContainers[$context]); + } + + return $this->getContextContainer($context)->get($name); } catch (\Throwable $e) { $message = \str_replace('dependency', 'resource', $e->getMessage()); @@ -920,8 +925,7 @@ protected function registerResource(string $name, callable $callback, array $inj throw new Exception("'utopia' is a reserved keyword.", 500); } - $this->resourceContainer->setResource($name, $callback, $injections, $context); - $this->resourceNames[$context][$name] = true; + $this->getContextContainer($context)->set($name, new Dependency($injections, $callback)); } protected function registerRequestResources(Request $request, Response $response, string $context, array $resources = []): void @@ -933,19 +937,28 @@ protected function registerRequestResources(Request $request, Response $response protected function refreshResources(string $context): void { - $resources = \array_unique(\array_merge( - \array_keys($this->resourceNames['utopia'] ?? []), - \array_keys($this->resourceNames[$context] ?? []) - )); - - foreach ($resources as $resource) { - $this->resourceContainer->refresh($resource, $context); + if ($context === 'utopia') { + return; } + + unset($this->scopedContainers[$context]); } protected function purgeResources(string $context): void { - $this->resourceContainer->purge($context); - unset($this->resourceNames[$context]); + unset($this->scopedContainers[$context]); + } + + protected function getContextContainer(string $context = 'utopia'): Container + { + if ($context === 'utopia') { + return $this->resourceContainer; + } + + if (!isset($this->scopedContainers[$context])) { + $this->scopedContainers[$context] = $this->resourceContainer->scope(); + } + + return $this->scopedContainers[$context]; } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index b1c63157..21280b7a 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Utopia\DI\Container; +use Utopia\DI\Dependency; use Utopia\Http\Tests\UtopiaFPMRequestTest; use Utopia\Validator\Text; use Utopia\Http\Adapter\FPM\Request; @@ -87,21 +88,21 @@ public function testCanGetEnvironmentVariable(): void public function testCanGetResources(): void { - $this->container->setResource('rand', fn () => rand()); - $this->container->setResource('first', fn ($second) => "first-{$second}", ['second']); - $this->container->setResource('second', fn () => 'second'); + $this->container->set('rand', new Dependency([], fn () => rand())); + $this->container->set('first', new Dependency(['second'], fn ($second) => "first-{$second}")); + $this->container->set('second', new Dependency([], fn () => 'second')); - $second = $this->container->getResource('second', '1'); - $first = $this->container->getResource('first', '1'); + $second = $this->container->get('second'); + $first = $this->container->get('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); - $resource = $this->container->getResource('rand', '1'); + $resource = $this->container->get('rand'); $this->assertNotEmpty($resource); - $this->assertEquals($resource, $this->container->getResource('rand', '1')); - $this->assertEquals($resource, $this->container->getResource('rand', '1')); - $this->assertEquals($resource, $this->container->getResource('rand', '1')); + $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'); @@ -124,11 +125,11 @@ public function testCanGetResources(): void public function testCanGetDefaultValueWithFunction(): void { - $this->container->setResource('first', fn ($second) => "first-{$second}", ['second']); - $this->container->setResource('second', fn () => 'second'); + $this->container->set('first', new Dependency(['second'], fn ($second) => "first-{$second}")); + $this->container->set('second', new Dependency([], fn () => 'second')); - $second = $this->container->getResource('second'); - $first = $this->container->getResource('first'); + $second = $this->container->get('second'); + $first = $this->container->get('first'); $this->assertEquals('second', $second); $this->assertEquals('first-second', $first); @@ -153,8 +154,8 @@ public function testCanGetDefaultValueWithFunction(): void public function testCanExecuteRoute(): void { - $this->container->setResource('rand', fn () => rand()); - $resource = $this->container->getResource('rand', '1'); + $this->container->set('rand', new Dependency([], fn () => rand())); + $resource = $this->container->get('rand'); $this->http ->error() @@ -179,7 +180,7 @@ public function testCanExecuteRoute(): void \ob_end_clean(); // With Params - $resource = $this->container->getResource('rand', '1'); + $resource = $this->container->get('rand'); $route = new Route('GET', '/path'); $route @@ -205,7 +206,7 @@ public function testCanExecuteRoute(): void $this->assertEquals($resource . '-param-x-param-y', $result); // With Error - $resource = $this->container->getResource('rand', '1'); + $resource = $this->container->get('rand'); $route = new Route('GET', '/path'); $route @@ -225,7 +226,7 @@ public function testCanExecuteRoute(): void $this->assertEquals('error: Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); // With Hooks - $resource = $this->container->getResource('rand', '1'); + $resource = $this->container->get('rand'); $this->http ->init() ->inject('rand') @@ -296,7 +297,7 @@ public function testCanExecuteRoute(): void $this->assertEquals('init-' . $resource . '-(init-api)-param-x-param-y-(shutdown-api)-shutdown', $result); - $resource = $this->container->getResource('rand', '1'); + $resource = $this->container->get('rand'); \ob_start(); $request = new UtopiaFPMRequestTest(); $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); @@ -587,7 +588,7 @@ public function testWildcardRoute(): void Http::init() ->action(function () { $route = $this->http->getRoute(); - $this->container->setResource('myRoute', fn () => $route); + $this->container->set('myRoute', new Dependency([], fn () => $route)); }); From 890f2d304115a99f4288d87195794d383af5096a Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 11:07:29 +0530 Subject: [PATCH 11/29] update appwrite base version --- Dockerfile.swoole | 2 +- docker-compose.yml | 2 -- example/Dockerfile | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile.swoole b/Dockerfile.swoole index 6e0fdba1..d74aa5d3 100644 --- a/Dockerfile.swoole +++ b/Dockerfile.swoole @@ -13,7 +13,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM appwrite/base:0.4.3 as final +FROM appwrite/base:0.5.0 as final LABEL maintainer="team@appwrite.io" WORKDIR /usr/src/code diff --git a/docker-compose.yml b/docker-compose.yml index 0c4e0011..322de5ba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: fpm: build: diff --git a/example/Dockerfile b/example/Dockerfile index 3abcbeed..d0128938 100644 --- a/example/Dockerfile +++ b/example/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /usr/local/src/ COPY composer.* /usr/local/src/ RUN composer install --ignore-platform-reqs --optimize-autoloader --no-plugins --no-scripts --prefer-dist -FROM appwrite/base:0.4.3 as final +FROM appwrite/base:0.5.0 as final WORKDIR /usr/src/code COPY ./src /usr/src/code/src COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor From 44b102856f491ed7ec7fcc84714fd3ec1fe89cbd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 11:10:19 +0530 Subject: [PATCH 12/29] update to use php 8.2 --- .github/workflows/test.yml | 2 +- Dockerfile.fpm | 4 ++-- composer.json | 11 ++++++----- composer.lock | 35 ++++++++++++++++++----------------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c707436..087875d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.2' - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 54c948a6..7e99f84e 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -13,11 +13,11 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM php:8.0-cli-alpine as final +FROM php:8.2-cli-alpine as final LABEL maintainer="team@appwrite.io" ENV DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=8 + PHP_VERSION=82 RUN \ apk add --no-cache --virtual .deps \ diff --git a/composer.json b/composer.json index 63fadaa6..41951688 100644 --- a/composer.json +++ b/composer.json @@ -36,15 +36,16 @@ }, "require": { "php": ">=8.2", - "ext-swoole": "*", "utopia-php/di": "dev-chore/extract-http-di-resources", - "utopia-php/validators": "0.2.*" + "utopia-php/validators": "0.2.*", + "ext-swoole": "*" }, "require-dev": { - "phpunit/phpunit": "^9.5.25", + "doctrine/instantiator": "^1.5", "laravel/pint": "1.*", - "swoole/ide-helper": "4.8.3", + "phpbench/phpbench": "^1.2", "phpstan/phpstan": "1.*", - "phpbench/phpbench": "^1.2" + "phpunit/phpunit": "^9.5.25", + "swoole/ide-helper": "4.8.3" } } diff --git a/composer.lock b/composer.lock index 9e96fc46..1a2ae8bf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b2a167dd76d9f5860a3028ac4ce13db", + "content-hash": "ef5f58135105bd84a8d21e3736fcd4ab", "packages": [ { "name": "psr/container", @@ -65,12 +65,12 @@ "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "66f3091bc89db14995215a6e09e9332a2434a83b" + "reference": "f4bbf60d05edb2342ea09d44e90573180c212ec7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/66f3091bc89db14995215a6e09e9332a2434a83b", - "reference": "66f3091bc89db14995215a6e09e9332a2434a83b", + "url": "https://api.github.com/repos/utopia-php/di/zipball/f4bbf60d05edb2342ea09d44e90573180c212ec7", + "reference": "f4bbf60d05edb2342ea09d44e90573180c212ec7", "shasum": "" }, "require": { @@ -132,7 +132,7 @@ "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-12T04:46:22+00:00" + "time": "2026-03-12T05:13:45+00:00" }, { "name": "utopia-php/validators", @@ -260,29 +260,30 @@ }, { "name": "doctrine/instantiator", - "version": "2.1.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", - "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { - "php": "^8.4" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^14", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5.58" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -309,7 +310,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -325,7 +326,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T06:47:08+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "doctrine/lexer", From 5bc45e3cbc41d5bf8d475aa8109b8c48a9da4996 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 11:34:20 +0530 Subject: [PATCH 13/29] fix: restore php 8.2 test runtime --- Dockerfile.fpm | 8 ++++---- composer.lock | 2 +- src/Http/Response.php | 14 +++++++++++--- tests/docker/start | 8 ++++---- tests/docker/supervisord.conf | 4 ++-- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 7e99f84e..8d8eb428 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -13,15 +13,15 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM php:8.2-cli-alpine as final +FROM php:8.2-fpm-alpine as final LABEL maintainer="team@appwrite.io" ENV DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=82 + PHP_FPM_POOL_CONF=/usr/local/etc/php-fpm.d/www.conf RUN \ apk add --no-cache --virtual .deps \ - supervisor php$PHP_VERSION php$PHP_VERSION-fpm php$PHP_VERSION-mbstring nginx bash + supervisor nginx bash # Nginx Configuration (with self-signed ssl certificates) @@ -29,7 +29,7 @@ COPY ./tests/docker/nginx.conf /etc/nginx/nginx.conf # PHP Configuration RUN mkdir -p /var/run/php -COPY ./tests/docker/www.conf /etc/php/$PHP_VERSION/fpm/pool.d/www.conf +COPY ./tests/docker/www.conf /usr/local/etc/php-fpm.d/www.conf # Script COPY ./tests/docker/start /usr/local/bin/start diff --git a/composer.lock b/composer.lock index 1a2ae8bf..6494a449 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ef5f58135105bd84a8d21e3736fcd4ab", + "content-hash": "190b1ab566fa3fb6fec7778fe45dcad8", "packages": [ { "name": "psr/container", diff --git a/src/Http/Response.php b/src/Http/Response.php index 2bc4a5f4..444bc4f8 100755 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -224,6 +224,8 @@ abstract class Response */ protected bool $sent = false; + protected bool $headersSent = false; + /** * @var array> */ @@ -488,6 +490,8 @@ public function send(string $body = ''): void ->appendCookies() ->appendHeaders(); + $this->headersSent = true; + if (!$this->disablePayload) { $length = strlen($body); @@ -566,9 +570,13 @@ public function chunk(string $body = '', bool $end = false): void $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime)); - $this - ->appendCookies() - ->appendHeaders(); + if (!$this->headersSent) { + $this + ->appendCookies() + ->appendHeaders(); + + $this->headersSent = true; + } if (!$this->disablePayload) { $this->write($body); diff --git a/tests/docker/start b/tests/docker/start index f6df2a64..66526ef0 100755 --- a/tests/docker/start +++ b/tests/docker/start @@ -1,6 +1,6 @@ #!/bin/bash -export PHP_VERSION=$PHP_VERSION +POOL_CONF="${PHP_FPM_POOL_CONF:-/usr/local/etc/php-fpm.d/www.conf}" chown -Rf www-data.www-data /usr/share/nginx/html/ @@ -12,13 +12,13 @@ function setEnvironmentVariable() { fi # Check whether variable already exists - if ! grep -q "\[$1\]" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf; then + if ! grep -q "\[$1\]" "$POOL_CONF"; then # Add variable - echo "env[$1] = $2" >> /etc/php/$PHP_VERSION/fpm/pool.d/www.conf + echo "env[$1] = $2" >> "$POOL_CONF" fi # Reset variable - # sed -i "s/^env\[$1.*/env[$1] = $2/g" /etc/php/$PHP_VERSION/fpm/pool.d/www.conf + # sed -i "s/^env\[$1.*/env[$1] = $2/g" "$POOL_CONF" } # Start supervisord and services diff --git a/tests/docker/supervisord.conf b/tests/docker/supervisord.conf index 41f5c4d3..00c651f3 100644 --- a/tests/docker/supervisord.conf +++ b/tests/docker/supervisord.conf @@ -22,8 +22,8 @@ supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket -[program:php8-fpm] -command=php-fpm%(ENV_PHP_VERSION)s -F +[program:php-fpm] +command=php-fpm -F autostart=true autorestart=true priority=5 From a4c9cfd9faa22b3ec13c2ce62d31072f186a1e5e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 13:39:37 +0530 Subject: [PATCH 14/29] chore: use container scopes --- composer.json | 2 +- composer.lock | 18 +++--- src/Http/Http.php | 142 +++++++++++++++++---------------------------- tests/HttpTest.php | 42 ++++++++++++++ 4 files changed, 103 insertions(+), 101 deletions(-) diff --git a/composer.json b/composer.json index 41951688..66c6d4d1 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": ">=8.2", - "utopia-php/di": "dev-chore/extract-http-di-resources", + "utopia-php/di": "0.2.*", "utopia-php/validators": "0.2.*", "ext-swoole": "*" }, diff --git a/composer.lock b/composer.lock index 6494a449..2ee8cf5b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "190b1ab566fa3fb6fec7778fe45dcad8", + "content-hash": "b7bee41baa98a69509660346704ff023", "packages": [ { "name": "psr/container", @@ -61,16 +61,16 @@ }, { "name": "utopia-php/di", - "version": "dev-chore/extract-http-di-resources", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "f4bbf60d05edb2342ea09d44e90573180c212ec7" + "reference": "6a9135908dddef66fd07733705b9625f42d302a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/f4bbf60d05edb2342ea09d44e90573180c212ec7", - "reference": "f4bbf60d05edb2342ea09d44e90573180c212ec7", + "url": "https://api.github.com/repos/utopia-php/di/zipball/6a9135908dddef66fd07733705b9625f42d302a4", + "reference": "6a9135908dddef66fd07733705b9625f42d302a4", "shasum": "" }, "require": { @@ -129,10 +129,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/di/tree/chore/extract-http-di-resources", + "source": "https://github.com/utopia-php/di/tree/0.2.0", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-12T05:13:45+00:00" + "time": "2026-03-12T07:54:46+00:00" }, { "name": "utopia-php/validators", @@ -3603,9 +3603,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "utopia-php/di": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Http/Http.php b/src/Http/Http.php index cafa5611..89513340 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -41,11 +41,6 @@ class Http protected Container $resourceContainer; - /** - * @var array - */ - protected array $scopedContainers = []; - /** * Current running mode * @@ -499,12 +494,10 @@ public static function onRequest(): Hook public function start() { $this->server->onRequest(function ($request, $response, $context, array $resources = []) { - try { - $this->registerRequestResources($request, $response, $context, $resources); - $this->run($request, $response, $context); - } finally { - $this->purgeResources($context); - } + $container = $this->resourceContainer->scope(); + + $this->registerRequestResources($resources, $container); + $this->run($request, $response, $context, $container); }); $this->server->onStart(function ($server) { $this->registerResource('server', function () use ($server) { @@ -513,7 +506,7 @@ public function start() try { foreach (self::$startHooks as $hook) { - $arguments = $this->getArguments($hook, 'utopia', [], []); + $arguments = $this->getArguments($hook, $this->resourceContainer, [], []); \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { @@ -522,7 +515,7 @@ public function start() foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, 'utopia', [], []); + $arguments = $this->getArguments($error, $this->resourceContainer, [], []); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -565,17 +558,19 @@ public function match(Request $request, bool $fresh = true): ?Route * @param Route $route * @param Request $request */ - public function execute(Route $route, Request $request, string $context): static + public function execute(Route $route, Request $request, string $context, ?Container $container = null): static { $arguments = []; $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); + $container ??= $this->resourceContainer->scope(); + $scope = $container->scope(); try { if ($route->getHook()) { foreach (self::$init as $hook) { // Global init hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -584,19 +579,19 @@ public function execute(Route $route, Request $request, string $context): static foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } - $arguments = $this->getArguments($route, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $scope, $pathValues, $request->getParams()); \call_user_func_array($route->getAction(), $arguments); foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -605,19 +600,19 @@ public function execute(Route $route, Request $request, string $context): static if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { - $this->registerResource('error', fn () => $e, [], $context); + $this->registerResource('error', fn () => $e, [], $scope); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks if (in_array($group, $error->getGroups())) { try { - $arguments = $this->getArguments($error, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $scope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -629,7 +624,7 @@ public function execute(Route $route, Request $request, string $context): static foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $context, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $scope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -638,9 +633,6 @@ public function execute(Route $route, Request $request, string $context): static } } - // Reset resources for the context - $this->refreshResources($context); - return $this; } @@ -654,7 +646,7 @@ public function execute(Route $route, Request $request, string $context): static * * @throws Exception */ - protected function getArguments(Hook $hook, string $context, array $values, array $requestParams): array + protected function getArguments(Hook $hook, Container $container, array $values, array $requestParams): array { $arguments = []; foreach ($hook->getParams() as $key => $param) { // Get value from route or request object @@ -664,7 +656,7 @@ protected function getArguments(Hook $hook, string $context, array $values, arra $arg = $existsInRequest ? $requestParams[$key] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, $this->resolveResources($param['injections'], $context)); + $arg = \call_user_func_array($arg, $this->resolveResources($param['injections'], $container)); } $value = $existsInValues ? $values[$key] : $arg; @@ -674,7 +666,7 @@ protected function getArguments(Hook $hook, string $context, array $values, arra } if ($paramExists) { - $this->validate($key, $param, $value, $context); + $this->validate($key, $param, $value, $container); } } @@ -683,7 +675,7 @@ protected function getArguments(Hook $hook, string $context, array $values, arra } foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->resolveResource($injection['name'], $context); + $arguments[$injection['order']] = $this->resolveResource($injection['name'], $container); } return $arguments; @@ -698,25 +690,26 @@ protected function getArguments(Hook $hook, string $context, array $values, arra * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context): static + public function run(Request $request, Response $response, string $context, ?Container $container = null): static { - $this->registerResource('context', fn () => $context, [], $context); - $this->registerResource('request', fn () => $request, [], $context); - $this->registerResource('response', fn () => $response, [], $context); + $container ??= $this->resourceContainer->scope(); + $this->registerResource('context', fn () => $context, [], $container); + $this->registerResource('request', fn () => $request, [], $container); + $this->registerResource('response', fn () => $response, [], $container); try { foreach (self::$requestHooks as $hook) { - $arguments = $this->getArguments($hook, $context, [], []); + $arguments = $this->getArguments($hook, $container, [], []); \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->registerResource('error', fn () => $e, [], $context); + $this->registerResource('error', fn () => $e, [], $container); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $context, [], []); + $arguments = $this->getArguments($error, $container, [], []); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -740,7 +733,7 @@ public function run(Request $request, Response $response, string $context): stat $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->registerResource('route', fn () => $route, [], $context); + $this->registerResource('route', fn () => $route, [], $container); if (self::REQUEST_METHOD_HEAD == $method) { $method = self::REQUEST_METHOD_GET; @@ -753,7 +746,7 @@ public function run(Request $request, Response $response, string $context): stat 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, $context, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($option, $container, [], $request->getParams())); } } } @@ -761,7 +754,7 @@ public function run(Request $request, Response $response, string $context): stat 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, $context, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($option, $container, [], $request->getParams())); } } } catch (\Throwable $e) { @@ -770,8 +763,8 @@ public function run(Request $request, Response $response, string $context): stat if (in_array('*', $error->getGroups())) { $this->registerResource('error', function () use ($e) { return $e; - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + }, [], $container); + \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); } } } @@ -785,24 +778,24 @@ public function run(Request $request, Response $response, string $context): stat $path = \parse_url($request->getURI(), PHP_URL_PATH); $route->path($path); - $this->registerResource('route', fn () => $route, [], $context); + $this->registerResource('route', fn () => $route, [], $container); } if (null !== $route) { - return $this->execute($route, $request, $context); + return $this->execute($route, $request, $context, $container); } 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, $context, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($option, $container, [], $request->getParams())); } } } foreach (self::$options as $option) { // Global options hooks if (in_array('*', $option->getGroups())) { - \call_user_func_array($option->getAction(), $this->getArguments($option, $context, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($option, $container, [], $request->getParams())); } } } catch (\Throwable $e) { @@ -810,8 +803,8 @@ public function run(Request $request, Response $response, string $context): stat if (in_array('*', $error->getGroups())) { $this->registerResource('error', function () use ($e) { return $e; - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + }, [], $container); + \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); } } } @@ -820,8 +813,8 @@ public function run(Request $request, Response $response, string $context): stat if (in_array('*', $error->getGroups())) { $this->registerResource('error', function () { return new Exception('Not Found', 404); - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + }, [], $container); + \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); } } } @@ -841,7 +834,7 @@ public function run(Request $request, Response $response, string $context): stat * * @throws Exception */ - protected function validate(string $key, array $param, mixed $value, $context): void + protected function validate(string $key, array $param, mixed $value, Container $container): void { if ($param['optional'] && \is_null($value)) { return; @@ -850,7 +843,7 @@ protected function validate(string $key, array $param, mixed $value, $context): $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->resolveResources($param['injections'], $context)); + $validator = \call_user_func_array($validator, $this->resolveResources($param['injections'], $container)); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class @@ -885,18 +878,14 @@ public function getResourceContainer(): Container return $this->resourceContainer; } - protected function resolveResource(string $name, string $context = 'utopia', bool $fresh = false): mixed + protected function resolveResource(string $name, Container $container): mixed { if ($name === 'utopia') { return $this; } try { - if ($fresh && $context !== 'utopia') { - unset($this->scopedContainers[$context]); - } - - return $this->getContextContainer($context)->get($name); + return $container->get($name); } catch (\Throwable $e) { $message = \str_replace('dependency', 'resource', $e->getMessage()); @@ -908,57 +897,30 @@ protected function resolveResource(string $name, string $context = 'utopia', boo } } - protected function resolveResources(array $list, string $context = 'utopia'): array + protected function resolveResources(array $list, Container $container): array { $resources = []; foreach ($list as $name) { - $resources[$name] = $this->resolveResource($name, $context); + $resources[$name] = $this->resolveResource($name, $container); } return $resources; } - protected function registerResource(string $name, callable $callback, array $injections = [], string $context = 'utopia'): void + protected function registerResource(string $name, callable $callback, array $injections = [], ?Container $container = null): void { if ($name === 'utopia') { throw new Exception("'utopia' is a reserved keyword.", 500); } - $this->getContextContainer($context)->set($name, new Dependency($injections, $callback)); + ($container ?? $this->resourceContainer)->set($name, new Dependency($injections, $callback)); } - protected function registerRequestResources(Request $request, Response $response, string $context, array $resources = []): void + protected function registerRequestResources(array $resources = [], ?Container $container = null): void { foreach ($resources as $name => $resource) { - $this->registerResource($name, fn () => $resource, [], $context); + $this->registerResource($name, fn () => $resource, [], $container); } } - - protected function refreshResources(string $context): void - { - if ($context === 'utopia') { - return; - } - - unset($this->scopedContainers[$context]); - } - - protected function purgeResources(string $context): void - { - unset($this->scopedContainers[$context]); - } - - protected function getContextContainer(string $context = 'utopia'): Container - { - if ($context === 'utopia') { - return $this->resourceContainer; - } - - if (!isset($this->scopedContainers[$context])) { - $this->scopedContainers[$context] = $this->resourceContainer->scope(); - } - - return $this->scopedContainers[$context]; - } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 21280b7a..9324b1e5 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -577,6 +577,48 @@ public function testCanRunRequest(): void $this->assertStringNotContainsString('HELLO', $result); } + public function testScopedResourcesUseNestedContainers(): void + { + $counter = 0; + $this->container->set('shared', new Dependency([], function () use (&$counter) { + $counter++; + + return $counter; + })); + + $http = new class (new Server(), 'Asia/Tel_Aviv', $this->container) extends Http { + public function createScope(?Container $container = null): Container + { + return ($container ?? $this->getResourceContainer())->scope(); + } + + public function shareValue(string $name, mixed $value, Container $container): void + { + $this->registerResource($name, fn () => $value, [], $container); + } + + public function resource(string $name, Container $container): mixed + { + return $this->resolveResource($name, $container); + } + }; + + $requestA = $http->createScope(); + $requestB = $http->createScope(); + $executionA = $http->createScope($requestA); + + $http->shareValue('requestId', 'request-a', $requestA); + $http->shareValue('requestId', 'request-b', $requestB); + $http->shareValue('requestId', 'execution-a', $executionA); + + $this->assertSame(1, $http->resource('shared', $requestA)); + $this->assertSame(1, $http->resource('shared', $requestB)); + $this->assertSame('request-a', $http->resource('requestId', $requestA)); + $this->assertSame('request-b', $http->resource('requestId', $requestB)); + $this->assertSame('execution-a', $http->resource('requestId', $executionA)); + $this->assertSame('request-a', $http->resource('requestId', $requestA)); + } + public function testWildcardRoute(): void { $method = $_SERVER['REQUEST_METHOD'] ?? null; From 62402857919103b67b3de8db12765433ac04e56c Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 13:42:59 +0530 Subject: [PATCH 15/29] remove utopia keyword --- src/Http/Http.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index 89513340..6d9aa180 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -880,10 +880,6 @@ public function getResourceContainer(): Container protected function resolveResource(string $name, Container $container): mixed { - if ($name === 'utopia') { - return $this; - } - try { return $container->get($name); } catch (\Throwable $e) { @@ -910,10 +906,6 @@ protected function resolveResources(array $list, Container $container): array protected function registerResource(string $name, callable $callback, array $injections = [], ?Container $container = null): void { - if ($name === 'utopia') { - throw new Exception("'utopia' is a reserved keyword.", 500); - } - ($container ?? $this->resourceContainer)->set($name, new Dependency($injections, $callback)); } From 6fd0984a4940b7a1b27bc61631b3282b3aa0f5b4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 13:46:26 +0530 Subject: [PATCH 16/29] remove optional container in run --- src/Http/Http.php | 4 ++-- tests/HttpTest.php | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index 6d9aa180..dc0ea808 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -690,9 +690,8 @@ protected function getArguments(Hook $hook, Container $container, array $values, * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context, ?Container $container = null): static + public function run(Request $request, Response $response, string $context, Container $container): static { - $container ??= $this->resourceContainer->scope(); $this->registerResource('context', fn () => $context, [], $container); $this->registerResource('request', fn () => $request, [], $container); $this->registerResource('response', fn () => $response, [], $container); @@ -883,6 +882,7 @@ protected function resolveResource(string $name, Container $container): mixed try { return $container->get($name); } catch (\Throwable $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')) { diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 9324b1e5..798f576a 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -566,8 +566,10 @@ public function testCanRunRequest(): void $response->send('HELLO'); }); + $scope = $this->http->getResourceContainer()->scope(); + \ob_start(); - $this->http->run(new Request(), new Response(), '1'); + $this->http->run(new Request(), new Response(), '1', $scope); $result = \ob_get_contents(); \ob_end_clean(); @@ -645,8 +647,10 @@ public function testWildcardRoute(): void } }); + $scope = $this->http->getResourceContainer()->scope(); + \ob_start(); - @$this->http->run(new Request(), new Response(), '1'); + @$this->http->run(new Request(), new Response(), '1', $scope); $result = \ob_get_contents(); \ob_end_clean(); @@ -655,7 +659,7 @@ public function testWildcardRoute(): void \ob_start(); $req = new Request(); $req = $req->setMethod('OPTIONS'); - @$this->http->run($req, new Response(), '1'); + @$this->http->run($req, new Response(), '1', $this->http->getResourceContainer()->scope()); $result = \ob_get_contents(); \ob_end_clean(); From ec4c1ffb29aec176931cdfd9c026b31e091c8537 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 13:49:30 +0530 Subject: [PATCH 17/29] remove optional container in run --- src/Http/Http.php | 14 +++++++++----- tests/HttpTest.php | 10 +++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index dc0ea808..1d8555c1 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -494,10 +494,7 @@ public static function onRequest(): Hook public function start() { $this->server->onRequest(function ($request, $response, $context, array $resources = []) { - $container = $this->resourceContainer->scope(); - - $this->registerRequestResources($resources, $container); - $this->run($request, $response, $context, $container); + $this->runWithResources($request, $response, $context, $resources); }); $this->server->onStart(function ($server) { $this->registerResource('server', function () use ($server) { @@ -690,8 +687,15 @@ protected function getArguments(Hook $hook, Container $container, array $values, * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context, Container $container): static + public function run(Request $request, Response $response, string $context): static + { + return $this->runWithResources($request, $response, $context); + } + + protected function runWithResources(Request $request, Response $response, string $context, array $resources = []): static { + $container = $this->resourceContainer->scope(); + $this->registerRequestResources($resources, $container); $this->registerResource('context', fn () => $context, [], $container); $this->registerResource('request', fn () => $request, [], $container); $this->registerResource('response', fn () => $response, [], $container); diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 798f576a..9324b1e5 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -566,10 +566,8 @@ public function testCanRunRequest(): void $response->send('HELLO'); }); - $scope = $this->http->getResourceContainer()->scope(); - \ob_start(); - $this->http->run(new Request(), new Response(), '1', $scope); + $this->http->run(new Request(), new Response(), '1'); $result = \ob_get_contents(); \ob_end_clean(); @@ -647,10 +645,8 @@ public function testWildcardRoute(): void } }); - $scope = $this->http->getResourceContainer()->scope(); - \ob_start(); - @$this->http->run(new Request(), new Response(), '1', $scope); + @$this->http->run(new Request(), new Response(), '1'); $result = \ob_get_contents(); \ob_end_clean(); @@ -659,7 +655,7 @@ public function testWildcardRoute(): void \ob_start(); $req = new Request(); $req = $req->setMethod('OPTIONS'); - @$this->http->run($req, new Response(), '1', $this->http->getResourceContainer()->scope()); + @$this->http->run($req, new Response(), '1'); $result = \ob_get_contents(); \ob_end_clean(); From f1020e7a94b181933eeadb447f9a2ada993b47a9 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 14:00:30 +0530 Subject: [PATCH 18/29] renaming --- src/Http/Http.php | 210 ++++++++++++++++++++++++--------------------- tests/HttpTest.php | 30 +++---- 2 files changed, 125 insertions(+), 115 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index 1d8555c1..d8f6bd78 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -39,7 +39,7 @@ class Http */ protected Files $files; - protected Container $resourceContainer; + protected Container $container; /** * Current running mode @@ -125,14 +125,13 @@ class Http * * @param Adapter $server * @param string $timezone - * @param Container|null $resourceContainer */ - public function __construct(Adapter $server, string $timezone, ?Container $resourceContainer = null) + public function __construct(Adapter $server, string $timezone) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - $this->resourceContainer = $resourceContainer ?? new Container(); + $this->container = new Container(); } /** @@ -344,6 +343,64 @@ public static function setAllowOverride(bool $value): void Router::setAllowOverride($value); } + /** + * Get the root application container. + */ + public function getContainer(): Container + { + return $this->container; + } + + /** + * Resolve a single resource from the given scope. + * + * @throws Exception + */ + protected function getResource(string $name, Container $scope): mixed + { + 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 ($message === $e->getMessage() && !\str_contains($message, 'resource')) { + $message = 'Failed to find resource: "' . $name . '"'; + } + + throw new Exception($message, 500, $e); + } + } + + /** + * Resolve multiple resources from the given scope. + * + * @param string[] $list + * @return array + * + * @throws Exception + */ + protected function getResources(array $list, Container $scope): array + { + $resources = []; + + foreach ($list as $name) { + $resources[$name] = $this->getResource($name, $scope); + } + + return $resources; + } + + /** + * Register a resource factory on the given scope. + * + * @param string[] $injections + */ + protected function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void + { + ($scope ?? $this->container)->set($name, new Dependency($injections, $callback)); + } + /** * Is http in production mode? * @@ -494,25 +551,25 @@ public static function onRequest(): Hook public function start() { $this->server->onRequest(function ($request, $response, $context, array $resources = []) { - $this->runWithResources($request, $response, $context, $resources); + $this->run($request, $response, $context, $resources); }); $this->server->onStart(function ($server) { - $this->registerResource('server', function () use ($server) { + $this->setResource('server', function () use ($server) { return $server; }); try { foreach (self::$startHooks as $hook) { - $arguments = $this->getArguments($hook, $this->resourceContainer, [], []); + $arguments = $this->getArguments($hook, $this->container, [], []); \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->registerResource('error', fn () => $e); + $this->setResource('error', fn () => $e); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $this->resourceContainer, [], []); + $arguments = $this->getArguments($error, $this->container, [], []); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -555,19 +612,19 @@ public function match(Request $request, bool $fresh = true): ?Route * @param Route $route * @param Request $request */ - public function execute(Route $route, Request $request, string $context, ?Container $container = null): static + public function execute(Route $route, Request $request, string $context, ?Container $requestScope = null): static { $arguments = []; $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); - $container ??= $this->resourceContainer->scope(); - $scope = $container->scope(); + $requestScope ??= $this->container->scope(); + $executionScope = $requestScope->scope(); try { if ($route->getHook()) { foreach (self::$init as $hook) { // Global init hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -576,19 +633,19 @@ public function execute(Route $route, Request $request, string $context, ?Contai foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } - $arguments = $this->getArguments($route, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($route->getAction(), $arguments); foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -597,19 +654,19 @@ public function execute(Route $route, Request $request, string $context, ?Contai if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { - $this->registerResource('error', fn () => $e, [], $scope); + $this->setResource('error', fn () => $e, [], $executionScope); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks if (in_array($group, $error->getGroups())) { try { - $arguments = $this->getArguments($error, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -621,7 +678,7 @@ public function execute(Route $route, Request $request, string $context, ?Contai foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $scope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $executionScope, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -643,7 +700,7 @@ public function execute(Route $route, Request $request, string $context, ?Contai * * @throws Exception */ - protected function getArguments(Hook $hook, Container $container, 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 @@ -653,7 +710,7 @@ protected function getArguments(Hook $hook, Container $container, array $values, $arg = $existsInRequest ? $requestParams[$key] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, $this->resolveResources($param['injections'], $container)); + $arg = \call_user_func_array($arg, $this->getResources($param['injections'], $scope)); } $value = $existsInValues ? $values[$key] : $arg; @@ -663,7 +720,7 @@ protected function getArguments(Hook $hook, Container $container, array $values, } if ($paramExists) { - $this->validate($key, $param, $value, $container); + $this->validate($key, $param, $value, $scope); } } @@ -672,7 +729,7 @@ protected function getArguments(Hook $hook, Container $container, array $values, } foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->resolveResource($injection['name'], $container); + $arguments[$injection['order']] = $this->getResource($injection['name'], $scope); } return $arguments; @@ -687,32 +744,29 @@ protected function getArguments(Hook $hook, Container $container, array $values, * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context): static - { - return $this->runWithResources($request, $response, $context); - } - - protected function runWithResources(Request $request, Response $response, string $context, array $resources = []): static + public function run(Request $request, Response $response, string $context, array $resources = []): static { - $container = $this->resourceContainer->scope(); - $this->registerRequestResources($resources, $container); - $this->registerResource('context', fn () => $context, [], $container); - $this->registerResource('request', fn () => $request, [], $container); - $this->registerResource('response', fn () => $response, [], $container); + $requestScope = $this->container->scope(); + foreach ($resources as $name => $resource) { + $this->setResource($name, fn () => $resource, [], $requestScope); + } + $this->setResource('context', fn () => $context, [], $requestScope); + $this->setResource('request', fn () => $request, [], $requestScope); + $this->setResource('response', fn () => $response, [], $requestScope); try { foreach (self::$requestHooks as $hook) { - $arguments = $this->getArguments($hook, $container, [], []); + $arguments = $this->getArguments($hook, $requestScope, [], []); \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->registerResource('error', fn () => $e, [], $container); + $this->setResource('error', fn () => $e, [], $requestScope); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $container, [], []); + $arguments = $this->getArguments($error, $requestScope, [], []); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -736,7 +790,7 @@ protected function runWithResources(Request $request, Response $response, string $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->registerResource('route', fn () => $route, [], $container); + $this->setResource('route', fn () => $route, [], $requestScope); if (self::REQUEST_METHOD_HEAD == $method) { $method = self::REQUEST_METHOD_GET; @@ -749,7 +803,7 @@ protected function runWithResources(Request $request, Response $response, string 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, $container, [], $request->getParams())); + \call_user_func_array($option->getAction(), $this->getArguments($option, $requestScope, [], $request->getParams())); } } } @@ -757,17 +811,17 @@ protected function runWithResources(Request $request, Response $response, string 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, $container, [], $request->getParams())); + \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())) { - $this->registerResource('error', function () use ($e) { + $this->setResource('error', function () use ($e) { return $e; - }, [], $container); - \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); + }, [], $requestScope); + \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); } } } @@ -781,43 +835,43 @@ protected function runWithResources(Request $request, Response $response, string $path = \parse_url($request->getURI(), PHP_URL_PATH); $route->path($path); - $this->registerResource('route', fn () => $route, [], $container); + $this->setResource('route', fn () => $route, [], $requestScope); } if (null !== $route) { - return $this->execute($route, $request, $context, $container); + return $this->execute($route, $request, $context, $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, $container, [], $request->getParams())); + \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, $container, [], $request->getParams())); + \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->registerResource('error', function () use ($e) { + $this->setResource('error', function () use ($e) { return $e; - }, [], $container); - \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); + }, [], $requestScope); + \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); } } } } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - $this->registerResource('error', function () { + $this->setResource('error', function () { return new Exception('Not Found', 404); - }, [], $container); - \call_user_func_array($error->getAction(), $this->getArguments($error, $container, [], $request->getParams())); + }, [], $requestScope); + \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); } } } @@ -837,7 +891,7 @@ protected function runWithResources(Request $request, Response $response, string * * @throws Exception */ - protected function validate(string $key, array $param, mixed $value, Container $container): void + protected function validate(string $key, array $param, mixed $value, Container $scope): void { if ($param['optional'] && \is_null($value)) { return; @@ -846,7 +900,7 @@ protected function validate(string $key, array $param, mixed $value, Container $ $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->resolveResources($param['injections'], $container)); + $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 @@ -875,48 +929,4 @@ public static function reset(): void self::$requestHooks = []; self::$wildcardRoute = null; } - - public function getResourceContainer(): Container - { - return $this->resourceContainer; - } - - protected function resolveResource(string $name, Container $container): mixed - { - try { - return $container->get($name); - } catch (\Throwable $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); - } - } - - protected function resolveResources(array $list, Container $container): array - { - $resources = []; - - foreach ($list as $name) { - $resources[$name] = $this->resolveResource($name, $container); - } - - return $resources; - } - - protected function registerResource(string $name, callable $callback, array $injections = [], ?Container $container = null): void - { - ($container ?? $this->resourceContainer)->set($name, new Dependency($injections, $callback)); - } - - protected function registerRequestResources(array $resources = [], ?Container $container = null): void - { - foreach ($resources as $name => $resource) { - $this->registerResource($name, fn () => $resource, [], $container); - } - } } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 9324b1e5..3c75c746 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -24,8 +24,8 @@ class HttpTest extends TestCase public function setUp(): void { Http::reset(); - $this->container = new Container(); - $this->http = new Http(new Server(), 'Asia/Tel_Aviv', $this->container); + $this->http = new Http(new Server(), 'Asia/Tel_Aviv'); + $this->container = $this->http->getContainer(); $this->saveRequest(); } @@ -580,29 +580,29 @@ public function testCanRunRequest(): void public function testScopedResourcesUseNestedContainers(): void { $counter = 0; - $this->container->set('shared', new Dependency([], function () use (&$counter) { - $counter++; - - return $counter; - })); - - $http = new class (new Server(), 'Asia/Tel_Aviv', $this->container) extends Http { - public function createScope(?Container $container = null): Container + $http = new class (new Server(), 'Asia/Tel_Aviv') extends Http { + public function createScope(?Container $scope = null): Container { - return ($container ?? $this->getResourceContainer())->scope(); + return ($scope ?? $this->getContainer())->scope(); } - public function shareValue(string $name, mixed $value, Container $container): void + public function shareValue(string $name, mixed $value, Container $scope): void { - $this->registerResource($name, fn () => $value, [], $container); + $this->setResource($name, fn () => $value, [], $scope); } - public function resource(string $name, Container $container): mixed + public function resource(string $name, Container $scope): mixed { - return $this->resolveResource($name, $container); + return $this->getResource($name, $scope); } }; + $http->getContainer()->set('shared', new Dependency([], function () use (&$counter) { + $counter++; + + return $counter; + })); + $requestA = $http->createScope(); $requestB = $http->createScope(); $executionA = $http->createScope($requestA); From 9616993cb942a87d7b3cee58d0780cb63406115e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 14:06:48 +0530 Subject: [PATCH 19/29] remove public getContainer --- src/Http/Http.php | 14 +++----------- tests/HttpTest.php | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index d8f6bd78..d728f502 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -344,15 +344,7 @@ public static function setAllowOverride(bool $value): void } /** - * Get the root application container. - */ - public function getContainer(): Container - { - return $this->container; - } - - /** - * Resolve a single resource from the given scope. + * Get a single resource from the given scope. * * @throws Exception */ @@ -373,7 +365,7 @@ protected function getResource(string $name, Container $scope): mixed } /** - * Resolve multiple resources from the given scope. + * Get multiple resources from the given scope. * * @param string[] $list * @return array @@ -392,7 +384,7 @@ protected function getResources(array $list, Container $scope): array } /** - * Register a resource factory on the given scope. + * Set a resource on the given scope. * * @param string[] $injections */ diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 3c75c746..5061f1e5 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -25,7 +25,7 @@ public function setUp(): void { Http::reset(); $this->http = new Http(new Server(), 'Asia/Tel_Aviv'); - $this->container = $this->http->getContainer(); + $this->container = $this->getHttpContainer($this->http); $this->saveRequest(); } @@ -48,6 +48,11 @@ protected function restoreRequest(): void $_SERVER['REQUEST_URI'] = $this->uri; } + protected function getHttpContainer(Http $http): Container + { + return \Closure::bind(fn (): Container => $this->container, $http, Http::class)(); + } + public function testCanGetDifferentModes(): void { $this->assertEmpty(Http::getMode()); @@ -583,12 +588,12 @@ public function testScopedResourcesUseNestedContainers(): void $http = new class (new Server(), 'Asia/Tel_Aviv') extends Http { public function createScope(?Container $scope = null): Container { - return ($scope ?? $this->getContainer())->scope(); + return ($scope ?? $this->container)->scope(); } - public function shareValue(string $name, mixed $value, Container $scope): void + public function defineResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void { - $this->setResource($name, fn () => $value, [], $scope); + $this->setResource($name, $callback, $injections, $scope); } public function resource(string $name, Container $scope): mixed @@ -597,19 +602,19 @@ public function resource(string $name, Container $scope): mixed } }; - $http->getContainer()->set('shared', new Dependency([], function () use (&$counter) { + $http->defineResource('shared', function () use (&$counter) { $counter++; return $counter; - })); + }); $requestA = $http->createScope(); $requestB = $http->createScope(); $executionA = $http->createScope($requestA); - $http->shareValue('requestId', 'request-a', $requestA); - $http->shareValue('requestId', 'request-b', $requestB); - $http->shareValue('requestId', 'execution-a', $executionA); + $http->defineResource('requestId', fn () => 'request-a', scope: $requestA); + $http->defineResource('requestId', fn () => 'request-b', scope: $requestB); + $http->defineResource('requestId', fn () => 'execution-a', scope: $executionA); $this->assertSame(1, $http->resource('shared', $requestA)); $this->assertSame(1, $http->resource('shared', $requestB)); From 233c235836822e6256fd855b2bac6d030abd1221 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 14:57:33 +0530 Subject: [PATCH 20/29] fix getcontainer --- src/Http/Http.php | 14 ++++++++++++-- tests/HttpTest.php | 10 +++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index d728f502..da58e5d6 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -126,12 +126,22 @@ class Http * @param Adapter $server * @param string $timezone */ - public function __construct(Adapter $server, string $timezone) + public function __construct(Adapter $server, string $timezone, ?Container $container = null) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - $this->container = new Container(); + $this->container = $container ?? new Container(); + } + + /** + * Get dependency injection container + * + * @return Container + */ + public function getContainer(): Container + { + return $this->container; } /** diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 5061f1e5..b28d9542 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -25,7 +25,7 @@ public function setUp(): void { Http::reset(); $this->http = new Http(new Server(), 'Asia/Tel_Aviv'); - $this->container = $this->getHttpContainer($this->http); + $this->container = $this->http->getContainer(); $this->saveRequest(); } @@ -48,9 +48,13 @@ protected function restoreRequest(): void $_SERVER['REQUEST_URI'] = $this->uri; } - protected function getHttpContainer(Http $http): Container + public function testCanInjectContainerInConstructor(): void { - return \Closure::bind(fn (): Container => $this->container, $http, Http::class)(); + $container = new Container(); + $http = new Http(new Server(), 'Asia/Tel_Aviv', $container); + + $this->assertSame($container, $http->getContainer()); + $this->assertInstanceOf(Container::class, (new Http(new Server(), 'Asia/Tel_Aviv'))->getContainer()); } public function testCanGetDifferentModes(): void From a39c96ea0adc4f194fd188859218f39ae74d5bab Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 14:59:46 +0530 Subject: [PATCH 21/29] fix getcontainer --- src/Http/Http.php | 2 +- tests/HttpTest.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.php b/src/Http/Http.php index da58e5d6..850d56e2 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -398,7 +398,7 @@ protected function getResources(array $list, Container $scope): array * * @param string[] $injections */ - protected function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void + public function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void { ($scope ?? $this->container)->set($name, new Dependency($injections, $callback)); } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index b28d9542..e01e1949 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -57,6 +57,29 @@ public function testCanInjectContainerInConstructor(): void $this->assertInstanceOf(Container::class, (new Http(new Server(), 'Asia/Tel_Aviv'))->getContainer()); } + public function testCanRegisterResourcesWithPublicApi(): void + { + $this->http->setResource('rand', fn () => 1234); + $this->http->setResource('second', fn () => 'second'); + $this->http->setResource('first', fn ($second) => "first-{$second}", ['second']); + + $route = new Route('GET', '/path'); + + $route + ->inject('rand') + ->inject('first') + ->action(function ($rand, $first) { + echo $rand . '-' . $first; + }); + + \ob_start(); + $this->http->execute($route, new Request(), '1'); + $result = \ob_get_contents(); + \ob_end_clean(); + + $this->assertSame('1234-first-second', $result); + } + public function testCanGetDifferentModes(): void { $this->assertEmpty(Http::getMode()); From 38fecf6da2b2986d05ff0f45a07b823984cb63f3 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 17:03:00 +0530 Subject: [PATCH 22/29] update --- README.md | 6 ++---- composer.json | 2 +- composer.lock | 14 +++++++------- src/Http/Http.php | 9 ++++----- tests/HttpTest.php | 26 ++++++++++---------------- 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index d930037e..5f432d74 100644 --- a/README.md +++ b/README.md @@ -222,11 +222,9 @@ Resources allow you to prepare dependencies for requests such as database connec Define a dependency on the DI container: ```php -use Utopia\DI\Dependency; - -$container->set('bootTime', new Dependency([], function () { +$container->set('bootTime', function () { return \microtime(true); -})); +}, []); ``` Inject resource into endpoint action: diff --git a/composer.json b/composer.json index 66c6d4d1..fa9a0426 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": ">=8.2", - "utopia-php/di": "0.2.*", + "utopia-php/di": "0.3.*", "utopia-php/validators": "0.2.*", "ext-swoole": "*" }, diff --git a/composer.lock b/composer.lock index 2ee8cf5b..b2d920c3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b7bee41baa98a69509660346704ff023", + "content-hash": "6bf6c5703f842ea4d1cdee8068e967b4", "packages": [ { "name": "psr/container", @@ -61,16 +61,16 @@ }, { "name": "utopia-php/di", - "version": "0.2.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "6a9135908dddef66fd07733705b9625f42d302a4" + "reference": "72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/6a9135908dddef66fd07733705b9625f42d302a4", - "reference": "6a9135908dddef66fd07733705b9625f42d302a4", + "url": "https://api.github.com/repos/utopia-php/di/zipball/72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2", + "reference": "72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2", "shasum": "" }, "require": { @@ -129,10 +129,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/di/tree/0.2.0", + "source": "https://github.com/utopia-php/di/tree/0.3.0", "issues": "https://github.com/utopia-php/di/issues" }, - "time": "2026-03-12T07:54:46+00:00" + "time": "2026-03-12T10:40:23+00:00" }, { "name": "utopia-php/validators", diff --git a/src/Http/Http.php b/src/Http/Http.php index 850d56e2..524bd9d5 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -3,7 +3,6 @@ namespace Utopia\Http; use Utopia\DI\Container; -use Utopia\DI\Dependency; use Utopia\Validator; class Http @@ -400,7 +399,7 @@ protected function getResources(array $list, Container $scope): array */ public function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void { - ($scope ?? $this->container)->set($name, new Dependency($injections, $callback)); + ($scope ?? $this->container)->set($name, $callback, $injections); } /** @@ -619,8 +618,8 @@ public function execute(Route $route, Request $request, string $context, ?Contai $arguments = []; $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); - $requestScope ??= $this->container->scope(); - $executionScope = $requestScope->scope(); + $requestScope ??= new Container($this->container); + $executionScope = new Container($requestScope); try { if ($route->getHook()) { @@ -748,7 +747,7 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr */ public function run(Request $request, Response $response, string $context, array $resources = []): static { - $requestScope = $this->container->scope(); + $requestScope = new Container($this->container); foreach ($resources as $name => $resource) { $this->setResource($name, fn () => $resource, [], $requestScope); } diff --git a/tests/HttpTest.php b/tests/HttpTest.php index e01e1949..a0c87f7c 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -4,7 +4,6 @@ use PHPUnit\Framework\TestCase; use Utopia\DI\Container; -use Utopia\DI\Dependency; use Utopia\Http\Tests\UtopiaFPMRequestTest; use Utopia\Validator\Text; use Utopia\Http\Adapter\FPM\Request; @@ -120,9 +119,9 @@ public function testCanGetEnvironmentVariable(): void public function testCanGetResources(): void { - $this->container->set('rand', new Dependency([], fn () => rand())); - $this->container->set('first', new Dependency(['second'], fn ($second) => "first-{$second}")); - $this->container->set('second', new Dependency([], 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->container->get('second'); $first = $this->container->get('first'); @@ -157,8 +156,8 @@ public function testCanGetResources(): void public function testCanGetDefaultValueWithFunction(): void { - $this->container->set('first', new Dependency(['second'], fn ($second) => "first-{$second}")); - $this->container->set('second', new Dependency([], fn () => 'second')); + $this->container->set('first', fn ($second) => "first-{$second}", ['second']); + $this->container->set('second', fn () => 'second', []); $second = $this->container->get('second'); $first = $this->container->get('first'); @@ -186,7 +185,7 @@ public function testCanGetDefaultValueWithFunction(): void public function testCanExecuteRoute(): void { - $this->container->set('rand', new Dependency([], fn () => rand())); + $this->container->set('rand', fn () => rand(), []); $resource = $this->container->get('rand'); $this->http @@ -613,11 +612,6 @@ public function testScopedResourcesUseNestedContainers(): void { $counter = 0; $http = new class (new Server(), 'Asia/Tel_Aviv') extends Http { - public function createScope(?Container $scope = null): Container - { - return ($scope ?? $this->container)->scope(); - } - public function defineResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void { $this->setResource($name, $callback, $injections, $scope); @@ -635,9 +629,9 @@ public function resource(string $name, Container $scope): mixed return $counter; }); - $requestA = $http->createScope(); - $requestB = $http->createScope(); - $executionA = $http->createScope($requestA); + $requestA = new Container($http->getContainer()); + $requestB = new Container($http->getContainer()); + $executionA = new Container($requestA); $http->defineResource('requestId', fn () => 'request-a', scope: $requestA); $http->defineResource('requestId', fn () => 'request-b', scope: $requestB); @@ -662,7 +656,7 @@ public function testWildcardRoute(): void Http::init() ->action(function () { $route = $this->http->getRoute(); - $this->container->set('myRoute', new Dependency([], fn () => $route)); + $this->container->set('myRoute', fn () => $route, []); }); From ca7fe2576a04472408d2b7f8b4213b4e7d61dd1d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 17:04:17 +0530 Subject: [PATCH 23/29] remove tests --- tests/HttpTest.php | 69 ---------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/tests/HttpTest.php b/tests/HttpTest.php index a0c87f7c..750daf21 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -47,38 +47,6 @@ protected function restoreRequest(): void $_SERVER['REQUEST_URI'] = $this->uri; } - public function testCanInjectContainerInConstructor(): void - { - $container = new Container(); - $http = new Http(new Server(), 'Asia/Tel_Aviv', $container); - - $this->assertSame($container, $http->getContainer()); - $this->assertInstanceOf(Container::class, (new Http(new Server(), 'Asia/Tel_Aviv'))->getContainer()); - } - - public function testCanRegisterResourcesWithPublicApi(): void - { - $this->http->setResource('rand', fn () => 1234); - $this->http->setResource('second', fn () => 'second'); - $this->http->setResource('first', fn ($second) => "first-{$second}", ['second']); - - $route = new Route('GET', '/path'); - - $route - ->inject('rand') - ->inject('first') - ->action(function ($rand, $first) { - echo $rand . '-' . $first; - }); - - \ob_start(); - $this->http->execute($route, new Request(), '1'); - $result = \ob_get_contents(); - \ob_end_clean(); - - $this->assertSame('1234-first-second', $result); - } - public function testCanGetDifferentModes(): void { $this->assertEmpty(Http::getMode()); @@ -608,43 +576,6 @@ public function testCanRunRequest(): void $this->assertStringNotContainsString('HELLO', $result); } - public function testScopedResourcesUseNestedContainers(): void - { - $counter = 0; - $http = new class (new Server(), 'Asia/Tel_Aviv') extends Http { - public function defineResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void - { - $this->setResource($name, $callback, $injections, $scope); - } - - public function resource(string $name, Container $scope): mixed - { - return $this->getResource($name, $scope); - } - }; - - $http->defineResource('shared', function () use (&$counter) { - $counter++; - - return $counter; - }); - - $requestA = new Container($http->getContainer()); - $requestB = new Container($http->getContainer()); - $executionA = new Container($requestA); - - $http->defineResource('requestId', fn () => 'request-a', scope: $requestA); - $http->defineResource('requestId', fn () => 'request-b', scope: $requestB); - $http->defineResource('requestId', fn () => 'execution-a', scope: $executionA); - - $this->assertSame(1, $http->resource('shared', $requestA)); - $this->assertSame(1, $http->resource('shared', $requestB)); - $this->assertSame('request-a', $http->resource('requestId', $requestA)); - $this->assertSame('request-b', $http->resource('requestId', $requestB)); - $this->assertSame('execution-a', $http->resource('requestId', $executionA)); - $this->assertSame('request-a', $http->resource('requestId', $requestA)); - } - public function testWildcardRoute(): void { $method = $_SERVER['REQUEST_METHOD'] ?? null; From c942103ab37e56f3cb6d9a87db6063eab18ef19d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 17:05:56 +0530 Subject: [PATCH 24/29] make public --- composer.json | 6 ------ composer.lock | 34 +++++----------------------------- src/Http/Http.php | 4 ++-- 3 files changed, 7 insertions(+), 37 deletions(-) diff --git a/composer.json b/composer.json index fa9a0426..8b8177b6 100644 --- a/composer.json +++ b/composer.json @@ -10,12 +10,6 @@ ], "license": "MIT", "minimum-stability": "stable", - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/utopia-php/di" - } - ], "autoload": { "psr-4": { "Utopia\\": "src/", diff --git a/composer.lock b/composer.lock index b2d920c3..980cf47a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6bf6c5703f842ea4d1cdee8068e967b4", + "content-hash": "5afd948989df91d546b1694354e2f9d2", "packages": [ { "name": "psr/container", @@ -91,46 +91,22 @@ "Tests\\E2E\\": "tests/e2e" } }, - "scripts": { - "format": [ - "vendor/bin/pint" - ], - "format:check": [ - "vendor/bin/pint --test" - ], - "analyze": [ - "vendor/bin/phpstan analyse --memory-limit=512M" - ], - "refactor": [ - "tools/rector/vendor/bin/rector process --config=rector.php" - ], - "refactor:check": [ - "tools/rector/vendor/bin/rector process --dry-run --config=rector.php" - ], - "fix": [ - "@refactor", - "@analyze", - "@format" - ], - "test": [ - "vendor/bin/phpunit --configuration phpunit.xml" - ] - }, + "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", - "psr-11", "utopia" ], "support": { - "source": "https://github.com/utopia-php/di/tree/0.3.0", - "issues": "https://github.com/utopia-php/di/issues" + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.3.0" }, "time": "2026-03-12T10:40:23+00:00" }, diff --git a/src/Http/Http.php b/src/Http/Http.php index 524bd9d5..eef62e85 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -357,7 +357,7 @@ public static function setAllowOverride(bool $value): void * * @throws Exception */ - protected function getResource(string $name, Container $scope): mixed + public function getResource(string $name, Container $scope): mixed { try { return $scope->get($name); @@ -381,7 +381,7 @@ protected function getResource(string $name, Container $scope): mixed * * @throws Exception */ - protected function getResources(array $list, Container $scope): array + public function getResources(array $list, Container $scope): array { $resources = []; From 1fadb9cc3af7f711c0cc71a2739d333f4529c9c4 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 12 Mar 2026 17:20:47 +0530 Subject: [PATCH 25/29] remove tests --- tests/HttpTest.php | 66 ---------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 750daf21..5c4d2a75 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -85,72 +85,6 @@ public function testCanGetEnvironmentVariable(): void $this->assertEquals(Http::getEnv('unknown', 'test'), 'test'); } - public function testCanGetResources(): void - { - $this->container->set('rand', fn () => rand(), []); - $this->container->set('first', fn ($second) => "first-{$second}", ['second']); - $this->container->set('second', fn () => 'second', []); - - $second = $this->container->get('second'); - $first = $this->container->get('first'); - $this->assertEquals('second', $second); - $this->assertEquals('first-second', $first); - - $resource = $this->container->get('rand'); - - $this->assertNotEmpty($resource); - $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'); - - $route - ->inject('rand') - ->param('x', 'x-def', new Text(200), 'x param', true) - ->param('y', 'y-def', new Text(200), 'y param', true) - ->action(function ($x, $y, $rand) { - echo $x . '-' . $y . '-' . $rand; - }); - - \ob_start(); - $this->http->execute($route, new Request(), '1'); - $result = \ob_get_contents(); - \ob_end_clean(); - - $this->assertEquals('x-def-y-def-' . $resource, $result); - } - - public function testCanGetDefaultValueWithFunction(): void - { - $this->container->set('first', fn ($second) => "first-{$second}", ['second']); - $this->container->set('second', fn () => 'second', []); - - $second = $this->container->get('second'); - $first = $this->container->get('first'); - $this->assertEquals('second', $second); - $this->assertEquals('first-second', $first); - - // Default Value using function - $route = new Route('GET', '/path'); - - $route - ->param('x', function ($first, $second) { - return $first . '-' . $second; - }, new Text(200), 'x param', true, ['first', 'second']) - ->action(function ($x) { - echo $x; - }); - - \ob_start(); - $this->http->execute($route, new Request(), '1'); - $result = \ob_get_contents(); - \ob_end_clean(); - - $this->assertEquals('first-second-second', $result); - } - public function testCanExecuteRoute(): void { $this->container->set('rand', fn () => rand(), []); From 664436bcc95b0bb328b75577ceaf8bb9a1c6794b Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 13 Mar 2026 10:21:55 +0530 Subject: [PATCH 26/29] add scoped request containers --- src/Http/Adapter/FPM/Server.php | 13 +- src/Http/Adapter/Swoole/Server.php | 15 +- src/Http/Http.php | 279 ++++++++++++++++++----------- tests/HttpTest.php | 140 +++++++++++++++ 4 files changed, 337 insertions(+), 110 deletions(-) diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index 4db7c3bc..8c77aa57 100755 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -2,6 +2,7 @@ namespace Utopia\Http\Adapter\FPM; +use Utopia\DI\Container; use Utopia\Http\Adapter; class Server extends Adapter @@ -14,11 +15,17 @@ public function onRequest(callable $callback) { $request = new Request(); $response = new Response(); - - call_user_func($callback, $request, $response, 'fpm', [ + $resources = [ 'fpmRequest' => $request, 'fpmResponse' => $response, - ]); + ]; + $configureRequestScope = function (Container $requestContainer) use ($request, $response) { + $requestContainer + ->set('fpmRequest', fn () => $request, []) + ->set('fpmResponse', fn () => $response, []); + }; + + call_user_func($callback, $request, $response, 'fpm', $resources, $configureRequestScope); } public function onStart(callable $callback) diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index fa475dd6..6589e41c 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -4,6 +4,7 @@ use Swoole\Coroutine; use Utopia\Http\Adapter; +use Utopia\DI\Container; use Swoole\Coroutine\Http\Server as SwooleServer; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; @@ -27,11 +28,19 @@ public function onRequest(callable $callback) { $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { $context = \strval(Coroutine::getCid()); - - call_user_func($callback, new Request($request), new Response($response), $context, [ + $requestAdapter = new Request($request); + $responseAdapter = new Response($response); + $resources = [ 'swooleRequest' => $request, 'swooleResponse' => $response, - ]); + ]; + $configureRequestScope = function (Container $requestContainer) use ($request, $response) { + $requestContainer + ->set('swooleRequest', fn () => $request, []) + ->set('swooleResponse', fn () => $response, []); + }; + + call_user_func($callback, $requestAdapter, $responseAdapter, $context, $resources, $configureRequestScope); }); } diff --git a/src/Http/Http.php b/src/Http/Http.php index eef62e85..db329c08 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,11 +2,14 @@ namespace Utopia\Http; +use Swoole\Coroutine; use Utopia\DI\Container; use Utopia\Validator; class Http { + protected const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + /** * Request method constants */ @@ -40,6 +43,8 @@ class Http protected Container $container; + protected ?Container $requestContainer = null; + /** * Current running mode * @@ -143,6 +148,22 @@ public function getContainer(): Container return $this->container; } + /** + * Get the current request dependency injection container if available. + */ + public function getRequestContainer(): ?Container + { + $context = $this->getCoroutineContext(); + + if (null !== $context) { + $requestContainer = $context[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null; + + return $requestContainer instanceof Container ? $requestContainer : null; + } + + return $this->requestContainer; + } + /** * GET * @@ -551,8 +572,14 @@ public static function onRequest(): Hook public function start() { - $this->server->onRequest(function ($request, $response, $context, array $resources = []) { - $this->run($request, $response, $context, $resources); + $this->server->onRequest(function ($request, $response, $context, array $resources = [], ?callable $configureRequestScope = null) { + $requestScope = new Container($this->container); + + if (\is_callable($configureRequestScope)) { + $configureRequestScope($requestScope); + } + + $this->run($request, $response, $context, $resources, $requestScope); }); $this->server->onStart(function ($server) { $this->setResource('server', function () use ($server) { @@ -619,13 +646,13 @@ public function execute(Route $route, Request $request, string $context, ?Contai $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); $requestScope ??= new Container($this->container); - $executionScope = new Container($requestScope); + $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, $executionScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -634,19 +661,19 @@ public function execute(Route $route, Request $request, string $context, ?Contai foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } - $arguments = $this->getArguments($route, $executionScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($route->getAction(), $arguments); foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -655,19 +682,19 @@ public function execute(Route $route, Request $request, string $context, ?Contai if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $executionScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { - $this->setResource('error', fn () => $e, [], $executionScope); + $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, $executionScope, $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(), 500, $e); @@ -679,13 +706,15 @@ public function execute(Route $route, Request $request, string $context, ?Contai foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $executionScope, $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(), 500, $e); } } } + } finally { + $this->setRequestContainer($previousRequestContainer); } return $this; @@ -745,139 +774,181 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context, array $resources = []): static + public function run(Request $request, Response $response, string $context, array $resources = [], ?Container $requestScope = null): static { - $requestScope = new Container($this->container); - foreach ($resources as $name => $resource) { - $this->setResource($name, fn () => $resource, [], $requestScope); - } - $this->setResource('context', fn () => $context, [], $requestScope); - $this->setResource('request', fn () => $request, [], $requestScope); - $this->setResource('response', fn () => $response, [], $requestScope); + $requestScope ??= new Container($this->container); + $previousRequestContainer = $this->setRequestContainer($requestScope); try { - - foreach (self::$requestHooks as $hook) { - $arguments = $this->getArguments($hook, $requestScope, [], []); - \call_user_func_array($hook->getAction(), $arguments); + foreach ($resources as $name => $resource) { + $this->setResource($name, fn () => $resource, [], $requestScope); } - } catch (\Exception $e) { - $this->setResource('error', fn () => $e, [], $requestScope); + $this->setResource('context', fn () => $context, [], $requestScope); + $this->setResource('request', fn () => $request, [], $requestScope); + $this->setResource('response', fn () => $response, [], $requestScope); - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - try { - $arguments = $this->getArguments($error, $requestScope, [], []); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); + try { + foreach (self::$requestHooks as $hook) { + $arguments = $this->getArguments($hook, $requestScope, [], []); + \call_user_func_array($hook->getAction(), $arguments); + } + } catch (\Exception $e) { + $this->setResource('error', fn () => $e, [], $requestScope); + + foreach (self::$errors as $error) { // Global error hooks + if (in_array('*', $error->getGroups())) { + try { + $arguments = $this->getArguments($error, $requestScope, [], []); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); + } } } } - } - 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; - } - $method = $request->getMethod(); - $route = $this->match($request); - $groups = ($route instanceof Route) ? $route->getGroups() : []; + return $this; + } + $method = $request->getMethod(); + $route = $this->match($request); + $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->setResource('route', fn () => $route, [], $requestScope); + $this->setResource('route', fn () => $route, [], $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, $requestScope, [], $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, $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, $requestScope, [], $request->getParams())); + } } - } - } 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); - \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); + } 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); + \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); + } } } + + return $this; } - return $this; - } + 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); + $this->setResource('route', fn () => $route, [], $requestScope); + } - $this->setResource('route', fn () => $route, [], $requestScope); - } + if (null !== $route) { + return $this->execute($route, $request, $context, $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())); + } + } + } - if (null !== $route) { - return $this->execute($route, $request, $context, $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())) { + 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())); } } - } - - 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); + \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); + } } } - } catch (\Throwable $e) { + } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - $this->setResource('error', function () use ($e) { - return $e; + $this->setResource('error', function () { + return new Exception('Not Found', 404); }, [], $requestScope); \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); } } } - } else { - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - $this->setResource('error', function () { - return new Exception('Not Found', 404); - }, [], $requestScope); - \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); - } + + return $this; + } finally { + $this->setRequestContainer($previousRequestContainer); + } + } + + protected function getCoroutineContext(): ?\ArrayObject + { + if (!\extension_loaded('swoole')) { + return null; + } + + if (Coroutine::getCid() < 0) { + return null; + } + + $context = Coroutine::getContext(); + + return $context instanceof \ArrayObject ? $context : null; + } + + protected function setRequestContainer(?Container $requestContainer): ?Container + { + $context = $this->getCoroutineContext(); + + if (null !== $context) { + $previousRequestContainer = $context[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null; + + if ($requestContainer instanceof Container) { + $context[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + } else { + unset($context[self::REQUEST_CONTAINER_CONTEXT_KEY]); } + + return $previousRequestContainer instanceof Container ? $previousRequestContainer : null; } - return $this; + $previousRequestContainer = $this->requestContainer; + $this->requestContainer = $requestContainer; + + return $previousRequestContainer; } /** diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 5c4d2a75..45f20fa8 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -363,6 +363,88 @@ public function testCanHookThrowExceptions() $this->assertEquals('(init)-y-def-x-def-(shutdown)', $result); } + public function testExecuteStoresErrorOnProvidedRequestScope(): void + { + $requestScope = new Container($this->container); + $previousError = new \Exception('previous error'); + + $this->http->setResource('error', fn () => $previousError, [], $requestScope); + + $this->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error-' . $error->getMessage(); + }); + + $route = new Route('GET', '/path'); + $route + ->param('x', 'x-def', new Text(1, min: 0), 'x param', false) + ->action(function ($x) { + echo $x; + }); + + \ob_start(); + $request = new UtopiaFPMRequestTest(); + $request::_setParams(['x' => 'param-x']); + $this->http->execute($route, $request, '1', $requestScope); + $result = \ob_get_contents(); + \ob_end_clean(); + + $error = $requestScope->get('error'); + + $this->assertEquals('error-Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); + $this->assertNotSame($previousError, $error); + $this->assertEquals('Invalid `x` param: Value must be a valid string and no longer than 1 chars', $error->getMessage()); + } + + public function testCanGetRequestContainerDuringExecute(): void + { + $this->assertNull($this->http->getRequestContainer()); + + $requestScope = new Container($this->container); + $capturedRequestContainer = null; + + $route = new Route('GET', '/path'); + $route->action(function () use (&$capturedRequestContainer, $requestScope) { + $capturedRequestContainer = $this->http->getRequestContainer(); + $this->assertSame($requestScope, $capturedRequestContainer); + }); + + $this->http->execute($route, new Request(), '1', $requestScope); + + $this->assertSame($requestScope, $capturedRequestContainer); + $this->assertNull($this->http->getRequestContainer()); + } + + public function testCanGetRequestContainerFromCoroutineContext(): void + { + if (!\extension_loaded('swoole')) { + $this->markTestSkipped('The swoole extension is required.'); + } + + $this->assertNull($this->http->getRequestContainer()); + + $requestScope = new Container($this->container); + $capturedRequestContainer = null; + $requestContainerAfterExecute = null; + + \Swoole\Coroutine\run(function () use ($requestScope, &$capturedRequestContainer, &$requestContainerAfterExecute) { + $route = new Route('GET', '/path'); + $route->action(function () use (&$capturedRequestContainer, $requestScope) { + $capturedRequestContainer = $this->http->getRequestContainer(); + $this->assertSame($requestScope, $capturedRequestContainer); + }); + + $this->http->execute($route, new Request(), '1', $requestScope); + $requestContainerAfterExecute = $this->http->getRequestContainer(); + }); + + $this->assertSame($requestScope, $capturedRequestContainer); + $this->assertNull($requestContainerAfterExecute); + $this->assertNull($this->http->getRequestContainer()); + } + public function testCanSetRoute() { $route = new Route('GET', '/path'); @@ -510,6 +592,64 @@ public function testCanRunRequest(): void $this->assertStringNotContainsString('HELLO', $result); } + public function testCanGetRequestContainerDuringRun(): void + { + $this->assertNull($this->http->getRequestContainer()); + + $capturedContainers = []; + + $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; + $_SERVER['REQUEST_URI'] = '/request-container'; + + Http::onRequest() + ->action(function () use (&$capturedContainers) { + $capturedContainers[] = $this->http->getRequestContainer(); + }); + + Http::get('/request-container') + ->inject('response') + ->action(function ($response) use (&$capturedContainers) { + $capturedContainers[] = $this->http->getRequestContainer(); + $response->send('OK'); + }); + + \ob_start(); + $this->http->run(new Request(), new Response(), '1'); + \ob_end_clean(); + + $this->assertCount(2, $capturedContainers); + $this->assertInstanceOf(Container::class, $capturedContainers[0]); + $this->assertSame($capturedContainers[0], $capturedContainers[1]); + $this->assertNull($this->http->getRequestContainer()); + } + + public function testCanGetFpmResourcesFromRequestContainerDuringStart(): void + { + $capturedResources = []; + + Http::get('/fpm-request-container') + ->inject('response') + ->action(function ($response) use (&$capturedResources) { + $requestContainer = $this->http->getRequestContainer(); + + $capturedResources['fpmRequest'] = $requestContainer?->get('fpmRequest'); + $capturedResources['fpmResponse'] = $requestContainer?->get('fpmResponse'); + + $response->send('OK'); + }); + + $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; + $_SERVER['REQUEST_URI'] = '/fpm-request-container'; + + \ob_start(); + $this->http->start(); + \ob_end_clean(); + + $this->assertInstanceOf(Request::class, $capturedResources['fpmRequest']); + $this->assertInstanceOf(Response::class, $capturedResources['fpmResponse']); + $this->assertNull($this->http->getRequestContainer()); + } + public function testWildcardRoute(): void { $method = $_SERVER['REQUEST_METHOD'] ?? null; From b872c6b88bc6fdce5eea1908a6d9eecb21d10ec5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 13 Mar 2026 11:21:10 +0530 Subject: [PATCH 27/29] cleanup --- README.md | 2 +- composer.lock | 26 +++++++++++++------------- src/Http/Adapter/FPM/Server.php | 4 ++-- src/Http/Adapter/Swoole/Server.php | 4 ++-- tests/HttpTest.php | 4 ++-- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5f432d74..f55045f4 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ Define a dependency on the DI container: ```php $container->set('bootTime', function () { return \microtime(true); -}, []); +}); ``` Inject resource into endpoint action: diff --git a/composer.lock b/composer.lock index 980cf47a..c854c2ab 100644 --- a/composer.lock +++ b/composer.lock @@ -61,16 +61,16 @@ }, { "name": "utopia-php/di", - "version": "0.3.0", + "version": "0.3.1", "source": { "type": "git", "url": "https://github.com/utopia-php/di.git", - "reference": "72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2" + "reference": "68873b7267842315d01d82a83b988bae525eab31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/di/zipball/72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2", - "reference": "72e1a3e67c58f8f03cf3faa5d79ee814136d3fe2", + "url": "https://api.github.com/repos/utopia-php/di/zipball/68873b7267842315d01d82a83b988bae525eab31", + "reference": "68873b7267842315d01d82a83b988bae525eab31", "shasum": "" }, "require": { @@ -106,9 +106,9 @@ ], "support": { "issues": "https://github.com/utopia-php/di/issues", - "source": "https://github.com/utopia-php/di/tree/0.3.0" + "source": "https://github.com/utopia-php/di/tree/0.3.1" }, - "time": "2026-03-12T10:40:23+00:00" + "time": "2026-03-13T05:47:23+00:00" }, { "name": "utopia-php/validators", @@ -383,16 +383,16 @@ }, { "name": "laravel/pint", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -409,8 +409,8 @@ "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.5", - "shipfastlabs/agent-detector": "^1.0.2" + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -447,7 +447,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-03-10T20:37:18+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "myclabs/deep-copy", diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index 8c77aa57..a6913ec9 100755 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -21,8 +21,8 @@ public function onRequest(callable $callback) ]; $configureRequestScope = function (Container $requestContainer) use ($request, $response) { $requestContainer - ->set('fpmRequest', fn () => $request, []) - ->set('fpmResponse', fn () => $response, []); + ->set('fpmRequest', fn () => $request) + ->set('fpmResponse', fn () => $response); }; call_user_func($callback, $request, $response, 'fpm', $resources, $configureRequestScope); diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index 6589e41c..0ef61139 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -36,8 +36,8 @@ public function onRequest(callable $callback) ]; $configureRequestScope = function (Container $requestContainer) use ($request, $response) { $requestContainer - ->set('swooleRequest', fn () => $request, []) - ->set('swooleResponse', fn () => $response, []); + ->set('swooleRequest', fn () => $request) + ->set('swooleResponse', fn () => $response); }; call_user_func($callback, $requestAdapter, $responseAdapter, $context, $resources, $configureRequestScope); diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 45f20fa8..d2221326 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -87,7 +87,7 @@ public function testCanGetEnvironmentVariable(): void public function testCanExecuteRoute(): void { - $this->container->set('rand', fn () => rand(), []); + $this->container->set('rand', fn () => rand()); $resource = $this->container->get('rand'); $this->http @@ -661,7 +661,7 @@ public function testWildcardRoute(): void Http::init() ->action(function () { $route = $this->http->getRoute(); - $this->container->set('myRoute', fn () => $route, []); + $this->container->set('myRoute', fn () => $route); }); From bd9526b85d8dd14979e1eb1b1e702db63a902966 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:22:07 +0000 Subject: [PATCH 28/29] feat: request scopes --- src/Http/Adapter.php | 3 + src/Http/Adapter/FPM/Server.php | 16 +- src/Http/Adapter/Swoole/Server.php | 33 +-- src/Http/Http.php | 328 +++++++++++------------------ 4 files changed, 148 insertions(+), 232 deletions(-) diff --git a/src/Http/Adapter.php b/src/Http/Adapter.php index 082ca6ab..f72d6bfb 100755 --- a/src/Http/Adapter.php +++ b/src/Http/Adapter.php @@ -2,9 +2,12 @@ namespace Utopia\Http; +use Utopia\DI\Container; + abstract class Adapter { abstract public function onStart(callable $callback); abstract public function onRequest(callable $callback); abstract public function start(); + abstract public function getContainer(): Container; } diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index a6913ec9..75842551 100755 --- a/src/Http/Adapter/FPM/Server.php +++ b/src/Http/Adapter/FPM/Server.php @@ -7,7 +7,7 @@ class Server extends Adapter { - public function __construct() + public function __construct(private Container $container) { } @@ -19,18 +19,18 @@ public function onRequest(callable $callback) 'fpmRequest' => $request, 'fpmResponse' => $response, ]; - $configureRequestScope = function (Container $requestContainer) use ($request, $response) { - $requestContainer - ->set('fpmRequest', fn () => $request) - ->set('fpmResponse', fn () => $response); - }; - call_user_func($callback, $request, $response, 'fpm', $resources, $configureRequestScope); + \call_user_func($callback, $request, $response, 'fpm', $resources); } public function onStart(callable $callback) { - call_user_func($callback, $this); + \call_user_func($callback, $this); + } + + public function getContainer(): Container + { + return $this->container; } public function start() diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index 0ef61139..d05c46c6 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -14,39 +14,40 @@ class Server extends Adapter { protected SwooleServer $server; + protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + protected Container $container; - public function __construct(string $host, ?string $port = null, array $settings = []) + public function __construct(string $host, ?string $port = null, array $settings = [], Container $container) { $this->server = new SwooleServer($host, $port); $this->server->set(\array_merge($settings, [ 'enable_coroutine' => true, 'http_parse_cookie' => false, ])); + $this->container = $container; } public function onRequest(callable $callback) { $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { - $context = \strval(Coroutine::getCid()); - $requestAdapter = new Request($request); - $responseAdapter = new Response($response); - $resources = [ - 'swooleRequest' => $request, - 'swooleResponse' => $response, - ]; - $configureRequestScope = function (Container $requestContainer) use ($request, $response) { - $requestContainer - ->set('swooleRequest', fn () => $request) - ->set('swooleResponse', fn () => $response); - }; - - call_user_func($callback, $requestAdapter, $responseAdapter, $context, $resources, $configureRequestScope); + Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = new Container($this->container); + + $utopiaRequest = new Request($request); + $utopiaResponse = new Response($response); + + \call_user_func($callback, $utopiaRequest, $utopiaResponse); }); } + public function getContainer(): Container + { + return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + } + public function onStart(callable $callback) { - call_user_func($callback, $this); + + \call_user_func($callback, $this); } public function start() diff --git a/src/Http/Http.php b/src/Http/Http.php index db329c08..01172bb7 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -8,8 +8,6 @@ class Http { - protected const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; - /** * Request method constants */ @@ -130,38 +128,11 @@ class Http * @param Adapter $server * @param string $timezone */ - public function __construct(Adapter $server, string $timezone, ?Container $container = null) + public function __construct(Adapter $server, string $timezone) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; - $this->container = $container ?? new Container(); - } - - /** - * 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 - { - $context = $this->getCoroutineContext(); - - if (null !== $context) { - $requestContainer = $context[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null; - - return $requestContainer instanceof Container ? $requestContainer : null; - } - - return $this->requestContainer; } /** @@ -378,10 +349,10 @@ public static function setAllowOverride(bool $value): void * * @throws Exception */ - public function getResource(string $name, Container $scope): mixed + public function getResource(string $name): mixed { try { - return $scope->get($name); + return $this->server->getContainer()->get($name); } catch (\Throwable $e) { // Normalize DI container errors to the Http layer's "resource" terminology. $message = \str_replace('dependency', 'resource', $e->getMessage()); @@ -402,12 +373,12 @@ public function getResource(string $name, Container $scope): mixed * * @throws Exception */ - public function getResources(array $list, Container $scope): array + public function getResources(array $list): array { $resources = []; foreach ($list as $name) { - $resources[$name] = $this->getResource($name, $scope); + $resources[$name] = $this->getResource($name); } return $resources; @@ -418,9 +389,9 @@ public function getResources(array $list, Container $scope): array * * @param string[] $injections */ - public function setResource(string $name, callable $callback, array $injections = [], ?Container $scope = null): void + public function setResource(string $name, callable $callback, array $injections = []): void { - ($scope ?? $this->container)->set($name, $callback, $injections); + $this->server->getContainer()->set($name, $callback, $injections); } /** @@ -572,15 +543,11 @@ public static function onRequest(): Hook public function start() { - $this->server->onRequest(function ($request, $response, $context, array $resources = [], ?callable $configureRequestScope = null) { - $requestScope = new Container($this->container); - if (\is_callable($configureRequestScope)) { - $configureRequestScope($requestScope); - } + $this->server->onRequest( + fn (Request $request, Response $response) => $this->run($request, $response) + ); - $this->run($request, $response, $context, $resources, $requestScope); - }); $this->server->onStart(function ($server) { $this->setResource('server', function () use ($server) { return $server; @@ -588,7 +555,7 @@ public function start() try { foreach (self::$startHooks as $hook) { - $arguments = $this->getArguments($hook, $this->container, [], []); + $arguments = $this->getArguments($hook, [], []); \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { @@ -597,7 +564,7 @@ public function start() foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $this->container, [], []); + $arguments = $this->getArguments($error, [], []); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -640,19 +607,17 @@ public function match(Request $request, bool $fresh = true): ?Route * @param Route $route * @param Request $request */ - public function execute(Route $route, Request $request, string $context, ?Container $requestScope = null): static + public function execute(Route $route, Request $request): static { $arguments = []; $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); - $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, $requestScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -660,20 +625,20 @@ public function execute(Route $route, Request $request, string $context, ?Contai foreach ($groups as $group) { foreach (self::$init as $hook) { // Group init hooks - if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); + if (\in_array($group, $hook->getGroups())) { + $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } - $arguments = $this->getArguments($route, $requestScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($route, $pathValues, $request->getParams()); \call_user_func_array($route->getAction(), $arguments); foreach ($groups as $group) { foreach (self::$shutdown as $hook) { // Group shutdown hooks - if (in_array($group, $hook->getGroups())) { - $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); + if (\in_array($group, $hook->getGroups())) { + $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } @@ -681,20 +646,20 @@ public function execute(Route $route, Request $request, string $context, ?Contai if ($route->getHook()) { foreach (self::$shutdown as $hook) { // Group shutdown hooks - if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $requestScope, $pathValues, $request->getParams()); + if (\in_array('*', $hook->getGroups())) { + $arguments = $this->getArguments($hook, $pathValues, $request->getParams()); \call_user_func_array($hook->getAction(), $arguments); } } } } catch (\Throwable $e) { - $this->setResource('error', fn () => $e, [], $requestScope); + $this->setResource('error', fn () => $e, []); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks - if (in_array($group, $error->getGroups())) { + if (\in_array($group, $error->getGroups())) { try { - $arguments = $this->getArguments($error, $requestScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); @@ -704,17 +669,15 @@ public function execute(Route $route, Request $request, string $context, ?Contai } foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { + if (\in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, $requestScope, $pathValues, $request->getParams()); + $arguments = $this->getArguments($error, $pathValues, $request->getParams()); \call_user_func_array($error->getAction(), $arguments); } catch (\Throwable $e) { throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); } } } - } finally { - $this->setRequestContainer($previousRequestContainer); } return $this; @@ -730,7 +693,7 @@ public function execute(Route $route, Request $request, string $context, ?Contai * * @throws Exception */ - protected function getArguments(Hook $hook, Container $scope, array $values, array $requestParams): array + protected function getArguments(Hook $hook, array $values, array $requestParams): array { $arguments = []; foreach ($hook->getParams() as $key => $param) { // Get value from route or request object @@ -740,7 +703,7 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr $arg = $existsInRequest ? $requestParams[$key] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, $this->getResources($param['injections'], $scope)); + $arg = \call_user_func_array($arg, $this->getResources($param['injections'])); } $value = $existsInValues ? $values[$key] : $arg; @@ -750,7 +713,7 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr } if ($paramExists) { - $this->validate($key, $param, $value, $scope); + $this->validate($key, $param, $value); } } @@ -759,7 +722,7 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr } foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->getResource($injection['name'], $scope); + $arguments[$injection['order']] = $this->getResource($injection['name']); } return $arguments; @@ -774,183 +737,132 @@ protected function getArguments(Hook $hook, Container $scope, array $values, arr * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response, string $context, array $resources = [], ?Container $requestScope = null): static + public function run(Request $request, Response $response): static { - $requestScope ??= new Container($this->container); - $previousRequestContainer = $this->setRequestContainer($requestScope); - try { - foreach ($resources as $name => $resource) { - $this->setResource($name, fn () => $resource, [], $requestScope); + foreach (self::$requestHooks as $hook) { + $arguments = $this->getArguments($hook, [], []); + \call_user_func_array($hook->getAction(), $arguments); } - $this->setResource('context', fn () => $context, [], $requestScope); - $this->setResource('request', fn () => $request, [], $requestScope); - $this->setResource('response', fn () => $response, [], $requestScope); + } catch (\Exception $e) { + $this->setResource('error', fn () => $e, []); - try { - foreach (self::$requestHooks as $hook) { - $arguments = $this->getArguments($hook, $requestScope, [], []); - \call_user_func_array($hook->getAction(), $arguments); - } - } catch (\Exception $e) { - $this->setResource('error', fn () => $e, [], $requestScope); - - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - try { - $arguments = $this->getArguments($error, $requestScope, [], []); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); - } + foreach (self::$errors as $error) { // Global error hooks + if (\in_array('*', $error->getGroups())) { + try { + $arguments = $this->getArguments($error, [], []); + \call_user_func_array($error->getAction(), $arguments); + } catch (\Throwable $e) { + throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); } } } + } - 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; - } - $method = $request->getMethod(); - $route = $this->match($request); - $groups = ($route instanceof Route) ? $route->getGroups() : []; + return $this; + } - $this->setResource('route', fn () => $route, [], $requestScope); + $method = $request->getMethod(); + $route = $this->match($request); + $groups = ($route instanceof Route) ? $route->getGroups() : []; - if (self::REQUEST_METHOD_HEAD == $method) { - $method = self::REQUEST_METHOD_GET; - $response->disablePayload(); - } + $this->setResource('route', fn () => $route, []); - 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())); - } - } - } + if (self::REQUEST_METHOD_HEAD == $method) { + $method = self::REQUEST_METHOD_GET; + $response->disablePayload(); + } - foreach (self::$options as $option) { // Global options hooks + 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('*', $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())) { - $this->setResource('error', function () use ($e) { - return $e; - }, [], $requestScope); - \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); + if (\in_array($group, $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, [], $request->getParams())); } } } - return $this; - } - - if (null === $route && null !== self::$wildcardRoute) { - $route = self::$wildcardRoute; - $this->route = $route; - $path = \parse_url($request->getURI(), PHP_URL_PATH); - $route->path($path); - - $this->setResource('route', fn () => $route, [], $requestScope); - } - - if (null !== $route) { - return $this->execute($route, $request, $context, $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, $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); - \call_user_func_array($error->getAction(), $this->getArguments($error, $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())); } } - } else { + } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - $this->setResource('error', function () { - return new Exception('Not Found', 404); - }, [], $requestScope); - \call_user_func_array($error->getAction(), $this->getArguments($error, $requestScope, [], $request->getParams())); + /** @var Hook $error */ + if (\in_array('*', $error->getGroups())) { + $this->setResource('error', function () use ($e) { + return $e; + }, []); + \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } } return $this; - } finally { - $this->setRequestContainer($previousRequestContainer); } - } - protected function getCoroutineContext(): ?\ArrayObject - { - if (!\extension_loaded('swoole')) { - return null; - } + if (null === $route && null !== self::$wildcardRoute) { + $route = self::$wildcardRoute; + $this->route = $route; + $path = \parse_url($request->getURI(), PHP_URL_PATH); + $route->path($path); - if (Coroutine::getCid() < 0) { - return null; + $this->setResource('route', fn () => $route, []); } - $context = Coroutine::getContext(); - - return $context instanceof \ArrayObject ? $context : null; - } - - protected function setRequestContainer(?Container $requestContainer): ?Container - { - $context = $this->getCoroutineContext(); - - if (null !== $context) { - $previousRequestContainer = $context[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? null; + if (null !== $route) { + return $this->execute($route, $request); + } 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 ($requestContainer instanceof Container) { - $context[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; - } else { - unset($context[self::REQUEST_CONTAINER_CONTEXT_KEY]); + foreach (self::$options as $option) { // Global options hooks + if (\in_array('*', $option->getGroups())) { + \call_user_func_array($option->getAction(), $this->getArguments($option, [], $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; + }, []); + \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); + } + } + } + } else { + foreach (self::$errors as $error) { // Global error hooks + if (\in_array('*', $error->getGroups())) { + $this->setResource('error', fn () => new Exception('Not Found', 404), []); + \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); + } } - - return $previousRequestContainer instanceof Container ? $previousRequestContainer : null; } - $previousRequestContainer = $this->requestContainer; - $this->requestContainer = $requestContainer; - - return $previousRequestContainer; + return $this; } + /** * Validate Param * @@ -963,7 +875,7 @@ protected function setRequestContainer(?Container $requestContainer): ?Container * * @throws Exception */ - protected function validate(string $key, array $param, mixed $value, Container $scope): void + protected function validate(string $key, array $param, mixed $value): void { if ($param['optional'] && \is_null($value)) { return; @@ -972,7 +884,7 @@ protected function validate(string $key, array $param, mixed $value, Container $ $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'], $scope)); + $validator = \call_user_func_array($validator, $this->getResources($param['injections'])); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class From bcb4562d28a70301a2518f332f7e38cffdc1d3b1 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 13 Mar 2026 16:13:46 +0530 Subject: [PATCH 29/29] fixes --- src/Http/Adapter/Swoole/Server.php | 6 +- src/Http/Http.php | 1 - tests/HttpTest.php | 140 ----------------------------- 3 files changed, 3 insertions(+), 144 deletions(-) diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index d05c46c6..cdad6d2a 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -14,17 +14,17 @@ class Server extends Adapter { protected SwooleServer $server; - protected const string REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; + protected const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; protected Container $container; - public function __construct(string $host, ?string $port = null, array $settings = [], Container $container) + public function __construct(string $host, ?string $port = null, array $settings = [], ?Container $container = null) { $this->server = new SwooleServer($host, $port); $this->server->set(\array_merge($settings, [ 'enable_coroutine' => true, 'http_parse_cookie' => false, ])); - $this->container = $container; + $this->container = $container ?? new Container(); } public function onRequest(callable $callback) diff --git a/src/Http/Http.php b/src/Http/Http.php index 01172bb7..ee91be61 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,7 +2,6 @@ namespace Utopia\Http; -use Swoole\Coroutine; use Utopia\DI\Container; use Utopia\Validator; diff --git a/tests/HttpTest.php b/tests/HttpTest.php index d2221326..16d67909 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -363,88 +363,6 @@ public function testCanHookThrowExceptions() $this->assertEquals('(init)-y-def-x-def-(shutdown)', $result); } - public function testExecuteStoresErrorOnProvidedRequestScope(): void - { - $requestScope = new Container($this->container); - $previousError = new \Exception('previous error'); - - $this->http->setResource('error', fn () => $previousError, [], $requestScope); - - $this->http - ->error() - ->inject('error') - ->action(function ($error) { - echo 'error-' . $error->getMessage(); - }); - - $route = new Route('GET', '/path'); - $route - ->param('x', 'x-def', new Text(1, min: 0), 'x param', false) - ->action(function ($x) { - echo $x; - }); - - \ob_start(); - $request = new UtopiaFPMRequestTest(); - $request::_setParams(['x' => 'param-x']); - $this->http->execute($route, $request, '1', $requestScope); - $result = \ob_get_contents(); - \ob_end_clean(); - - $error = $requestScope->get('error'); - - $this->assertEquals('error-Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); - $this->assertNotSame($previousError, $error); - $this->assertEquals('Invalid `x` param: Value must be a valid string and no longer than 1 chars', $error->getMessage()); - } - - public function testCanGetRequestContainerDuringExecute(): void - { - $this->assertNull($this->http->getRequestContainer()); - - $requestScope = new Container($this->container); - $capturedRequestContainer = null; - - $route = new Route('GET', '/path'); - $route->action(function () use (&$capturedRequestContainer, $requestScope) { - $capturedRequestContainer = $this->http->getRequestContainer(); - $this->assertSame($requestScope, $capturedRequestContainer); - }); - - $this->http->execute($route, new Request(), '1', $requestScope); - - $this->assertSame($requestScope, $capturedRequestContainer); - $this->assertNull($this->http->getRequestContainer()); - } - - public function testCanGetRequestContainerFromCoroutineContext(): void - { - if (!\extension_loaded('swoole')) { - $this->markTestSkipped('The swoole extension is required.'); - } - - $this->assertNull($this->http->getRequestContainer()); - - $requestScope = new Container($this->container); - $capturedRequestContainer = null; - $requestContainerAfterExecute = null; - - \Swoole\Coroutine\run(function () use ($requestScope, &$capturedRequestContainer, &$requestContainerAfterExecute) { - $route = new Route('GET', '/path'); - $route->action(function () use (&$capturedRequestContainer, $requestScope) { - $capturedRequestContainer = $this->http->getRequestContainer(); - $this->assertSame($requestScope, $capturedRequestContainer); - }); - - $this->http->execute($route, new Request(), '1', $requestScope); - $requestContainerAfterExecute = $this->http->getRequestContainer(); - }); - - $this->assertSame($requestScope, $capturedRequestContainer); - $this->assertNull($requestContainerAfterExecute); - $this->assertNull($this->http->getRequestContainer()); - } - public function testCanSetRoute() { $route = new Route('GET', '/path'); @@ -592,64 +510,6 @@ public function testCanRunRequest(): void $this->assertStringNotContainsString('HELLO', $result); } - public function testCanGetRequestContainerDuringRun(): void - { - $this->assertNull($this->http->getRequestContainer()); - - $capturedContainers = []; - - $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; - $_SERVER['REQUEST_URI'] = '/request-container'; - - Http::onRequest() - ->action(function () use (&$capturedContainers) { - $capturedContainers[] = $this->http->getRequestContainer(); - }); - - Http::get('/request-container') - ->inject('response') - ->action(function ($response) use (&$capturedContainers) { - $capturedContainers[] = $this->http->getRequestContainer(); - $response->send('OK'); - }); - - \ob_start(); - $this->http->run(new Request(), new Response(), '1'); - \ob_end_clean(); - - $this->assertCount(2, $capturedContainers); - $this->assertInstanceOf(Container::class, $capturedContainers[0]); - $this->assertSame($capturedContainers[0], $capturedContainers[1]); - $this->assertNull($this->http->getRequestContainer()); - } - - public function testCanGetFpmResourcesFromRequestContainerDuringStart(): void - { - $capturedResources = []; - - Http::get('/fpm-request-container') - ->inject('response') - ->action(function ($response) use (&$capturedResources) { - $requestContainer = $this->http->getRequestContainer(); - - $capturedResources['fpmRequest'] = $requestContainer?->get('fpmRequest'); - $capturedResources['fpmResponse'] = $requestContainer?->get('fpmResponse'); - - $response->send('OK'); - }); - - $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; - $_SERVER['REQUEST_URI'] = '/fpm-request-container'; - - \ob_start(); - $this->http->start(); - \ob_end_clean(); - - $this->assertInstanceOf(Request::class, $capturedResources['fpmRequest']); - $this->assertInstanceOf(Response::class, $capturedResources['fpmResponse']); - $this->assertNull($this->http->getRequestContainer()); - } - public function testWildcardRoute(): void { $method = $_SERVER['REQUEST_METHOD'] ?? null;