diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e17e5507..a6f1e642 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: run: composer validate --strict - name: Setup Docker - run: docker-compose up -d --build + run: docker compose up -d --build - name: Wait for Server to be ready run: sleep 10 @@ -28,4 +28,7 @@ jobs: run: docker compose exec fpm vendor/bin/phpunit --configuration phpunit.xml - name: Run Swoole Tests - run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file + run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml + + - name: Run Swoole Corotuine Tests + run: docker compose exec swoole-coroutine vendor/bin/phpunit --configuration phpunit.xml diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..044ea2ae --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,9 @@ +{ + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003, + "pathMappings": { + "/usr/src/code": "${workspaceRoot}" + } + } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ab8893b..361c6a7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,9 +66,9 @@ $ git push origin [name_of_your_new_branch] ### Testing -- `docker-compose up -d` -- `docker-compose exec web vendor/bin/phpunit --configuration phpunit.xml` -- `docker-compose exec web vendor/bin/psalm --show-info=true` +- `docker compose up -d` +- `docker compose exec web vendor/bin/phpunit --configuration phpunit.xml` +- `docker compose exec web vendor/bin/psalm --show-info=true` ## Introducing New Features diff --git a/Dockerfile.swoole b/Dockerfile.swoole index 6e0fdba1..a930c97a 100644 --- a/Dockerfile.swoole +++ b/Dockerfile.swoole @@ -1,9 +1,10 @@ FROM composer:2.0 AS step0 - ARG TESTING=true +ARG DEBUG=false ENV TESTING=$TESTING +ENV DEBUG=$DEBUG WORKDIR /usr/local/src/ @@ -13,17 +14,35 @@ 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.9.0 as final + +ARG TESTING=true +ARG DEBUG=false + +ENV TESTING=$TESTING +ENV DEBUG=$DEBUG + LABEL maintainer="team@appwrite.io" +RUN \ + if [ "$DEBUG" == "true" ]; then \ + apk add boost boost-dev; \ + fi + WORKDIR /usr/src/code +COPY ./dev /usr/src/code/dev COPY ./src /usr/src/code/src COPY ./tests /usr/src/code/tests COPY ./phpunit.xml /usr/src/code/phpunit.xml COPY ./phpbench.json /usr/src/code/phpbench.json COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor +# Enable Extensions +RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi +RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi +RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi + EXPOSE 80 CMD ["php", "tests/e2e/server-swoole.php"] diff --git a/Dockerfile.swoole_coroutines b/Dockerfile.swoole_coroutines new file mode 100644 index 00000000..a02852da --- /dev/null +++ b/Dockerfile.swoole_coroutines @@ -0,0 +1,48 @@ +FROM composer:2.0 AS step0 + +ARG TESTING=true +ARG DEBUG=false + +ENV TESTING=$TESTING +ENV DEBUG=$DEBUG + +WORKDIR /usr/local/src/ + +COPY composer.* /usr/local/src/ + +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.9.0 as final + +ARG TESTING=true +ARG DEBUG=false + +ENV TESTING=$TESTING +ENV DEBUG=$DEBUG + +LABEL maintainer="team@appwrite.io" + +RUN \ + if [ "$DEBUG" == "true" ]; then \ + apk add boost boost-dev; \ + fi + +WORKDIR /usr/src/code + +COPY ./dev /usr/src/code/dev +COPY ./src /usr/src/code/src +COPY ./tests /usr/src/code/tests +COPY ./phpunit.xml /usr/src/code/phpunit.xml +COPY ./phpbench.json /usr/src/code/phpbench.json +COPY --from=step0 /usr/local/src/vendor /usr/src/code/vendor + +# Enable Extensions +RUN if [ "$DEBUG" == "true" ]; then cp /usr/src/code/dev/xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini; fi +RUN if [ "$DEBUG" = "false" ]; then rm -rf /usr/src/code/dev; fi +RUN if [ "$DEBUG" = "false" ]; then rm -f /usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so; fi + +EXPOSE 80 + +CMD ["php", "tests/e2e/server-swoole-coroutine.php"] diff --git a/composer.json b/composer.json index b5c168bc..1d316c1c 100644 --- a/composer.json +++ b/composer.json @@ -12,22 +12,28 @@ "minimum-stability": "stable", "autoload": { "psr-4": { - "Utopia\\": "src/", + "Utopia\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { "Tests\\E2E\\": "tests/e2e" } }, "scripts": { "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", - "check": "vendor/bin/phpstan analyse -c phpstan.neon", + "check": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M", "test": "vendor/bin/phpunit --configuration phpunit.xml", "bench": "vendor/bin/phpbench run --report=benchmark" }, "require": { "php": ">=8.0", - "ext-swoole": "*" + "ext-swoole": "*", + "utopia-php/servers": "0.1.* " }, "require-dev": { + "ext-xdebug": "*", "phpunit/phpunit": "^9.5.25", "laravel/pint": "^1.2", "swoole/ide-helper": "4.8.3", diff --git a/composer.lock b/composer.lock index 67cde3f0..fd7e5633 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,110 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "33b8cf270cfbd8f86cbd1338d81f5140", - "packages": [], + "content-hash": "cd780eab9403d28fc6a81d6e2063ae4c", + "packages": [ + { + "name": "utopia-php/di", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/di.git", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/di/zipball/22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "reference": "22490c95f7ac3898ed1c33f1b1b5dd577305ee31", + "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" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple and lite library for managing dependency injections", + "keywords": [ + "framework", + "http", + "php", + "upf" + ], + "support": { + "issues": "https://github.com/utopia-php/di/issues", + "source": "https://github.com/utopia-php/di/tree/0.1.0" + }, + "time": "2024-08-08T14:35:19+00:00" + }, + { + "name": "utopia-php/servers", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/servers.git", + "reference": "7d9e4f364fb1ab1889fb89ca96eb9946467cb09c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/7d9e4f364fb1ab1889fb89ca96eb9946467cb09c", + "reference": "7d9e4f364fb1ab1889fb89ca96eb9946467cb09c", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "utopia-php/di": "0.1.*" + }, + "require-dev": { + "laravel/pint": "^0.2.3", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Servers\\": "src/Servers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "description": "A base library for building Utopia style servers.", + "keywords": [ + "framework", + "php", + "servers", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/servers/issues", + "source": "https://github.com/utopia-php/servers/tree/0.1.0" + }, + "time": "2024-08-08T14:31:39+00:00" + } + ], "packages-dev": [ { "name": "doctrine/annotations", @@ -232,16 +334,16 @@ }, { "name": "laravel/pint", - "version": "v1.16.1", + "version": "v1.17.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "9266a47f1b9231b83e0cfd849009547329d871b1" + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/9266a47f1b9231b83e0cfd849009547329d871b1", - "reference": "9266a47f1b9231b83e0cfd849009547329d871b1", + "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", "shasum": "" }, "require": { @@ -252,13 +354,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.59.3", - "illuminate/view": "^10.48.12", - "larastan/larastan": "^2.9.7", + "friendsofphp/php-cs-fixer": "^3.61.1", + "illuminate/view": "^10.48.18", + "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.8" + "pestphp/pest": "^2.35.0" }, "bin": [ "builds/pint" @@ -294,7 +396,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-06-18T16:50:05+00:00" + "time": "2024-08-06T15:11:54+00:00" }, { "name": "myclabs/deep-copy", @@ -735,16 +837,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.6", + "version": "1.11.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6ac78f1165346c83b4a753f7e4186d969c6ad0ee" + "reference": "640410b32995914bde3eed26fa89552f9c2c082f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6ac78f1165346c83b4a753f7e4186d969c6ad0ee", - "reference": "6ac78f1165346c83b4a753f7e4186d969c6ad0ee", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f", + "reference": "640410b32995914bde3eed26fa89552f9c2c082f", "shasum": "" }, "require": { @@ -789,7 +891,7 @@ "type": "github" } ], - "time": "2024-07-01T15:33:06+00:00" + "time": "2024-08-08T09:02:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1112,45 +1214,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.19", + "version": "9.6.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + "reference": "49d7820565836236411f5dc002d16dd689cde42f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", + "reference": "49d7820565836236411f5dc002d16dd689cde42f", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -1195,7 +1297,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.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20" }, "funding": [ { @@ -1211,7 +1313,7 @@ "type": "tidelift" } ], - "time": "2024-04-05T04:35:58+00:00" + "time": "2024-07-10T11:45:39+00:00" }, { "name": "psr/cache", @@ -2330,23 +2432,23 @@ }, { "name": "seld/jsonlint", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", "shasum": "" }, "require": { "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.5", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, "bin": [ @@ -2378,7 +2480,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" }, "funding": [ { @@ -2390,7 +2492,7 @@ "type": "tidelift" } ], - "time": "2024-02-07T12:57:50+00:00" + "time": "2024-07-11T14:55:45+00:00" }, { "name": "swoole/ide-helper", @@ -2436,16 +2538,16 @@ }, { "name": "symfony/console", - "version": "v7.1.2", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0aa29ca177f432ab68533432db0de059f39c92ae" + "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae", - "reference": "0aa29ca177f432ab68533432db0de059f39c92ae", + "url": "https://api.github.com/repos/symfony/console/zipball/cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", + "reference": "cb1dcb30ebc7005c29864ee78adb47b5fb7c3cd9", "shasum": "" }, "require": { @@ -2509,7 +2611,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.1.2" + "source": "https://github.com/symfony/console/tree/v7.1.3" }, "funding": [ { @@ -2525,7 +2627,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T10:03:55+00:00" + "time": "2024-07-26T12:41:01+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2662,16 +2764,16 @@ }, { "name": "symfony/finder", - "version": "v7.1.1", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6" + "reference": "717c6329886f32dc65e27461f80f2a465412fdca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6", - "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6", + "url": "https://api.github.com/repos/symfony/finder/zipball/717c6329886f32dc65e27461f80f2a465412fdca", + "reference": "717c6329886f32dc65e27461f80f2a465412fdca", "shasum": "" }, "require": { @@ -2706,7 +2808,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.1.1" + "source": "https://github.com/symfony/finder/tree/v7.1.3" }, "funding": [ { @@ -2722,7 +2824,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-07-24T07:08:44+00:00" }, { "name": "symfony/options-resolver", @@ -3111,16 +3213,16 @@ }, { "name": "symfony/process", - "version": "v7.1.1", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", - "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", + "url": "https://api.github.com/repos/symfony/process/zipball/7f2f542c668ad6c313dc4a5e9c3321f733197eca", + "reference": "7f2f542c668ad6c313dc4a5e9c3321f733197eca", "shasum": "" }, "require": { @@ -3152,7 +3254,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.1" + "source": "https://github.com/symfony/process/tree/v7.1.3" }, "funding": [ { @@ -3168,7 +3270,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:57:53+00:00" + "time": "2024-07-26T12:44:47+00:00" }, { "name": "symfony/service-contracts", @@ -3255,16 +3357,16 @@ }, { "name": "symfony/string", - "version": "v7.1.2", + "version": "v7.1.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" + "reference": "ea272a882be7f20cad58d5d78c215001617b7f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", - "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", + "url": "https://api.github.com/repos/symfony/string/zipball/ea272a882be7f20cad58d5d78c215001617b7f07", + "reference": "ea272a882be7f20cad58d5d78c215001617b7f07", "shasum": "" }, "require": { @@ -3322,7 +3424,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.2" + "source": "https://github.com/symfony/string/tree/v7.1.3" }, "funding": [ { @@ -3338,7 +3440,7 @@ "type": "tidelift" } ], - "time": "2024-06-28T09:27:18+00:00" + "time": "2024-07-22T10:25:37+00:00" }, { "name": "theseer/tokenizer", @@ -3449,6 +3551,8 @@ "php": ">=8.0", "ext-swoole": "*" }, - "platform-dev": [], + "platform-dev": { + "ext-xdebug": "*" + }, "plugin-api-version": "2.6.0" } diff --git a/dev/xdebug.ini b/dev/xdebug.ini new file mode 100644 index 00000000..30835305 --- /dev/null +++ b/dev/xdebug.ini @@ -0,0 +1,7 @@ +zend_extension=xdebug + +[xdebug] +xdebug.mode=develop,debug,profile +xdebug.client_host=host.docker.internal +xdebug.start_with_request=yes +xdebug.output_dir=/tmp/xdebug \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0c4e0011..bd2952c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,10 @@ -version: '3' - services: fpm: build: context: . dockerfile: Dockerfile.fpm ports: - - "9020:80" + - "9400:80" volumes: - ./src:/usr/share/nginx/html/src - ./tests:/usr/share/nginx/html/tests @@ -18,12 +16,42 @@ services: build: context: . dockerfile: Dockerfile.swoole - ports: - - "9501:80" + ports: + - "9401:80" volumes: + - ./dev:/usr/src/code/dev:rw - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests + - ./tmp/xdebug:/tmp/xdebug networks: - testing + swoole-coroutine: + build: + context: . + dockerfile: Dockerfile.swoole_coroutines + ports: + - "9402:80" + volumes: + - ./dev:/usr/src/code/dev:rw + - ./src:/usr/src/code/src + - ./tests:/usr/src/code/tests + - ./tmp/xdebug:/tmp/xdebug + networks: + - testing + + mariadb: + image: mariadb:10.11 # fix issues when upgrading using: mysql_upgrade -u root -p + container_name: mariadb + networks: + - testing + ports: + - "3307:3306" + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_DATABASE=test + - MYSQL_USER=user + - MYSQL_PASSWORD=password + command: "mysqld --innodb-flush-method=fsync --max-connections=10000" + networks: - testing: \ No newline at end of file + testing: diff --git a/phpunit.xml b/phpunit.xml index de6deb0b..c87db0dc 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,6 +11,8 @@ ./tests/e2e/Client.php + ./tests/MockRequest.php + ./tests/MockResponse.php ./tests/ diff --git a/src/Http/Adapter/FPM/Request.php b/src/Http/Adapter/FPM/Request.php index ecabb457..bdc8ebf5 100644 --- a/src/Http/Adapter/FPM/Request.php +++ b/src/Http/Adapter/FPM/Request.php @@ -165,6 +165,33 @@ public function setURI(string $uri): static return $this; } + /** + * Get Query String + * + * Return HTTP request query string + * + * @return string + */ + public function getQueryString(): string + { + return $this->getServer('QUERY_STRING') ?? ''; + } + + /** + * Set Query String + * + * Set HTTP request query string + * + * @param string $value + * @return static + */ + public function setQueryString(string $value): static + { + $this->setServer('QUERY_STRING', $value); + + return $this; + } + /** * Get files * diff --git a/src/Http/Adapter/FPM/Response.php b/src/Http/Adapter/FPM/Response.php index e58cc3d4..41a81436 100644 --- a/src/Http/Adapter/FPM/Response.php +++ b/src/Http/Adapter/FPM/Response.php @@ -28,9 +28,9 @@ public function write(string $content): bool * @param string $content * @return void */ - public function end(string $content = null): void + public function end(string $content = ''): void { - if (!is_null($content)) { + if (!empty($content)) { echo $content; } } @@ -40,9 +40,10 @@ public function end(string $content = null): void * Send Status Code * * @param int $statusCode + * @param string $reason * @return void */ - protected function sendStatus(int $statusCode): void + protected function sendStatus(int $statusCode, string $reason): void { http_response_code($statusCode); } diff --git a/src/Http/Adapter/FPM/Server.php b/src/Http/Adapter/FPM/Server.php index e90f0cd7..46e54124 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,9 +15,6 @@ public function onRequest(callable $callback) $request = new Request(); $response = new Response(); - Http::setResource('fpmRequest', fn () => $request); - Http::setResource('fpmResponse', fn () => $response); - call_user_func($callback, $request, $response, 'fpm'); } diff --git a/src/Http/Adapter/Swoole/Request.php b/src/Http/Adapter/Swoole/Request.php index 8a717730..d93310d2 100644 --- a/src/Http/Adapter/Swoole/Request.php +++ b/src/Http/Adapter/Swoole/Request.php @@ -12,7 +12,7 @@ class Request extends UtopiaRequest * * @var SwooleRequest */ - protected SwooleRequest $swoole; + public SwooleRequest $swoole; /** * Request constructor. @@ -179,6 +179,33 @@ public function setURI(string $uri): static return $this; } + /** + * Get Query String + * + * Return HTTP request query string + * + * @return string + */ + public function getQueryString(): string + { + return $this->getServer('query_string') ?? ''; + } + + /** + * Set Query String + * + * Set HTTP request query string + * + * @param string $value + * @return static + */ + public function setQueryString(string $value): static + { + $this->setServer('query_string', $value); + + return $this; + } + /** * Get Referer * @@ -301,11 +328,6 @@ public function removeHeader(string $key): static return $this; } - public function getSwooleRequest(): SwooleRequest - { - return $this->swoole; - } - /** * Generate input * diff --git a/src/Http/Adapter/Swoole/Response.php b/src/Http/Adapter/Swoole/Response.php index 4bb03926..515fd103 100644 --- a/src/Http/Adapter/Swoole/Response.php +++ b/src/Http/Adapter/Swoole/Response.php @@ -12,7 +12,7 @@ class Response extends UtopiaResponse * * @var SwooleResponse */ - protected $swoole; + public $swoole; /** * Response constructor. @@ -23,11 +23,6 @@ public function __construct(SwooleResponse $response) parent::__construct(\microtime(true)); } - public function getSwooleResponse(): SwooleResponse - { - return $this->swoole; - } - /** * Write * @@ -42,10 +37,10 @@ public function write(string $content): bool /** * End * - * @param string|null $content + * @param string $content * @return void */ - public function end(string $content = null): void + public function end(string $content = ''): void { $this->swoole->end($content); } @@ -54,11 +49,12 @@ public function end(string $content = null): void * Send Status Code * * @param int $statusCode + * @param string $reason * @return void */ - protected function sendStatus(int $statusCode): void + protected function sendStatus(int $statusCode, string $reason = ''): void { - $this->swoole->status((string) $statusCode); + $this->swoole->status((string) $statusCode, $reason); } /** diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index 9fe73e98..0fc63b47 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -2,14 +2,9 @@ namespace Utopia\Http\Adapter\Swoole; -use Swoole\Coroutine; use Utopia\Http\Adapter; -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; +use Swoole\Http\Server as SwooleServer; +use Swoole\Runtime; class Server extends Adapter { @@ -19,33 +14,32 @@ public function __construct(string $host, string $port = null, array $settings = { $this->server = new SwooleServer($host, $port); $this->server->set(\array_merge($settings, [ - 'enable_coroutine' => true + 'open_http2_protocol' => true, + 'dispatch_mode' => 2, ])); } 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); + $this->server->on('request', function ($request, $response) use ($callback) { + go(function () use ($request, $response, $callback) { + call_user_func($callback, new Request($request), new Response($response)); + }); }); } public function onStart(callable $callback) { - call_user_func($callback, $this); + $this->server->on('start', function () use ($callback) { + go(function () use ($callback) { + call_user_func($callback); + }); + }); } public function start() { - if(Coroutine::getCid() === -1) { - run(fn () => $this->server->start()); - } else { - $this->server->start(); - } + Runtime::enableCoroutine(); + return $this->server->start(); } } diff --git a/src/Http/Adapter/SwooleCoroutine/Request.php b/src/Http/Adapter/SwooleCoroutine/Request.php new file mode 100644 index 00000000..7b63ab69 --- /dev/null +++ b/src/Http/Adapter/SwooleCoroutine/Request.php @@ -0,0 +1,9 @@ +server = new SwooleServer($host, $port, false, true); + $this->server->set(\array_merge($settings, [ + 'enable_coroutine' => true + ])); + } + + public function onRequest(callable $callback) + { + $this->server->handle('/', function ($request, $response) use ($callback) { + go(function () use ($request, $response, $callback) { + call_user_func($callback, new Request($request), new Response($response)); + }); + }); + } + + public function onStart(callable $callback) + { + call_user_func($callback, $this); + } + + public function start() + { + go(function () { + $this->server->start(); + }); + } +} diff --git a/src/Http/Exception.php b/src/Http/Exception.php index 46e55958..4de36cc3 100644 --- a/src/Http/Exception.php +++ b/src/Http/Exception.php @@ -2,6 +2,8 @@ namespace Utopia\Http; -class Exception extends \Exception +use Utopia\Servers\Exception as ServersException; + +class Exception extends ServersException { } diff --git a/src/Http/Hook.php b/src/Http/Hook.php index 2266da26..e83a4f88 100644 --- a/src/Http/Hook.php +++ b/src/Http/Hook.php @@ -2,279 +2,8 @@ namespace Utopia\Http; -class Hook -{ - /** - * Description - * - * @var string - */ - protected string $desc = ''; - - /** - * Parameters - * - * List of route params names and validators - * - * @var array - */ - protected array $params = []; - - /** - * Group - * - * @var array - */ - protected array $groups = []; - - /** - * Labels - * - * List of route label names - * - * @var array - */ - protected array $labels = []; - - /** - * Action Callback - * - * @var callable - */ - protected $action; - - /** - * Injections - * - * List of route required injections for action callback - * - * @var array - */ - protected array $injections = []; - - public function __construct() - { - $this->action = function (): void { - }; - } - - /** - * Add Description - * - * @param string $desc - * @return static - */ - public function desc(string $desc): static - { - $this->desc = $desc; - - return $this; - } - - /** - * Get Description - * - * @return string - */ - public function getDesc(): string - { - return $this->desc; - } - - /** - * Add Group - * - * @param array $groups - * @return static - */ - public function groups(array $groups): static - { - $this->groups = $groups; - - return $this; - } - - /** - * Get Groups - * - * @return array - */ - public function getGroups(): array - { - return $this->groups; - } - - /** - * Add Label - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function label(string $key, mixed $value): static - { - $this->labels[$key] = $value; - - return $this; - } - - /** - * Get Label - * - * Return given label value or default value if label doesn't exists - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getLabel(string $key, mixed $default): mixed - { - return (isset($this->labels[$key])) ? $this->labels[$key] : $default; - } - - /** - * Add Action - * - * @param callable $action - * @return static - */ - public function action(callable $action): static - { - $this->action = $action; +use Utopia\Servers\Hook as ServersHook; - return $this; - } - - /** - * Get Action - * - * @return callable - */ - public function getAction() - { - return $this->action; - } - - /** - * Get Injections - * - * @return array - */ - public function getInjections(): array - { - return $this->injections; - } - - /** - * Inject - * - * @param string $injection - * @return static - * - * @throws Exception - */ - public function inject(string $injection): static - { - if (array_key_exists($injection, $this->injections)) { - throw new Exception('Injection already declared for '.$injection); - } - - $this->injections[$injection] = [ - 'name' => $injection, - 'order' => count($this->params) + count($this->injections), - ]; - - return $this; - } - - /** - * Add Param - * - * @param string $key - * @param mixed $default - * @param Validator|callable $validator - * @param string $description - * @param bool $optional - * @param array $injections - * @param bool $skipValidation - * @return static - */ - public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = [], bool $skipValidation = false): static - { - $this->params[$key] = [ - 'default' => $default, - 'validator' => $validator, - 'description' => $description, - 'optional' => $optional, - 'injections' => $injections, - 'skipValidation' => $skipValidation, - 'value' => null, - 'order' => count($this->params) + count($this->injections), - ]; - - return $this; - } - - /** - * Get Params - * - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * Get Param Values - * - * @return array - */ - public function getParamsValues(): array - { - $values = []; - - foreach ($this->params as $key => $param) { - $values[$key] = $param['value']; - } - - return $values; - } - - /** - * Set Param Value - * - * @param string $key - * @param mixed $value - * @return static - * - * @throws Exception - */ - public function setParamValue(string $key, mixed $value): static - { - if (!isset($this->params[$key])) { - throw new Exception('Unknown key'); - } - - $this->params[$key]['value'] = $value; - - return $this; - } - - /** - * Get Param Value - * - * @param string $key - * @return mixed - * - * @throws Exception - */ - public function getParamValue(string $key): mixed - { - if (!isset($this->params[$key])) { - throw new Exception('Unknown key'); - } - - return $this->params[$key]['value']; - } +class Hook extends ServersHook +{ } diff --git a/src/Http/Http.php b/src/Http/Http.php index 37638edb..05b61aaa 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,85 +2,28 @@ namespace Utopia\Http; -class Http +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Servers\Base; + +class Http extends Base { /** * Request method constants */ public const REQUEST_METHOD_GET = 'GET'; - public const REQUEST_METHOD_POST = 'POST'; - public const REQUEST_METHOD_PUT = 'PUT'; - public const REQUEST_METHOD_PATCH = 'PATCH'; - public const REQUEST_METHOD_DELETE = 'DELETE'; - public const REQUEST_METHOD_OPTIONS = 'OPTIONS'; - public const REQUEST_METHOD_HEAD = 'HEAD'; - /** - * Mode Type - */ - public const MODE_TYPE_DEVELOPMENT = 'development'; - - public const MODE_TYPE_STAGE = 'stage'; - - public const MODE_TYPE_PRODUCTION = 'production'; - - /** - * @var array - */ - protected array $resources = [ - 'error' => null, - ]; - /** * @var Files */ protected Files $files; - /** - * @var array - */ - protected static array $resourcesCallbacks = []; - - /** - * Current running mode - * - * @var string - */ - protected static string $mode = ''; - - /** - * Errors - * - * Errors callbacks - * - * @var Hook[] - */ - protected static array $errors = []; - - /** - * Init - * - * A callback function that is initialized on application start - * - * @var Hook[] - */ - protected static array $init = []; - - /** - * Shutdown - * - * A callback function that is initialized on application end - * - * @var Hook[] - */ - protected static array $shutdown = []; - /** * Options * @@ -90,29 +33,6 @@ class Http */ protected static array $options = []; - /** - * Server Start hooks - * - * @var Hook[] - */ - protected static array $startHooks = []; - - /** - * Request hooks - * - * @var Hook[] - */ - protected static array $requestHooks = []; - - /** - * Route - * - * Memory cached result for chosen route - * - * @var Route|null - */ - protected ?Route $route = null; - /** * Wildcard route * If set, this get's executed if no other route is matched @@ -126,17 +46,37 @@ class Http */ protected Adapter $server; + protected string|null $requestClass = null; + protected string|null $responseClass = null; + /** * Http * * @param Adapter $server - * @param string $timezone + * @param string $timezone */ - public function __construct(Adapter $server, string $timezone) + public function __construct(Adapter $server, Container $container, string $timezone) { \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; + $this->container = $container; + } + + /** + * Set Request Class + */ + public function setResponseClass(string $responseClass) + { + $this->responseClass = $responseClass; + } + + /** + * Set Request Class + */ + public function setRequestClass(string $requestClass) + { + $this->requestClass = $requestClass; } /** @@ -144,7 +84,7 @@ public function __construct(Adapter $server, string $timezone) * * Add GET request route * - * @param string $url + * @param string $url * @return Route */ public static function get(string $url): Route @@ -157,7 +97,7 @@ public static function get(string $url): Route * * Add POST request route * - * @param string $url + * @param string $url * @return Route */ public static function post(string $url): Route @@ -170,7 +110,7 @@ public static function post(string $url): Route * * Add PUT request route * - * @param string $url + * @param string $url * @return Route */ public static function put(string $url): Route @@ -183,7 +123,7 @@ public static function put(string $url): Route * * Add PATCH request route * - * @param string $url + * @param string $url * @return Route */ public static function patch(string $url): Route @@ -196,7 +136,7 @@ public static function patch(string $url): Route * * Add DELETE request route * - * @param string $url + * @param string $url * @return Route */ public static function delete(string $url): Route @@ -213,43 +153,11 @@ public static function delete(string $url): Route */ public static function wildcard(): Route { - self::$wildcardRoute = new Route('', ''); - - return self::$wildcardRoute; - } - - /** - * Init - * - * Set a callback function that will be initialized on application start - * - * @return Hook - */ - public static function init(): Hook - { - $hook = new Hook(); - $hook->groups(['*']); + $route = new Route('', ''); - self::$init[] = $hook; + self::$wildcardRoute = $route; - return $hook; - } - - /** - * Shutdown - * - * Set a callback function that will be initialized on application end - * - * @return Hook - */ - public static function shutdown(): Hook - { - $hook = new Hook(); - $hook->groups(['*']); - - self::$shutdown[] = $hook; - - return $hook; + return $route; } /** @@ -269,37 +177,6 @@ public static function options(): Hook return $hook; } - /** - * Error - * - * An error callback for failed or no matched requests - * - * @return Hook - */ - public static function error(): Hook - { - $hook = new Hook(); - $hook->groups(['*']); - - self::$errors[] = $hook; - - return $hook; - } - - /** - * Get env var - * - * Method for querying env varialbles. If $key is not found $default value will be returned. - * - * @param string $key - * @param string|null $default - * @return string|null - */ - public static function getEnv(string $key, string $default = null): ?string - { - return $_SERVER[$key] ?? $default; - } - /** * Get Mode * @@ -317,7 +194,7 @@ public static function getMode(): string * * Set current mode * - * @param string $value + * @param string $value * @return void */ public static function setMode(string $value): void @@ -325,134 +202,6 @@ public static function setMode(string $value): void self::$mode = $value; } - /** - * Get allow override - * - * - * @return bool - */ - public static function getAllowOverride(): bool - { - return Router::getAllowOverride(); - } - - /** - * Set Allow override - * - * - * @param bool $value - * @return void - */ - 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; - } - - $this->resources[$context] ??= []; - - $resourcesCallback = &self::$resourcesCallbacks[$context] ?? []; - if(empty($resourcesCallback) || !\array_key_exists($name, $resourcesCallback)) { - $resourcesCallback = &self::$resourcesCallbacks['utopia']; - } - - 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 . '"'); - } - - $this->resources[$context][$name] = \call_user_func_array( - $resourcesCallback[$name]['callback'], - $this->getResources($resourcesCallback[$name]['injections'], $context) - ); - } - - $resourcesCallback[$name]['reset'][$context] = false; - return $this->resources[$context][$name]; - } - - /** - * 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::$resourcesCallbacks[$context] ??= []; - - self::$resourcesCallbacks[$context][$name] = ['callback' => $callback, 'injections' => $injections, 'resets' => []]; - } - - /** - * Is http in production mode? - * - * @return bool - */ - public static function isProduction(): bool - { - return self::MODE_TYPE_PRODUCTION === self::$mode; - } - - /** - * Is http in development mode? - * - * @return bool - */ - public static function isDevelopment(): bool - { - return self::MODE_TYPE_DEVELOPMENT === self::$mode; - } - - /** - * Is http in stage mode? - * - * @return bool - */ - public static function isStage(): bool - { - return self::MODE_TYPE_STAGE === self::$mode; - } - /** * Get Routes * @@ -466,25 +215,26 @@ public static function getRoutes(): array } /** - * Get the current route + * Get allow override * - * @return null|Route + * + * @return bool */ - public function getRoute(): ?Route + public static function getAllowOverride(): bool { - return $this->route ?? null; + return Router::getAllowOverride(); } /** - * Set the current route + * Set Allow override * - * @param Route $route + * + * @param bool $value + * @return void */ - public function setRoute(Route $route): self + public static function setAllowOverride(bool $value): void { - $this->route = $route; - - return $this; + Router::setAllowOverride($value); } /** @@ -492,8 +242,8 @@ public function setRoute(Route $route): self * * Add routing route method, path and callback * - * @param string $method - * @param string $url + * @param string $method + * @param string $url * @return Route */ public static function addRoute(string $method, string $url): Route @@ -508,12 +258,12 @@ public static function addRoute(string $method, string $url): Route /** * Load directory. * - * @param string $directory - * @param string|null $root + * @param string $directory + * @param string|null $root * @return void * * @throws \Exception - */ + */ public function loadFiles(string $directory, string $root = null): void { $this->files->load($directory, $root); @@ -522,7 +272,7 @@ public function loadFiles(string $directory, string $root = null): void /** * Is file loaded. * - * @param string $uri + * @param string $uri * @return bool */ protected function isFileLoaded(string $uri): bool @@ -533,7 +283,7 @@ protected function isFileLoaded(string $uri): bool /** * Get file contents. * - * @param string $uri + * @param string $uri * @return string * * @throws \Exception @@ -546,7 +296,7 @@ protected function getFileContents(string $uri): mixed /** * Get file MIME type. * - * @param string $uri + * @param string $uri * @return string * * @throws \Exception @@ -556,53 +306,63 @@ protected function getFileMimeType(string $uri): mixed return $this->files->getFileMimeType($uri); } - public static function onStart(): Hook + public function start() { - $hook = new Hook(); - self::$startHooks[] = $hook; - return $hook; - } + $this->server->onRequest(function ($request, $response) { + $dependency = new Dependency(); - public static function onRequest(): Hook - { - $hook = new Hook(); - self::$requestHooks[] = $hook; - return $hook; - } + if (!\is_null($this->requestClass)) { + $request = new $this->requestClass($request); + } - public function start() - { - $this->server->onRequest(function ($request, $response, $context) { - try { - $this->run($request, $response, $context); - } finally { - if(isset(self::$resourcesCallbacks[$context])) { - unset(self::$resourcesCallbacks[$context]); - } + if (!\is_null($this->responseClass)) { + $response = new $this->responseClass($response); } + + $context = clone $this->container; + + $context->set(clone $dependency->setName('request')->setCallback(fn () => $request)) + ->set(clone $dependency->setName('response')->setCallback(fn () => $response)); + + // More base injection for GraphQL only + if($request->getUri() === '/v1/graphql') { + $context->set(clone $dependency->setName('http')->setCallback(fn () => $this)) + ->set(clone $dependency->setName('context')->setCallback(fn () => $context)); + } + + + $this->run($context); }); - $this->server->onStart(function ($server) { - $this->resources['utopia'] ??= []; - $this->resources['utopia']['server'] = $server; - self::setResource('server', function () use ($server) { - return $server; - }); - try { - foreach (self::$startHooks as $hook) { - $arguments = $this->getArguments($hook, 'utopia', [], []); - \call_user_func_array($hook->getAction(), $arguments); + $this->server->onStart(function () { + $container = clone $this->container; + + $dependency = new Dependency(); + $container + ->set( + $dependency + ->setName('server') + ->setCallback(fn () => $this->server) + ); + + try { + foreach (self::$start as $hook) { + $this->prepare($container, $hook, [], [])->inject($hook, true); } - } catch(\Exception $e) { - self::setResource('error', fn () => $e); + } catch (\Exception $e) { + $dependency = new Dependency(); + $container->set( + $dependency + ->setName('error') + ->setCallback(fn () => $e) + ); foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { try { - $arguments = $this->getArguments($error, 'utopia', [], []); - \call_user_func_array($error->getAction(), $arguments); + $this->prepare($container, $error, [], [])->inject($error, true); } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); + throw new Exception('Error handler had an error: ' . $e->getMessage() . ' on: ' . $e->getFile() . ':' . $e->getLine(), 500, $e); } } } @@ -617,34 +377,32 @@ public function start() * * Find matching route given current user request * - * @param Request $request - * @param bool $fresh If true, will not match any cached route + * @param Request $request * @return null|Route */ - public function match(Request $request, bool $fresh = true): ?Route + public function match(Request $request): ?Route { - if (null !== $this->route && !$fresh) { - return $this->route; - } - $url = \parse_url($request->getURI(), PHP_URL_PATH); $method = $request->getMethod(); $method = (self::REQUEST_METHOD_HEAD == $method) ? self::REQUEST_METHOD_GET : $method; - $this->route = Router::match($method, $url); + return Router::match($method, $url); + } - return $this->route; + + public function execute(Route $route, Request $request, Container $context): self + { + return $this->lifecycle($route, $request, $context); } /** * Execute a given route with middlewares and error handling * - * @param Route $route - * @param Request $request + * @param Route $route + * @param Request $request */ - public function execute(Route $route, Request $request, string $context): static + protected function lifecycle(Route $route, Request $request, Container $context): static { - $arguments = []; $groups = $route->getGroups(); $pathValues = $route->getPathValues($request); @@ -652,8 +410,7 @@ public function execute(Route $route, Request $request, string $context): static if ($route->getHook()) { foreach (self::$init as $hook) { // Global init hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); - \call_user_func_array($hook->getAction(), $arguments); + $this->prepare($context, $hook, $pathValues, $request->getParams())->inject($hook, true); } } } @@ -661,43 +418,43 @@ 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()); - \call_user_func_array($hook->getAction(), $arguments); + $this->prepare($context, $hook, $pathValues, $request->getParams())->inject($hook, true); } } } - $arguments = $this->getArguments($route, $context, $pathValues, $request->getParams()); - \call_user_func_array($route->getAction(), $arguments); + $this->prepare($context, $route, $pathValues, $request->getParams())->inject($route, true); 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()); - \call_user_func_array($hook->getAction(), $arguments); + $this->prepare($context, $hook, $pathValues, $request->getParams())->inject($hook, true); } } } if ($route->getHook()) { - foreach (self::$shutdown as $hook) { // Group shutdown hooks + foreach (self::$shutdown as $hook) { // Global shutdown hooks if (in_array('*', $hook->getGroups())) { - $arguments = $this->getArguments($hook, $context, $pathValues, $request->getParams()); - \call_user_func_array($hook->getAction(), $arguments); + $this->prepare($context, $hook, $pathValues, $request->getParams())->inject($hook, true); } } } } catch (\Throwable $e) { - self::setResource('error', fn () => $e, [], $context); + $dependency = new Dependency(); + $context->set( + $dependency + ->setName('error') + ->setCallback(fn () => $e) + ); 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()); - \call_user_func_array($error->getAction(), $arguments); + $this->prepare($context, $error, $pathValues, $request->getParams())->inject($error, true); } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); + throw new Exception('Group error handler had an error: ' . $e->getMessage() . ' on: ' . $e->getFile() . ':' . $e->getLine(), 500, $e); } } } @@ -706,107 +463,33 @@ 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()); - \call_user_func_array($error->getAction(), $arguments); + $this->prepare($context, $error, $pathValues, $request->getParams())->inject($error, true); } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); + throw new Exception('Global error handler had an error: ' . $e->getMessage() . ' on: ' . $e->getFile() . ':' . $e->getLine(), 500, $e); } } } } - // Reset resources for the context - $this->resources[$context] = []; + unset($context); return $this; } - /** - * Get Arguments - * - * @param Hook $hook - * @param array $values - * @param array $requestParams - * @return array - * - * @throws Exception - */ - protected function getArguments(Hook $hook, string $context, array $values, array $requestParams): array - { - $arguments = []; - foreach ($hook->getParams() as $key => $param) { // Get value from route or request object - $existsInRequest = \array_key_exists($key, $requestParams); - $existsInValues = \array_key_exists($key, $values); - $paramExists = $existsInRequest || $existsInValues; - - $arg = $existsInRequest ? $requestParams[$key] : $param['default']; - if (\is_callable($arg)) { - $arg = \call_user_func_array($arg, $this->getResources($param['injections'])); - } - $value = $existsInValues ? $values[$key] : $arg; - - if (!$param['skipValidation']) { - if (!$paramExists && !$param['optional']) { - throw new Exception('Param "' . $key . '" is not optional.', 400); - } - - if ($paramExists) { - $this->validate($key, $param, $value, $context); - } - } - - $hook->setParamValue($key, $value); - $arguments[$param['order']] = $value; - } - - foreach ($hook->getInjections() as $key => $injection) { - $arguments[$injection['order']] = $this->getResource($injection['name'], $context); - } - - return $arguments; - } - /** * Run * * This is the place to initialize any pre routing logic. * This is where you might want to parse the application current URL by any desired logic * - * @param Request $request - * @param Response $response; + * @param Container $context */ - public function run(Request $request, Response $response, string $context): static + public function run(Container $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); - - self::setResource('response', fn () => $response, [], $context); - - try { - - foreach (self::$requestHooks as $hook) { - $arguments = $this->getArguments($hook, $context, [], []); - \call_user_func_array($hook->getAction(), $arguments); - } - } catch(\Exception $e) { - self::setResource('error', fn () => $e, [], $context); - - foreach (self::$errors as $error) { // Global error hooks - if (in_array('*', $error->getGroups())) { - try { - $arguments = $this->getArguments($error, $context, [], []); - \call_user_func_array($error->getAction(), $arguments); - } catch (\Throwable $e) { - throw new Exception('Error handler had an error: ' . $e->getMessage(), 500, $e); - } - } - } - } + $request = $context->get('request'); + /** @var Request $request */ + $response = $context->get('response'); + /** @var Response $response */ if ($this->isFileLoaded($request->getURI())) { $time = (60 * 60 * 24 * 365 * 2); // 45 days cache @@ -819,11 +502,23 @@ public function run(Request $request, Response $response, string $context): stat return $this; } + $method = $request->getMethod(); $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - self::setResource('route', fn () => $route, [], $context); + if (null === $route && null !== self::$wildcardRoute) { + $route = self::$wildcardRoute; + $path = \parse_url($request->getURI(), PHP_URL_PATH); + $route->path($path); + } + + $dependency = new Dependency(); + $context->set( + $dependency + ->setName('route') + ->setCallback(fn () => $route ?? new Route($request->getMethod(), $request->getURI())) + ); if (self::REQUEST_METHOD_HEAD == $method) { $method = self::REQUEST_METHOD_GET; @@ -836,7 +531,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())); + $this->prepare($context, $option, [], $request->getParams())->inject($option, true); } } } @@ -844,17 +539,21 @@ 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())); + $this->prepare($context, $option, [], $request->getParams())->inject($option, true); } } } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks /** @var Hook $error */ if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + $dependency = new Dependency(); + $context->set( + $dependency + ->setName('error') + ->setCallback(fn () => $e) + ); + + $this->prepare($context, $error, [], $request->getParams())->inject($error, true); } } } @@ -862,49 +561,48 @@ public function run(Request $request, Response $response, string $context): stat 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); - - self::setResource('route', fn () => $route, [], $context); - } - if (null !== $route) { - return $this->execute($route, $request, $context); + return $this->lifecycle($route, $request, $context); } 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())); + $this->prepare($context, $option, [], $request->getParams())->inject($option, true); } } } 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())); + $this->prepare($context, $option, [], $request->getParams())->inject($option, true); } } } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () use ($e) { - return $e; - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + $dependency = new Dependency(); + $context->set( + $dependency + ->setName('error') + ->setCallback(fn () => $e) + ); + + $this->prepare($context, $error, [], $request->getParams())->inject($error, true); } } } } else { foreach (self::$errors as $error) { // Global error hooks if (in_array('*', $error->getGroups())) { - self::setResource('error', function () { - return new Exception('Not Found', 404); - }, [], $context); - \call_user_func_array($error->getAction(), $this->getArguments($error, $context, [], $request->getParams())); + $dependency = new Dependency(); + $dependency + ->setName('error') + ->setCallback(fn () => new Exception('Not Found', 404)); + + $context->set($dependency); + + $this->prepare($context, $error, [], $request->getParams())->inject($error, true); } } } @@ -912,39 +610,6 @@ public function run(Request $request, Response $response, string $context): stat return $this; } - /** - * Validate Param - * - * Creates an validator instance and validate given value with given rules. - * - * @param string $key - * @param array $param - * @param mixed $value - * @return void - * - * @throws Exception - */ - protected function validate(string $key, array $param, mixed $value, $context): void - { - if ($param['optional'] && \is_null($value)) { - return; - } - - $validator = $param['validator']; // checking whether the class exists - - if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'], $context)); - } - - if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class - throw new Exception('Validator object is not an instance of the Validator class', 500); - } - - if (!$validator->isValid($value)) { - throw new Exception('Invalid `' . $key . '` param: ' . $validator->getDescription(), 400); - } - } - /** * Reset all the static variables * @@ -953,12 +618,7 @@ protected function validate(string $key, array $param, mixed $value, $context): public static function reset(): void { Router::reset(); - self::$resourcesCallbacks = []; - self::$mode = ''; - self::$errors = []; - self::$init = []; - self::$shutdown = []; self::$options = []; - self::$startHooks = []; + parent::reset(); } } diff --git a/src/Http/Request.php b/src/Http/Request.php index 65f67b28..e8894e04 100755 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -90,6 +90,19 @@ public function getQuery(string $key, mixed $default = null): mixed return $this->queryString[$key] ?? $default; } + /** + * Set query string parameters + * + * @param array $params + * @return static + */ + public function setQuery(array $params): static + { + $this->queryString = $params; + + return $this; + } + /** * Get payload * @@ -203,10 +216,7 @@ abstract public function setMethod(string $method): static; * * @return string */ - public function getURI(): string - { - return $this->getServer('REQUEST_URI') ?? ''; - } + abstract public function getURI(): string; /** * Get Path @@ -218,6 +228,25 @@ public function getURI(): string */ abstract public function setURI(string $uri): static; + /** + * Get query string + * + * Method for querying HTTP GET request query string + * + * @return string + */ + abstract public function getQueryString(): string; + + /** + * Set query string + * + * Method for setting HTTP GET request query string + * + * @param string $value + * @return static + */ + abstract public function setQueryString(string $value): static; + /** * Get files * @@ -454,18 +483,6 @@ public function getRangeUnit(): ?string return null; } - /** - * Set query string parameters - * - * @param array $params - * @return static - */ - public function setQueryString(array $params): static - { - $this->queryString = $params; - - return $this; - } /** * Set payload parameters diff --git a/src/Http/Response.php b/src/Http/Response.php index 8d085d63..66a0eeaa 100755 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -475,7 +475,10 @@ public function send(string $body = ''): void $this->sent = true; - $this->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime)); + $this + ->addHeader('Server', array_key_exists('Server', $this->headers) ? $this->headers['Server'] : 'Utopia/Http') + ->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime)) + ; $this ->appendCookies() @@ -523,7 +526,7 @@ abstract public function write(string $content): bool; * @param string $content * @return void */ - abstract public function end(string $content = null): void; + abstract public function end(string $content = ''): void; /** * Output response @@ -571,7 +574,7 @@ public function chunk(string $body = '', bool $end = false): void protected function appendHeaders(): static { // Send status code header - $this->sendStatus($this->statusCode); + $this->sendStatus($this->statusCode, $this->statusCodes[$this->statusCode] ?? 'Unknown HTTP status code'); // Send content type header if (!empty($this->contentType)) { @@ -590,9 +593,10 @@ protected function appendHeaders(): static * Send Status Code * * @param int $statusCode + * @param string $reason * @return void */ - abstract protected function sendStatus(int $statusCode): void; + abstract protected function sendStatus(int $statusCode, string $reason): void; /** * Send Header diff --git a/src/Http/Route.php b/src/Http/Route.php index 8a7d0620..f2dd40ae 100755 --- a/src/Http/Route.php +++ b/src/Http/Route.php @@ -51,8 +51,6 @@ public function __construct(string $method, string $path) $this->path($path); $this->method = $method; $this->order = ++self::$counter; - $this->action = function (): void { - }; } /** diff --git a/src/Http/Validator.php b/src/Http/Validator.php index 6a8304ea..f75b1550 100755 --- a/src/Http/Validator.php +++ b/src/Http/Validator.php @@ -2,7 +2,9 @@ namespace Utopia\Http; -abstract class Validator +use Utopia\Servers\Validator as ServersValidator; + +abstract class Validator extends ServersValidator { public const TYPE_BOOLEAN = 'boolean'; @@ -17,41 +19,4 @@ abstract class Validator public const TYPE_OBJECT = 'object'; public const TYPE_MIXED = 'mixed'; - - /** - * Get Description - * - * Returns validator description - * - * @return string - */ - abstract public function getDescription(): string; - - /** - * Is array - * - * Returns true if an array or false if not. - * - * @return bool - */ - abstract public function isArray(): bool; - - /** - * Is valid - * - * Returns true if valid or false if not. - * - * @param mixed $value - * @return bool - */ - abstract public function isValid($value): bool; - - /** - * Get Type - * - * Returns validator type. - * - * @return string - */ - abstract public function getType(): string; } diff --git a/src/Http/Validator/Multiple.php b/src/Http/Validator/Multiple.php new file mode 100644 index 00000000..4c919377 --- /dev/null +++ b/src/Http/Validator/Multiple.php @@ -0,0 +1,115 @@ +addRule($rule); + } + + $this->type = $type; + } + /** + * Add rule + * + * Add a new rule to the end of the rules containing array + * + * @param Validator $rule + * @return $this + */ + public function addRule(Validator $rule) + { + $this->rules[] = $rule; + + return $this; + } + + /** + * Get Description + * + * Returns validator description + * + * @return string + */ + public function getDescription(): string + { + $description = ''; + foreach ($this->rules as $key => $rule) { + $description .= ++$key . '. ' . $rule->getDescription() . " \n"; + } + + 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->rules as $rule) { /* @var $rule Validator */ + if (false === $rule->isValid($value)) { + 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/tests/HookTest.php b/tests/HookTest.php index 458cf9ad..4bff1fee 100644 --- a/tests/HookTest.php +++ b/tests/HookTest.php @@ -3,6 +3,8 @@ namespace Utopia\Http; use PHPUnit\Framework\TestCase; +use Utopia\DI\Container; +use Utopia\DI\Dependency; use Utopia\Http\Validator\Numeric; use Utopia\Http\Validator\Text; @@ -38,11 +40,8 @@ public function testGroupsCanBeSet() public function testActionCanBeSet() { - $this->assertEquals(function () { - }, $this->hook->getAction()); - $this->hook->action(fn () => 'hello world'); - + $this->assertIsCallable($this->hook->getAction()); $this->assertEquals('hello world', $this->hook->getAction()()); } @@ -59,17 +58,38 @@ public function testParamCanBeSet() public function testResourcesCanBeInjected() { - $this->assertEquals([], $this->hook->getInjections()); - - $this->hook + $main = $this->hook + ->setName('test') ->inject('user') ->inject('time') - ->action(function () { + ->setCallback(function ($user, $time) { + return $user . ':' . $time; + }); + + $user = new Dependency(); + $user + ->setName('user') + ->setCallback(function () { + return 'user'; }); - $this->assertCount(2, $this->hook->getInjections()); - $this->assertEquals('user', $this->hook->getInjections()['user']['name']); - $this->assertEquals('time', $this->hook->getInjections()['time']['name']); + $time = new Dependency(); + $time + ->setName('time') + ->setCallback(function () { + return '00:00:00'; + }); + + $context = new Container(); + + $context + ->set($user) + ->set($time) + ; + + $result = $context->inject($main); + + $this->assertEquals('user:00:00:00', $result); } public function testParamValuesCanBeSet() diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 0d314cd3..70f41a27 100755 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -3,16 +3,20 @@ namespace Utopia\Http; use PHPUnit\Framework\TestCase; -use Utopia\Http\Tests\UtopiaFPMRequestTest; +use Throwable; +use Utopia\DI\Container; +use Utopia\DI\Dependency; +use Utopia\Http\Tests\MockRequest as Request; +use Utopia\Http\Tests\MockResponse as Response; use Utopia\Http\Validator\Text; -use Utopia\Http\Adapter\FPM\Request; -use Utopia\Http\Adapter\FPM\Response; use Utopia\Http\Adapter\FPM\Server; class HttpTest extends TestCase { protected ?Http $http; + protected Container $context; + protected ?string $method; protected ?string $uri; @@ -20,7 +24,28 @@ class HttpTest extends TestCase public function setUp(): void { Http::reset(); - $this->http = new Http(new Server(), 'Asia/Tel_Aviv'); + + $this->context = new Container(); + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(fn () => new Request()); + + $response = new Dependency(); + $response + ->setName('response') + ->setCallback(fn () => new Response()); + + $this->context + ->set($request) + ->set($response); + + $this->http = new Http(new Server(), $this->context, 'Asia/Tel_Aviv'); + + $this->http->setRequestClass(Request::class); + $this->http->setResponseClass(Response::class); + $this->saveRequest(); } @@ -71,85 +96,72 @@ public function testCanGetDifferentModes(): void $this->assertTrue(Http::isStage()); } - public function testCanGetEnvironmentVariable(): void - { - // Mock - $_SERVER['key'] = 'value'; - - $this->assertEquals(Http::getEnv('key'), 'value'); - $this->assertEquals(Http::getEnv('unknown', 'test'), 'test'); - } - - public function testCanGetResources(): void + public function testCanExecuteRoute(): void { - Http::setResource('rand', fn () => rand()); - Http::setResource('first', fn ($second) => "first-{$second}", ['second']); - Http::setResource('second', fn () => 'second'); - - $second = $this->http->getResource('second', '1'); - $first = $this->http->getResource('first', '1'); - $this->assertEquals('second', $second); - $this->assertEquals('first-second', $first); - - $resource = $this->http->getResource('rand', '1'); + $context = clone $this->context; - $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->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error: ' . $error->getMessage() . ' on file: ' . $error->getFile() . ' on line: ' . $error->getLine(); + }); // Default Params - $route = new Route('GET', '/path'); + $route = $this->http->addRoute('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; + ->action(function ($x, $y) { + echo $x . '-' . $y; + }); + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request([]); + $request->setURI('/path'); + $request->setMethod('GET'); + return $request; }); + $context + ->set($request); + \ob_start(); - $this->http->execute($route, new Request(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); - $this->assertEquals('x-def-y-def-' . $resource, $result); + $this->assertEquals('x-def-y-def', $result); } - public function testCanGetDefaultValueWithFunction(): void + public function testCanExecuteRouteWithParams(): void { - Http::setResource('first', fn ($second) => "first-{$second}", ['second']); - Http::setResource('second', fn () => 'second'); - - $second = $this->http->getResource('second'); - $first = $this->http->getResource('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; + $context = clone $this->context; + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request(['x' => 'param-x', 'y' => 'param-y', 'z' => 'param-z']); + $request->setURI('/test-params'); + $request->setMethod('GET'); + return $request; }); - \ob_start(); - $this->http->execute($route, new Request(), '1'); - $result = \ob_get_contents(); - \ob_end_clean(); + $rand = new Dependency(); + $rand + ->setName('rand') + ->setCallback(function () { + return rand(0, 1000); + }); - $this->assertEquals('first-second-second', $result); - } - - public function testCanExecuteRoute(): void - { - Http::setResource('rand', fn () => rand()); - $resource = $this->http->getResource('rand', '1'); + $context + ->set($request) + ->set($rand); $this->http ->error() @@ -158,24 +170,7 @@ public function testCanExecuteRoute(): void echo 'error: ' . $error->getMessage(); }); - // Default Params - $route = new Route('GET', '/path'); - - $route - ->param('x', 'x-def', new Text(200), 'x param', true) - ->param('y', 'y-def', new Text(200), 'y param', true) - ->action(function ($x, $y) { - echo $x . '-' . $y; - }); - - \ob_start(); - $this->http->execute($route, new Request(), '1'); - $result = \ob_get_contents(); - \ob_end_clean(); - - // With Params - $resource = $this->http->getResource('rand', '1'); - $route = new Route('GET', '/path'); + $route = $this->http->addRoute('GET', '/test-params'); $route ->param('x', 'x-def', new Text(200), 'x param', true) @@ -191,17 +186,16 @@ public function testCanExecuteRoute(): void }); \ob_start(); - $request = new UtopiaFPMRequestTest(); - $request::_setParams(['x' => 'param-x', 'y' => 'param-y', 'z' => 'param-z']); - $this->http->execute($route, $request, '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); - + $resource = $context->get('rand'); $this->assertEquals($resource . '-param-x-param-y', $result); + } - // With Error - $resource = $this->http->getResource('rand', '1'); - $route = new Route('GET', '/path'); + public function testCanExecuteRouteWithParamsWithError(): void + { + $route = $this->http->addRoute('GET', '/test-params-error'); $route ->param('x', 'x-def', new Text(1, min: 0), 'x param', false) @@ -210,17 +204,48 @@ public function testCanExecuteRoute(): void echo $x . '-', $y; }); + $this->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error: ' . $error->getMessage(); + }); + \ob_start(); - $request = new UtopiaFPMRequestTest(); - $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->http->execute($route, $request, '1'); + $context = clone $this->context; + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request(['x' => 'param-x', 'y' => 'param-y']); + $request->setURI('/test-params-error'); + $request->setMethod('GET'); + return $request; + }); + + $rand = new Dependency(); + $rand + ->setName('rand') + ->setCallback(function () { + return rand(0, 1000); + }); + + $context + ->set($request) + ->set($rand); + + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); $this->assertEquals('error: Invalid `x` param: Value must be a valid string and no longer than 1 chars', $result); + } + + public function testCanExecuteRouteWithParamsWithHooks(): void + { + $context = clone $this->context; - // With Hooks - $resource = $this->http->getResource('rand', '1'); $this->http ->init() ->inject('rand') @@ -230,6 +255,7 @@ public function testCanExecuteRoute(): void $this->http ->shutdown() + ->desc('global shutdown') ->action(function () { echo '-shutdown'; }); @@ -243,6 +269,7 @@ public function testCanExecuteRoute(): void $this->http ->shutdown() + ->desc('api shutdown') ->groups(['api']) ->action(function () { echo '-(shutdown-api)'; @@ -262,7 +289,14 @@ public function testCanExecuteRoute(): void echo '-(shutdown-homepage)'; }); - $route = new Route('GET', '/path'); + $this->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error: ' . $error->getMessage(); + }); + + $route = $this->http->addRoute('GET', '/path-1'); $route ->groups(['api']) @@ -272,7 +306,7 @@ public function testCanExecuteRoute(): void echo $x . '-', $y; }); - $homepage = new Route('GET', '/path'); + $homepage = $this->http->addRoute('GET', '/path-2'); $homepage ->groups(['homepage']) @@ -283,19 +317,61 @@ public function testCanExecuteRoute(): void }); \ob_start(); - $request = new UtopiaFPMRequestTest(); - $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->http->execute($route, $request, '1'); + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request(['x' => 'param-x', 'y' => 'param-y']); + $request->setURI('/path-1'); + $request->setMethod('GET'); + return $request; + }); + + $rand = new Dependency(); + $rand + ->setName('rand') + ->setCallback(function () { + return rand(0, 1000); + }); + + $context + ->set($request) + ->set($rand); + + $resource = $context->get('rand'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); $this->assertEquals('init-' . $resource . '-(init-api)-param-x-param-y-(shutdown-api)-shutdown', $result); - $resource = $this->http->getResource('rand', '1'); + $context = clone $this->context; + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request(['x' => 'param-x', 'y' => 'param-y']); + $request->setURI('/path-2'); + $request->setMethod('GET'); + return $request; + }); + + $rand = new Dependency(); + $rand + ->setName('rand') + ->setCallback(function () { + return rand(0, 1000); + }); + + $context + ->set($request) + ->set($rand); + + $resource = $context->get('rand'); \ob_start(); - $request = new UtopiaFPMRequestTest(); - $request::_setParams(['x' => 'param-x', 'y' => 'param-y']); - $this->http->execute($homepage, $request, '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); @@ -304,6 +380,8 @@ public function testCanExecuteRoute(): void public function testCanAddAndExecuteHooks() { + $context = clone $this->context; + $this->http ->init() ->action(function () { @@ -317,22 +395,34 @@ public function testCanAddAndExecuteHooks() }); // Default Params - $route = new Route('GET', '/path'); + $route = $this->http->addRoute('GET', '/path-3'); $route ->param('x', 'x-def', new Text(200), 'x param', true) ->action(function ($x) { echo $x; }); + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request([]); + $request->setURI('/path-3'); + $request->setMethod('GET'); + return $request; + }); + + $context + ->set($request); + \ob_start(); - $this->http->execute($route, new Request(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); - $this->assertEquals('(init)-x-def-(shutdown)', $result); // Default Params - $route = new Route('GET', '/path'); + $route = $this->http->addRoute('GET', '/path-4'); $route ->param('x', 'x-def', new Text(200), 'x param', true) ->hook(false) @@ -340,8 +430,22 @@ public function testCanAddAndExecuteHooks() echo $x; }); + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request([]); + $request->setURI('/path-4'); + $request->setMethod('GET'); + return $request; + }); + + $context + ->set($request) + ; + \ob_start(); - $this->http->execute($route, new Request(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); @@ -380,6 +484,8 @@ public function testAllowRouteOverrides() public function testCanHookThrowExceptions() { + $context = clone $this->context; + $this->http ->init() ->param('y', '', new Text(5), 'y param', false) @@ -387,52 +493,70 @@ public function testCanHookThrowExceptions() echo '(init)-' . $y . '-'; }); - $this->http - ->error() - ->inject('error') - ->action(function ($error) { - echo 'error-' . $error->getMessage(); - }); - $this->http ->shutdown() ->action(function () { echo '-(shutdown)'; }); + $this->http + ->error() + ->inject('error') + ->action(function ($error) { + echo 'error: ' . $error->getMessage(); + }); + // param not provided for init - $route = new Route('GET', '/path'); + $route = Http::addRoute('GET', '/path-5'); $route ->param('x', 'x-def', new Text(200), 'x param', true) ->action(function ($x) { echo $x; }); + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request([]); + $request->setURI('/path-5'); + $request->setMethod('GET'); + return $request; + }); + + $context + ->set($request); + \ob_start(); - $this->http->execute($route, new Request(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); - $this->assertEquals('error-Param "y" is not optional.', $result); + $this->assertEquals('error: Param "y" is not optional.', $result); + + $context = clone $this->context; + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $request = new Request(['y' => 'y-def']); + $request->setURI('/path-5'); + $request->setMethod('GET'); + return $request; + }); + + $context + ->set($request); \ob_start(); - $_GET['y'] = 'y-def'; - $this->http->execute($route, new Request(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); $this->assertEquals('(init)-y-def-x-def-(shutdown)', $result); } - public function testCanSetRoute() - { - $route = new Route('GET', '/path'); - - $this->assertEquals($this->http->getRoute(), null); - $this->http->setRoute($route); - $this->assertEquals($this->http->getRoute(), $route); - } - public function providerRouteMatching(): array { return [ @@ -480,8 +604,8 @@ public function testCanMatchRoute(string $method, string $path, string $url = nu $_SERVER['REQUEST_METHOD'] = $method; $_SERVER['REQUEST_URI'] = $url; - $this->assertEquals($expected, $this->http->match(new Request())); - $this->assertEquals($expected, $this->http->getRoute()); + $route = $this->http->match(new Request()); + $this->assertEquals($expected, $route); } public function testNoMismatchRoute(): void @@ -501,73 +625,59 @@ public function testNoMismatchRoute(): void ], ]; - foreach ($requests as $request) { - Http::get($request['path']); + foreach ($requests as $requestObj) { + Http::get($requestObj['path']); - $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; - $_SERVER['REQUEST_URI'] = $request['url']; + $context = clone $this->context; - $route = $this->http->match(new Request(), fresh: true); + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () use ($requestObj) { + $_SERVER['REQUEST_METHOD'] = Http::REQUEST_METHOD_GET; + $_SERVER['REQUEST_URI'] = $requestObj['url']; + return new Request(); + }); - $this->assertEquals(null, $route); - $this->assertEquals(null, $this->http->getRoute()); - } - } + $context + ->set($request); - public function testCanMatchFreshRoute(): void - { - $route1 = Http::get('/path1'); - $route2 = Http::get('/path2'); + $this->http->run($context); - try { - // Match first request - $_SERVER['REQUEST_METHOD'] = 'HEAD'; - $_SERVER['REQUEST_URI'] = '/path1'; - $matched = $this->http->match(new Request()); - $this->assertEquals($route1, $matched); - $this->assertEquals($route1, $this->http->getRoute()); - - // Second request match returns cached route - $_SERVER['REQUEST_METHOD'] = 'HEAD'; - $_SERVER['REQUEST_URI'] = '/path2'; - $request2 = new Request(); - $matched = $this->http->match($request2, fresh: false); - $this->assertEquals($route1, $matched); - $this->assertEquals($route1, $this->http->getRoute()); - - // Fresh match returns new route - $matched = $this->http->match($request2, fresh: true); - $this->assertEquals($route2, $matched); - $this->assertEquals($route2, $this->http->getRoute()); - } catch (\Exception $e) { - $this->fail($e->getMessage()); + $this->assertEquals($_SERVER['REQUEST_METHOD'], $context->get('route')->getMethod()); + $this->assertEquals($_SERVER['REQUEST_URI'], $context->get('route')->getPath()); } } public function testCanRunRequest(): void { // Test head requests - - $method = (isset($_SERVER['REQUEST_METHOD'])) ? $_SERVER['REQUEST_METHOD'] : null; - $uri = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : null; - - $_SERVER['REQUEST_METHOD'] = 'HEAD'; - $_SERVER['REQUEST_URI'] = '/path'; - Http::get('/path') ->inject('response') ->action(function ($response) { - $response->send('HELLO'); + echo 'HELLO'; }); \ob_start(); - $this->http->run(new Request(), new Response(), '1'); + + $context = clone $this->context; + + $request = new Dependency(); + $request + ->setName('request') + ->setCallback(function () { + $_SERVER['REQUEST_METHOD'] = 'HEAD'; + $_SERVER['REQUEST_URI'] = '/path'; + return new Request(); + }); + + $this->context + ->set($request); + + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); - $_SERVER['REQUEST_METHOD'] = $method; - $_SERVER['REQUEST_URI'] = $uri; - $this->assertStringNotContainsString('HELLO', $result); } @@ -580,34 +690,46 @@ public function testWildcardRoute(): void $_SERVER['REQUEST_URI'] = '/unknown_path'; Http::init() - ->action(function () { - $route = $this->http->getRoute(); - Http::setResource('myRoute', fn () => $route); + ->inject('route') + ->inject('di') + ->action(function (Route $route, Container $di) { + $dependency = new Dependency(); + $dependency->setName('myRoute'); + $dependency->setCallback(fn () => $route); + $di->set($dependency); }); - Http::wildcard() - ->inject('myRoute') ->inject('response') - ->action(function (mixed $myRoute, $response) { - if ($myRoute == null) { - $response->send('ROUTE IS NULL!'); - } else { - $response->send('HELLO'); - } + ->action(function (Response $response) { + echo 'HELLO'; }); + Http::get('/') + ->inject('response') + ->action(function (Response $response) { + $response->send('root /'); + }); + + Http::error() + ->inject('error') + ->inject('response') + ->action(function (Throwable $error, Response $response) { + $response->send($error->getMessage() . ' on file: ' . $error->getFile() . ' on line: ' . $error->getLine()); + }); + + $context = clone $this->context; + \ob_start(); - @$this->http->run(new Request(), new Response(), '1'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); $this->assertEquals('HELLO', $result); \ob_start(); - $req = new Request(); - $req = $req->setMethod('OPTIONS'); - @$this->http->run($req, new Response(), '1'); + $context->get('request')->setMethod('OPTIONS'); + $this->http->run($context); $result = \ob_get_contents(); \ob_end_clean(); diff --git a/tests/MockRequest.php b/tests/MockRequest.php new file mode 100644 index 00000000..936c7440 --- /dev/null +++ b/tests/MockRequest.php @@ -0,0 +1,65 @@ +overrides = $overrides; + } + + /** + * Get Param + * + * Get param by current method name + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function getParam(string $key, $default = null): mixed + { + $params = \array_merge($this->overrides, parent::getParams()); + + if (\array_key_exists($key, $params)) { + return $params[$key]; + } + + return $default; + } + + /** + * Get Params + * + * Get all params of current method + * + * @return array + */ + public function getParams(): array + { + $params = \array_merge(parent::getParams(), $this->overrides); + + return $params; + } + + /** + * Send Header + * + * Output Header + * + * @param string $key + * @param string $value + * @return void + */ + public function sendHeader(string $key, string $value): void + { + } +} diff --git a/tests/MockResponse.php b/tests/MockResponse.php new file mode 100644 index 00000000..9aec35e6 --- /dev/null +++ b/tests/MockResponse.php @@ -0,0 +1,33 @@ +assertEquals($this->request->getQuery('unknown', 'test'), 'test'); } - public function testCanSetQueryString() + public function testCanSetQuery() { - $this->request->setQueryString(['key' => 'value']); + $this->request->setQuery(['key' => 'value']); $this->assertEquals($this->request->getQuery('key'), 'value'); $this->assertEquals($this->request->getQuery('unknown', 'test'), 'test'); @@ -156,6 +156,22 @@ public function testCanSetUri() $this->assertEquals('/page.html', $this->request->getURI()); } + public function testCanGetQueryString() + { + $this->assertEquals('', $this->request->getQueryString()); + + $_SERVER['QUERY_STRING'] = 'text=hello&value=key'; + + $this->assertEquals('text=hello&value=key', $this->request->getQueryString()); + } + + public function testCanSetQueryString() + { + $this->request->setURI('text=hello&value=key'); + + $this->assertEquals('text=hello&value=key', $this->request->getURI()); + } + public function testCanGetPort() { $_SERVER['HTTP_HOST'] = 'localhost:8080'; diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index d9d0acc1..a993b0ea 100755 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Utopia\Http\Adapter\FPM\Response; +use Utopia\Http\Tests\MockResponse; class FPMResponseTest extends TestCase { @@ -11,7 +12,7 @@ class FPMResponseTest extends TestCase public function setUp(): void { - $this->response = new Response(); + $this->response = new MockResponse(); } public function tearDown(): void diff --git a/tests/RouteTest.php b/tests/RouteTest.php index 4a966b87..97a484e3 100755 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -48,8 +48,7 @@ public function testCanSetAndGetGroups() public function testCanSetAndGetAction() { - $this->assertEquals(function (): void { - }, $this->route->getAction()); + $this->assertEquals(null, $this->route->getAction()); $this->route->action(fn () => 'hello world'); @@ -67,21 +66,6 @@ public function testCanGetAndSetParam() $this->assertCount(2, $this->route->getParams()); } - public function testCanInjectResources() - { - $this->assertEquals([], $this->route->getInjections()); - - $this->route - ->inject('user') - ->inject('time') - ->action(function () { - }); - - $this->assertCount(2, $this->route->getInjections()); - $this->assertEquals('user', $this->route->getInjections()['user']['name']); - $this->assertEquals('time', $this->route->getInjections()['time']['name']); - } - public function testCanSetAndGetLabels() { $this->assertEquals('default', $this->route->getLabel('key', 'default')); diff --git a/tests/UtopiaRequestTest.php b/tests/UtopiaRequestTest.php deleted file mode 100644 index 4c78751d..00000000 --- a/tests/UtopiaRequestTest.php +++ /dev/null @@ -1,77 +0,0 @@ -assertEquals('123', $response['body']); } + public function testHeaders() + { + $response = $this->client->call(Client::METHOD_GET, '/headers'); + $this->assertGreaterThan(8, count($response['headers'])); + $this->assertEquals('value1', $response['headers']['key1']); + $this->assertEquals('value2', $response['headers']['key2']); + $this->assertNotEmpty($response['body']); + } + + public function testHead() + { + $response = $this->client->call(Client::METHOD_HEAD, '/headers'); + $this->assertGreaterThan(8, $response['headers']); + $this->assertEquals('value1', $response['headers']['key1']); + $this->assertEquals('value2', $response['headers']['key2']); + $this->assertEmpty(trim($response['body'])); + } + + public function testNoContent() + { + $response = $this->client->call(Client::METHOD_DELETE, '/no-content'); + $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEmpty(trim($response['body'])); + } + public function testChunkResponse() { $response = $this->client->call(Client::METHOD_GET, '/chunked'); @@ -30,9 +55,29 @@ public function testRedirect() $this->assertEquals('Hello World!', $response['body']); } - public function testFile() + public function testHumans() { $response = $this->client->call(Client::METHOD_GET, '/humans.txt'); - $this->assertEquals(204, $response['headers']['status-code']); + $this->assertEquals('humans.txt', $response['body']); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Utopia', $response['headers']['x-engine']); + } + + public function testParamInjection() + { + $response = $this->client->call(Client::METHOD_GET, '/param-injection?param=1234567891011'); + $this->assertEquals(400, $response['headers']['status-code']); + $this->assertStringStartsWith('Invalid `param` param: Value must be a valid string and at least 1 chars and no longer than 10 chars', $response['body']); + + $response = $this->client->call(Client::METHOD_GET, '/param-injection?param=test4573'); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertStringStartsWith('Hello World!test4573', $response['body']); + } + + public function testNotFound() + { + $response = $this->client->call(Client::METHOD_GET, '/non-existing-page'); + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertStringStartsWith('Not Found on ', $response['body']); } } diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 5bf15b2e..6597d109 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -61,6 +61,10 @@ public function call(string $method, string $path = '', array $headers = [], arr $responseType = ''; $responseBody = ''; + if($method == self::METHOD_HEAD) { + curl_setopt($ch, CURLOPT_NOBODY, true); + } + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); diff --git a/tests/e2e/ResponseFPMTest.php b/tests/e2e/ResponseFPMTest.php index 8a3cb4ff..88cff38e 100644 --- a/tests/e2e/ResponseFPMTest.php +++ b/tests/e2e/ResponseFPMTest.php @@ -12,6 +12,6 @@ class ResponseFPMTest extends TestCase public function setUp(): void { - $this->client = new Client(); + $this->client = new Client('http://fpm'); } } diff --git a/tests/e2e/ResponseSwooleCoroutineTest.php b/tests/e2e/ResponseSwooleCoroutineTest.php new file mode 100644 index 00000000..d1811e37 --- /dev/null +++ b/tests/e2e/ResponseSwooleCoroutineTest.php @@ -0,0 +1,17 @@ +client = new Client('http://swoole-coroutine'); + } +} diff --git a/tests/e2e/ResponseSwooleTest.php b/tests/e2e/ResponseSwooleTest.php index 0851554a..6d793b90 100755 --- a/tests/e2e/ResponseSwooleTest.php +++ b/tests/e2e/ResponseSwooleTest.php @@ -14,10 +14,4 @@ public function setUp(): void { $this->client = new Client('http://swoole'); } - - public function testSwooleResources(): void - { - $response = $this->client->call(Client::METHOD_DELETE, '/swoole-test'); - $this->assertEquals('DELETE', $response['body']); - } } diff --git a/tests/e2e/init.php b/tests/e2e/init.php index 64bb3cfa..540891e6 100644 --- a/tests/e2e/init.php +++ b/tests/e2e/init.php @@ -1,7 +1,8 @@ setName('num') + ->setCallback(function () { + return 10; + }); + +$container->set($dependency); + +Http::init() + ->inject('response') + ->action(function ($response) { + $response->addHeader('X-Engine', 'Utopia'); + }); + + +// Http::wildcard() +// ->inject('response') +// ->action(function ($response) { +// $response->send('WILDCARD'); +// }); + Http::get('/') ->inject('response') ->action(function (Response $response) { $response->send('Hello World!'); }); +Http::get('/headers') + ->inject('response') + ->action(function (Response $response) { + $response + ->addHeader('key1', 'value1') + ->addHeader('key2', 'value2') + ->send('Hello World!'); + }); + +Http::get('/keys') + ->inject('response') + ->inject('key') + ->action(function (Response $response, string $key) { + if (rand(0, 50) == 1) { + System::sleep(1); + } + + $response->send($key); + }); + Http::get('/value/:value') ->param('value', '', new Text(64)) ->inject('response') @@ -40,7 +86,49 @@ }); Http::get('/humans.txt') + ->inject('response') + ->action(function (Response $response) { + $response + ->setStatusCode(200) + ->text('humans.txt'); + }); + +Http::delete('/no-content') ->inject('response') ->action(function (Response $response) { $response->noContent(); }); + +Http::get('/db-ping') + ->inject('pool') + ->inject('response') + ->action(function (PDOPool $pool, Response $response) { + $pdo = $pool->get(); + + $statement = $pdo->query('SELECT 1;'); + $output = ''; + while ($row = $statement->fetch()) { + // var_dump('worked!'); + $output .= $row[0]; + } + + $pool->put($pdo); + + $response->send($output); + }); + +Http::get('/param-injection') + ->inject('response') + ->param('param', 'default', fn ($num) => new Text($num), 'test param', false, ['num']) + ->action(function (Response $response, string $param) { + $response->send('Hello World!' . $param); + }); + +Http::error() + ->inject('error') + ->inject('response') + ->action(function (Throwable $error, Response $response) { + $response + ->setStatusCode($error->getCode()) + ->send($error->getMessage().' on file: '.$error->getFile().' on line: '.$error->getLine()); + }); diff --git a/tests/e2e/server-fpm.php b/tests/e2e/server-fpm.php index b886c118..75f285a4 100644 --- a/tests/e2e/server-fpm.php +++ b/tests/e2e/server-fpm.php @@ -1,10 +1,16 @@ start(); diff --git a/tests/e2e/server-swoole-coroutine.php b/tests/e2e/server-swoole-coroutine.php new file mode 100644 index 00000000..58995168 --- /dev/null +++ b/tests/e2e/server-swoole-coroutine.php @@ -0,0 +1,52 @@ +withHost('mariadb') + ->withPort(3306) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName('test') + ->withCharset('utf8mb4') + ->withUsername('user') + ->withPassword('password'), 9000); + + +$dependency = new Dependency(); + +$dependency + ->setName('key') + ->inject('request') + ->setCallback(function (Request $request) { + return $request->getHeader('x-utopia-key', 'unknown'); + }); + +$container->set($dependency); + +$dependency1 = new Dependency(); +$dependency1 + ->setName('pool') + ->setCallback(function () use ($pool) { + return $pool; + }); + +$container->set($dependency1); + +$server = new Server('0.0.0.0', '80'); +$http = new Http($server, $container, 'UTC'); + +echo "Server started\n"; + +$http->start(); diff --git a/tests/e2e/server-swoole.php b/tests/e2e/server-swoole.php index e15e1be4..367d4ee7 100755 --- a/tests/e2e/server-swoole.php +++ b/tests/e2e/server-swoole.php @@ -1,29 +1,52 @@ inject('swooleRequest') - ->inject('swooleResponse') - ->action(function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) { - $method = $swooleRequest->getMethod(); - $swooleResponse->header('Content-Type', 'text/plain'); - $swooleResponse->header('Cache-Control', 'no-cache'); - $swooleResponse->setStatusCode(200); - $swooleResponse->write($method); - $swooleResponse->end(); +$container = new Container(); + +require_once __DIR__.'/init.php'; + +$pool = new PDOPool((new PDOConfig()) + ->withHost('mariadb') + ->withPort(3306) + // ->withUnixSocket('/tmp/mysql.sock') + ->withDbName('test') + ->withCharset('utf8mb4') + ->withUsername('user') + ->withPassword('password'), 9000); + + +$dependency = new Dependency(); + +$dependency + ->setName('key') + ->inject('request') + ->setCallback(function (Request $request) { + return $request->getHeader('x-utopia-key', 'unknown'); + }); + +$container->set($dependency); + +$dependency1 = new Dependency(); +$dependency1 + ->setName('pool') + ->setCallback(function () use ($pool) { + return $pool; }); +$container->set($dependency1); + $server = new Server('0.0.0.0', '80'); -$http = new Http($server, 'UTC'); +$http = new Http($server, $container, 'UTC'); + +echo "Server started\n"; -run(function () use ($http) { - $http->start(); -}); +$http->start(); diff --git a/tests/k6/benchmark.js b/tests/k6/benchmark.js new file mode 100644 index 00000000..f9946a8f --- /dev/null +++ b/tests/k6/benchmark.js @@ -0,0 +1,50 @@ +import http from 'k6/http'; +import { check } from 'k6'; +import { Counter } from 'k6/metrics'; + +// A simple counter for http requests +export const requests = new Counter('http_reqs'); + +export const options = { + scenarios: { + contacts: { + executor: 'ramping-arrival-rate', + preAllocatedVUs: 1000, + timeUnit: '30s', + startRate: 1000000, + stages: [ + { target: 1000000, duration: '10m' }, + ], + }, + }, +}; + +// Example list of keys to iterate over +// const keys = ['key1', 'key2', 'key3']; // Add your actual keys here +const keys = ['key1']; // Add your actual keys here + +export default function () { + // Iterate over each key for the request + keys.forEach(key => { + const config = { + headers: { + 'X-Utopia-Key': key, + } + }; + + const resDb = http.get('http://localhost:9401', config); + + if(resDb.status !== 200) { + console.log(`Error: ${resDb.status}`); + } + + check(resDb, { + 'status is 200': (r) => r.status === 200, + // Check if the echoed key in response is the same as the sent key + // 'response contains the same X-Utopia-Key value': (r) => { + // // Assuming the response is JSON and has a key that echoes the header value + // return r.body === key; + // }, + }); + }); +}