diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9b19238 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,20 @@ +name: "CodeQL" + +on: [pull_request] +jobs: + analyse: + name: Static Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - run: git checkout HEAD^2 + + - name: Run PHPStan + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d2bc1dd --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: "Tests" + +on: [pull_request] +jobs: + test: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - run: git checkout HEAD^2 + + - name: Run Tests + run: | + docker compose up -d + sleep 10 + docker compose exec -T web vendor/bin/phpunit --configuration phpunit.xml diff --git a/.gitignore b/.gitignore index 50b321e..f1b5974 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor composer.lock .phpunit.result.cache +.idea diff --git a/Dockerfile b/Dockerfile index 325d287..dae8340 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,6 @@ FROM composer:2.0 AS step0 - ARG TESTING=true - ENV TESTING=$TESTING WORKDIR /usr/local/src/ @@ -13,17 +11,16 @@ RUN composer update --ignore-platform-reqs --optimize-autoloader \ --no-plugins --no-scripts --prefer-dist \ `if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi` -FROM php:8.0-cli-alpine as final +FROM php:8.4-cli-alpine3.21 AS final LABEL maintainer="team@appwrite.io" ENV DEBIAN_FRONTEND=noninteractive \ - PHP_VERSION=8 + PHP_VERSION=84 RUN \ apk add --no-cache --virtual .deps \ supervisor php$PHP_VERSION php$PHP_VERSION-fpm nginx bash - # Nginx Configuration (with self-signed ssl certificates) COPY ./tests/docker/nginx.conf /etc/nginx/nginx.conf diff --git a/composer.json b/composer.json index 244834b..af6e7b9 100644 --- a/composer.json +++ b/composer.json @@ -24,14 +24,19 @@ }, "require-dev": { "phpunit/phpunit": "9.*", - "laravel/pint": "1.*" + "laravel/pint": "1.*", + "phpstan/phpstan": "2.*" }, "scripts": { "format": "vendor/bin/pint", "lint": "vendor/bin/pint --test", + "check": "vendor/bin/phpstan analyse --level 8 src tests --memory-limit=4G", "test": "docker-compose up -d && sleep 10 && docker-compose exec web vendor/bin/phpunit --configuration phpunit.xml" }, "config": { + "platform": { + "php": "8.0" + }, "allow-plugins": { "php-http/discovery": false, "tbachert/spi": false diff --git a/composer.lock b/composer.lock index 4d21e7b..8b42925 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c7f241593838cc2c4b51033133c5213b", + "content-hash": "38fa21db6b1dadea0bedcc5b68e83ddb", "packages": [ { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.1", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", + "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.1" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2025-11-24T14:40:29+00:00" }, { "name": "composer/semver", @@ -145,24 +145,21 @@ }, { "name": "google/protobuf", - "version": "v4.32.0", + "version": "v4.33.2", "source": { "type": "git", "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646" + "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", - "reference": "9a9a92ecbe9c671dc1863f6d4a91ea3ea12c8646", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", + "reference": "fbd96b7bf1343f4b0d8fb358526c7ba4d72f1318", "shasum": "" }, "require": { "php": ">=8.1.0" }, - "provide": { - "ext-protobuf": "*" - }, "require-dev": { "phpunit/phpunit": ">=5.0.0 <8.5.27" }, @@ -186,9 +183,9 @@ "proto" ], "support": { - "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.0" + "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.33.2" }, - "time": "2025-08-14T20:00:33+00:00" + "time": "2025-12-05T22:12:22+00:00" }, { "name": "nyholm/psr7", @@ -336,20 +333,20 @@ }, { "name": "open-telemetry/api", - "version": "1.5.0", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/api.git", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5" + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/7692075f486c14d8cfd37fba98a08a5667f089e5", - "reference": "7692075f486c14d8cfd37fba98a08a5667f089e5", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", + "reference": "45bda7efa8fcdd9bdb0daa2f26c8e31f062f49d4", "shasum": "" }, "require": { - "open-telemetry/context": "^1.0", + "open-telemetry/context": "^1.4", "php": "^8.1", "psr/log": "^1.1|^2.0|^3.0", "symfony/polyfill-php82": "^1.26" @@ -365,7 +362,7 @@ ] }, "branch-alias": { - "dev-main": "1.4.x-dev" + "dev-main": "1.8.x-dev" } }, "autoload": { @@ -398,24 +395,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-07T23:07:38+00:00" + "time": "2025-10-19T10:49:48+00:00" }, { "name": "open-telemetry/context", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/context.git", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6" + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/438f71812242db3f196fb4c717c6f92cbc819be6", - "reference": "438f71812242db3f196fb4c717c6f92cbc819be6", + "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf", + "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf", "shasum": "" }, "require": { @@ -461,20 +458,20 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-08-13T01:12:00+00:00" + "time": "2025-09-19T00:05:49+00:00" }, { "name": "open-telemetry/exporter-otlp", - "version": "1.3.2", + "version": "1.3.3", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/exporter-otlp.git", - "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2" + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", - "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/07b02bc71838463f6edcc78d3485c04b48fb263d", + "reference": "07b02bc71838463f6edcc78d3485c04b48fb263d", "shasum": "" }, "require": { @@ -521,24 +518,24 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-06-16T00:24:51+00:00" + "time": "2025-11-13T08:04:37+00:00" }, { "name": "open-telemetry/gen-otlp-protobuf", - "version": "1.5.0", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0" + "reference": "673af5b06545b513466081884b47ef15a536edde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/585bafddd4ae6565de154610b10a787a455c9ba0", - "reference": "585bafddd4ae6565de154610b10a787a455c9ba0", + "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde", + "reference": "673af5b06545b513466081884b47ef15a536edde", "shasum": "" }, "require": { @@ -588,27 +585,27 @@ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-01-15T23:07:07+00:00" + "time": "2025-09-17T23:10:12+00:00" }, { "name": "open-telemetry/sdk", - "version": "1.7.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06" + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/52690d4b37ae4f091af773eef3c238ed2bc0aa06", - "reference": "52690d4b37ae4f091af773eef3c238ed2bc0aa06", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", + "reference": "3dfc3d1ad729ec7eb25f1b9a4ae39fe779affa99", "shasum": "" }, "require": { "ext-json": "*", "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.4", - "open-telemetry/context": "^1.0", + "open-telemetry/api": "^1.7", + "open-telemetry/context": "^1.4", "open-telemetry/sem-conv": "^1.0", "php": "^8.1", "php-http/discovery": "^1.14", @@ -642,7 +639,7 @@ ] }, "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-main": "1.9.x-dev" } }, "autoload": { @@ -681,11 +678,11 @@ ], "support": { "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V", - "docs": "https://opentelemetry.io/docs/php", + "docs": "https://opentelemetry.io/docs/languages/php", "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", "source": "https://github.com/open-telemetry/opentelemetry-php" }, - "time": "2025-09-05T07:17:06+00:00" + "time": "2025-11-25T10:59:15+00:00" }, { "name": "open-telemetry/sem-conv", @@ -746,24 +743,26 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.0.0", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", - "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", "shasum": "" }, "require": { "php": "^8" }, "require-dev": { - "phpunit/phpunit": "^9", - "vimeo/psalm": "^4|^5" + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" }, "type": "library", "autoload": { @@ -809,7 +808,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:36:18+00:00" + "time": "2025-09-24T15:06:41+00:00" }, { "name": "paragonie/random_compat", @@ -863,16 +862,16 @@ }, { "name": "php-amqplib/php-amqplib", - "version": "v3.7.3", + "version": "v3.7.4", "source": { "type": "git", "url": "https://github.com/php-amqplib/php-amqplib.git", - "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59" + "reference": "381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/9f50fe69a9f1a19e2cb25596a354d705de36fe59", - "reference": "9f50fe69a9f1a19e2cb25596a354d705de36fe59", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd", + "reference": "381b6f7c600e0e0c7463cdd7f7a1a3bc6268e5fd", "shasum": "" }, "require": { @@ -938,9 +937,9 @@ ], "support": { "issues": "https://github.com/php-amqplib/php-amqplib/issues", - "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.3" + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.7.4" }, - "time": "2025-02-18T20:11:13+00:00" + "time": "2025-11-23T17:00:56+00:00" }, { "name": "php-http/discovery", @@ -1023,16 +1022,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.46", + "version": "3.0.47", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", - "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d", + "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d", "shasum": "" }, "require": { @@ -1113,7 +1112,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47" }, "funding": [ { @@ -1129,7 +1128,7 @@ "type": "tidelift" } ], - "time": "2025-06-26T16:29:55+00:00" + "time": "2025-10-06T01:07:24+00:00" }, { "name": "psr/container", @@ -1617,16 +1616,16 @@ }, { "name": "symfony/http-client", - "version": "v7.3.3", + "version": "v7.4.1", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019" + "reference": "26cc224ea7103dda90e9694d9e139a389092d007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", - "reference": "333b9bd7639cbdaecd25a3a48a9d2dcfaa86e019", + "url": "https://api.github.com/repos/symfony/http-client/zipball/26cc224ea7103dda90e9694d9e139a389092d007", + "reference": "26cc224ea7103dda90e9694d9e139a389092d007", "shasum": "" }, "require": { @@ -1657,12 +1656,13 @@ "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", "symfony/amphp-http-client-meta": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -1693,7 +1693,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v7.3.3" + "source": "https://github.com/symfony/http-client/tree/v7.4.1" }, "funding": [ { @@ -1713,7 +1713,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T07:45:05+00:00" + "time": "2025-12-04T21:12:57+00:00" }, { "name": "symfony/http-client-contracts", @@ -2040,16 +2040,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -2103,7 +2103,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -2114,12 +2114,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "tbachert/spi", @@ -2309,28 +2313,29 @@ }, { "name": "utopia-php/framework", - "version": "0.33.24", + "version": "0.33.34", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0" + "reference": "76def92594c32504ec80eaacdb60ff8fad73c856" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/5112b1023342163e3fbedec99f38fc32c8700aa0", - "reference": "5112b1023342163e3fbedec99f38fc32c8700aa0", + "url": "https://api.github.com/repos/utopia-php/http/zipball/76def92594c32504ec80eaacdb60ff8fad73c856", + "reference": "76def92594c32504ec80eaacdb60ff8fad73c856", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.3", "utopia-php/compression": "0.1.*", - "utopia-php/telemetry": "0.1.*" + "utopia-php/telemetry": "0.1.*", + "utopia-php/validators": "0.1.*" }, "require-dev": { - "laravel/pint": "^1.2", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.25" + "laravel/pint": "1.*", + "phpbench/phpbench": "1.*", + "phpstan/phpstan": "1.*", + "phpunit/phpunit": "9.*" }, "type": "library", "autoload": { @@ -2350,9 +2355,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.24" + "source": "https://github.com/utopia-php/http/tree/0.33.34" }, - "time": "2025-09-04T04:18:39+00:00" + "time": "2025-12-08T07:55:31+00:00" }, { "name": "utopia-php/pools", @@ -2521,6 +2526,51 @@ "source": "https://github.com/utopia-php/telemetry/tree/0.1.1" }, "time": "2025-03-17T11:57:52+00:00" + }, + { + "name": "utopia-php/validators", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/validators.git", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.*", + "phpstan/phpstan": "1.*", + "phpunit/phpunit": "11.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight collection of reusable validators for Utopia projects", + "keywords": [ + "php", + "utopia", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/utopia-php/validators/issues", + "source": "https://github.com/utopia-php/validators/tree/0.1.0" + }, + "time": "2025-11-18T11:05:46+00:00" } ], "packages-dev": [ @@ -2596,16 +2646,16 @@ }, { "name": "laravel/pint", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a" + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a", - "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a", + "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", + "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "shasum": "" }, "require": { @@ -2616,22 +2666,19 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.82.2", - "illuminate/view": "^11.45.1", - "larastan/larastan": "^3.5.0", - "laravel-zero/framework": "^11.45.0", + "friendsofphp/php-cs-fixer": "^3.90.0", + "illuminate/view": "^12.40.1", + "larastan/larastan": "^3.8.0", + "laravel-zero/framework": "^12.0.4", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.1", - "pestphp/pest": "^2.36.0" + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.4" }, "bin": [ "builds/pint" ], "type": "project", "autoload": { - "files": [ - "overrides/Runner/Parallel/ProcessFactory.php" - ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -2651,6 +2698,7 @@ "description": "An opinionated code formatter for PHP.", "homepage": "https://laravel.com", "keywords": [ + "dev", "format", "formatter", "lint", @@ -2661,7 +2709,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-07-10T18:09:32+00:00" + "time": "2025-11-25T21:15:52+00:00" }, { "name": "myclabs/deep-copy", @@ -2725,16 +2773,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -2777,9 +2825,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -2899,6 +2947,59 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.1.33", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-12-05T10:24:31+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.32", @@ -3220,16 +3321,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.25", + "version": "9.6.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7" + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", "shasum": "" }, "require": { @@ -3254,7 +3355,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -3303,7 +3404,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" }, "funding": [ { @@ -3327,7 +3428,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:38:31+00:00" + "time": "2025-12-06T07:45:52+00:00" }, { "name": "sebastian/cli-parser", @@ -3770,16 +3871,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -3835,15 +3936,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -4330,16 +4443,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -4368,7 +4481,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -4376,12 +4489,12 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -4389,6 +4502,9 @@ "ext-json": "*", "ext-redis": "*" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "platform-overrides": { + "php": "8.0" + }, + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yml b/docker-compose.yml index f7624f2..cd4f603 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,8 @@ -version: '3' - services: - web: - build: . - ports: - - "9020:80" - volumes: - - ./tests:/usr/share/nginx/html/tests - - ./src:/usr/share/nginx/html/src \ No newline at end of file + web: + build: . + ports: + - "9020:80" + volumes: + - ./tests:/usr/share/nginx/html/tests + - ./src:/usr/share/nginx/html/src \ No newline at end of file diff --git a/src/Platform/Action.php b/src/Platform/Action.php index aeac0b7..1f27834 100644 --- a/src/Platform/Action.php +++ b/src/Platform/Action.php @@ -41,16 +41,34 @@ abstract class Action protected ?string $desc = null; + /** + * @var array + */ protected array $groups = []; + /** + * @var callable + */ protected $callback; + /** + * @var array + */ protected array $options = []; + /** + * @var array> + */ protected array $params = []; + /** + * @var array + */ protected array $injections = []; + /** + * @var array + */ protected array $labels = []; protected string $type = self::TYPE_DEFAULT; @@ -104,7 +122,7 @@ public function desc(string $description): self /** * Get the value of groups * - * @return array + * @return array */ public function getGroups(): array { @@ -114,7 +132,7 @@ public function getGroups(): array /** * Set Groups * - * @param array $groups + * @param array $groups * @return self */ public function groups(array $groups): self @@ -150,7 +168,7 @@ public function callback(mixed $callback): self /** * Get the value of params * - * @return array + * @return array> */ public function getParams(): array { @@ -160,19 +178,30 @@ public function getParams(): array /** * Set Param * - * @param string $key - * @param mixed $default - * @param Validator|callable $validator - * @param string $description - * @param bool $optional - * @param array $injections - * @param bool $skipValidation - * @param bool $deprecated - * @param string $example + * @param string $key + * @param mixed $default + * @param Validator|callable $validator + * @param string $description + * @param bool $optional + * @param array $injections + * @param bool $skipValidation + * @param bool $deprecated + * @param string $example + * @param string|null $model * @return self */ - public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = [], bool $skipValidation = false, bool $deprecated = false, string $example = ''): self - { + public function param( + string $key, + mixed $default, + Validator|callable $validator, + string $description = '', + bool $optional = false, + array $injections = [], + bool $skipValidation = false, + bool $deprecated = false, + string $example = '', + ?string $model = null + ): self { $param = [ 'default' => $default, 'validator' => $validator, @@ -180,8 +209,9 @@ public function param(string $key, mixed $default, Validator|callable $validator 'optional' => $optional, 'injections' => $injections, 'skipValidation' => $skipValidation, - 'deprecated' => $deprecated, // TODO: @Meldiron implement tests + 'deprecated' => $deprecated, 'example' => $example, + 'model' => $model, ]; $this->options['param:'.$key] = array_merge($param, ['type' => 'param']); $this->params[$key] = $param; @@ -192,7 +222,7 @@ public function param(string $key, mixed $default, Validator|callable $validator /** * Get the value of injections * - * @return array + * @return array */ public function getInjections(): array { @@ -225,7 +255,7 @@ public function inject(string $injection): self /** * Get the value of labels * - * @return array + * @return array */ public function getLabels(): array { @@ -249,7 +279,7 @@ public function label(string $key, mixed $value): self /** * Get Http Options * - * @return array + * @return array */ public function getOptions(): array { diff --git a/src/Platform/Module.php b/src/Platform/Module.php index f2d8fbd..007a5b2 100644 --- a/src/Platform/Module.php +++ b/src/Platform/Module.php @@ -6,6 +6,9 @@ abstract class Module { + /** + * @var array> + */ protected array $services = [ 'all' => [], Service::TYPE_TASK => [], @@ -19,7 +22,7 @@ abstract class Module * * @param string $key * @param Service $service - * @return Platform + * @return self */ public function addService(string $key, Service $service): self { @@ -33,7 +36,7 @@ public function addService(string $key, Service $service): self * Remove Service * * @param string $key - * @return Platform + * @return self */ public function removeService(string $key): self { @@ -51,8 +54,9 @@ public function removeService(string $key): self /** * Get Service * - * @param string $key + * @param string $key * @return Service|null + * @throws Exception */ public function getService(string $key): ?Service { @@ -67,7 +71,7 @@ public function getService(string $key): ?Service /** * Get Services * - * @return array + * @return array */ public function getServices(): array { @@ -78,7 +82,7 @@ public function getServices(): array * Get services by type * * @param string $type - * @return array + * @return array */ public function getServicesByType(string $type): array { diff --git a/src/Platform/Platform.php b/src/Platform/Platform.php index 5dab41c..41a2649 100644 --- a/src/Platform/Platform.php +++ b/src/Platform/Platform.php @@ -33,6 +33,7 @@ public function __construct(Module $module) /** * Initialize Application * + * @param array $params * @return void */ public function init(string $type, array $params = []): void @@ -71,7 +72,7 @@ public function init(string $type, array $params = []): void /** * Init HTTP service * - * @param Service $service + * @param array $services * @return void */ protected function initHttp(array $services): void @@ -94,7 +95,12 @@ protected function initHttp(array $services): void break; case Action::TYPE_DEFAULT: default: - $hook = App::addRoute($action->getHttpMethod(), $action->getHttpPath()); + $httpMethod = $action->getHttpMethod(); + $httpPath = $action->getHttpPath(); + if ($httpMethod === null || $httpPath === null) { + throw new Exception('HTTP method and path must be set for default actions'); + } + $hook = App::addRoute($httpMethod, $httpPath); break; } @@ -111,8 +117,19 @@ protected function initHttp(array $services): void foreach ($action->getOptions() as $key => $option) { switch ($option['type']) { case 'param': - $key = substr($key, stripos($key, ':') + 1); - $hook->param($key, $option['default'], $option['validator'], $option['description'], $option['optional'], $option['injections'], $option['skipValidation'], $option['deprecated'], $option['example']); + $key = \substr($key, \stripos($key, ':') + 1); + $hook->param( + $key, + $option['default'], + $option['validator'], + $option['description'], + $option['optional'], + $option['injections'], + $option['skipValidation'], + $option['deprecated'], + $option['example'], + $option['model'], + ); break; case 'injection': $hook->inject($option['name']); @@ -132,6 +149,7 @@ protected function initHttp(array $services): void /** * Init CLI Services * + * @param array $services * @return void */ protected function initTasks(array $services): void @@ -182,7 +200,7 @@ protected function initTasks(array $services): void /** * Init worker Services * - * @param array $params + * @param array $services * @return void */ protected function initWorker(array $services, string $workerName): void @@ -299,7 +317,7 @@ public function getService(string $key): ?Service /** * Get Services * - * @return array + * @return array */ public function getServices(): array { @@ -351,7 +369,7 @@ public function setWorker(Server $worker): self * @param string|null $default * @return mixed */ - public function getEnv(string $key, string $default = null): mixed + public function getEnv(string $key, ?string $default = null): mixed { return $_SERVER[$key] ?? $default; } diff --git a/src/Platform/Scope/HTTP.php b/src/Platform/Scope/HTTP.php index af22c01..8b7c350 100644 --- a/src/Platform/Scope/HTTP.php +++ b/src/Platform/Scope/HTTP.php @@ -10,6 +10,9 @@ trait HTTP protected ?string $httpAliasPath = null; + /** + * @var array + */ protected array $httpAliasParams = []; /** @@ -41,9 +44,9 @@ public function setHttpMethod(string $method): self /** * Get httpPath * - * @return string + * @return string|null */ - public function getHttpPath(): string + public function getHttpPath(): ?string { return $this->httpPath; } @@ -61,7 +64,7 @@ public function getHttpAliasPath(): ?string /** * Get the value of httpAliasParams * - * @return array + * @return array */ public function getHttpAliasParams(): array { @@ -71,9 +74,9 @@ public function getHttpAliasParams(): array /** * Get the value of httpMethod * - * @return string + * @return string|null */ - public function getHttpMethod(): string + public function getHttpMethod(): ?string { return $this->httpMethod; } @@ -82,7 +85,7 @@ public function getHttpMethod(): string * Set httpAlias path and params * * @param string $path - * @param array $params + * @param array $params * @return self */ public function httpAlias(string $path, array $params = []): self diff --git a/src/Platform/Service.php b/src/Platform/Service.php index 7fab9d6..852ef8c 100644 --- a/src/Platform/Service.php +++ b/src/Platform/Service.php @@ -12,6 +12,9 @@ abstract class Service public const TYPE_WORKER = 'Worker'; + /** + * @var array + */ protected array $actions; protected string $type; @@ -80,7 +83,7 @@ public function getAction(string $key): ?Action /** * Get Actions * - * @return array + * @return array */ public function getActions(): array { diff --git a/tests/Platform/TestActionCLI.php b/tests/Platform/TestActionCLI.php index 5248451..31753d7 100644 --- a/tests/Platform/TestActionCLI.php +++ b/tests/Platform/TestActionCLI.php @@ -18,7 +18,11 @@ public function __construct() }); } - public function action($email, $list) + /** + * @param mixed $email + * @param mixed $list + */ + public function action($email, $list): void { echo $email.'-'.implode('-', $list); } diff --git a/tests/Platform/TestActionChunked.php b/tests/Platform/TestActionChunked.php index 2a44b27..71eced1 100644 --- a/tests/Platform/TestActionChunked.php +++ b/tests/Platform/TestActionChunked.php @@ -16,7 +16,10 @@ public function __construct() }); } - public function action($response) + /** + * @param mixed $response + */ + public function action($response): void { foreach (['Hello ', 'World!'] as $key => $word) { $response->chunk($word, $key == 1); diff --git a/tests/Platform/TestActionInit.php b/tests/Platform/TestActionInit.php index 79175c2..f1c49e6 100644 --- a/tests/Platform/TestActionInit.php +++ b/tests/Platform/TestActionInit.php @@ -17,7 +17,7 @@ public function __construct() }); } - public function action(Response $response) + public function action(Response $response): void { $response->addHeader('x-init', 'init-called'); } diff --git a/tests/Platform/TestActionRedirect.php b/tests/Platform/TestActionRedirect.php index 3066ce9..e782638 100644 --- a/tests/Platform/TestActionRedirect.php +++ b/tests/Platform/TestActionRedirect.php @@ -16,7 +16,10 @@ public function __construct() }); } - public function action($response) + /** + * @param mixed $response + */ + public function action($response): void { $response->redirect('/'); } diff --git a/tests/Platform/TestActionRoot.php b/tests/Platform/TestActionRoot.php index d50a6cf..08cacee 100644 --- a/tests/Platform/TestActionRoot.php +++ b/tests/Platform/TestActionRoot.php @@ -17,7 +17,10 @@ public function __construct() }); } - public function action($response) + /** + * @param mixed $response + */ + public function action($response): void { $response->send('Hello World!'); } diff --git a/tests/Platform/TestActionWithModel.php b/tests/Platform/TestActionWithModel.php new file mode 100644 index 0000000..cd5e54d --- /dev/null +++ b/tests/Platform/TestActionWithModel.php @@ -0,0 +1,87 @@ +httpPath = '/model-test'; + $this->httpMethod = 'POST'; + $this->groups(['test']); + + $this + ->desc('Test action with model parameter') + ->param( + key: 'user', + default: null, + validator: new Text(5000), + description: 'User object in JSON format', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: '{"name": "John Doe", "email": "john@example.com"}', + model: 'user' + ) + ->param( + key: 'settings', + default: null, + validator: new Text(2000), + description: 'User settings object', + optional: true, + injections: [], + skipValidation: false, + deprecated: false, + example: '{"theme": "dark", "notifications": true}', + model: 'userSettings' + ) + ->param( + key: 'legacyData', + default: null, + validator: new Text(1000), + description: 'Legacy data format (deprecated)', + optional: true, + injections: [], + skipValidation: true, + deprecated: true, + example: '{"old_format": true}', + model: 'legacyData' + ) + ->param( + key: 'simpleField', + default: '', + validator: new Text(100), + description: 'A simple text field without model' + ) + ->inject('response') + ->callback(function ($user, $settings, $legacyData, $simpleField, $response) { + $this->action($user, $settings, $legacyData, $simpleField, $response); + }); + } + + /** + * @param mixed $user + * @param mixed $settings + * @param mixed $legacyData + * @param mixed $simpleField + * @param mixed $response + */ + public function action($user, $settings, $legacyData, $simpleField, $response): void + { + $response->json([ + 'success' => true, + 'user' => $user, + 'settings' => $settings, + 'legacyData' => $legacyData, + 'simpleField' => $simpleField, + ]); + } +} diff --git a/tests/e2e/CLITest.php b/tests/e2e/CLITest.php index 370b510..01f4f51 100644 --- a/tests/e2e/CLITest.php +++ b/tests/e2e/CLITest.php @@ -16,7 +16,7 @@ public function tearDown(): void { } - public function testCLISetup() + public function testCLISetup(): void { ob_start(); diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php index 335ddeb..63f8455 100644 --- a/tests/e2e/Client.php +++ b/tests/e2e/Client.php @@ -45,9 +45,9 @@ public function __construct() * * @param string $method * @param string $path - * @param array $params - * @param array $headers - * @return array|string + * @param array $headers + * @param array $params + * @return array|string * * @throws Exception */ @@ -60,11 +60,12 @@ public function call(string $method, string $path = '', array $headers = [], arr $responseType = ''; $responseBody = ''; + /** @phpstan-ignore-next-line */ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_HTTPHEADER, array_values($headers)); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { diff --git a/tests/e2e/HTTPServicesTest.php b/tests/e2e/HTTPServicesTest.php index 7cdc273..cf1cf37 100644 --- a/tests/e2e/HTTPServicesTest.php +++ b/tests/e2e/HTTPServicesTest.php @@ -13,7 +13,6 @@ public function setUp(): void public function tearDown(): void { - $this->client = null; } /** @@ -21,32 +20,39 @@ public function tearDown(): void */ protected $client; - public function testRootAction() + public function testRootAction(): void { $response = $this->client->call(Client::METHOD_GET, '/'); + $this->assertIsArray($response); $this->assertEquals('Hello World!', $response['body']); } - public function testChunkedAction() + public function testChunkedAction(): void { $response = $this->client->call(Client::METHOD_GET, '/chunked'); + $this->assertIsArray($response); $this->assertEquals('Hello World!', $response['body']); } - public function testRedirectAction() + public function testRedirectAction(): void { $response = $this->client->call(Client::METHOD_GET, '/redirect'); + $this->assertIsArray($response); $this->assertEquals('Hello World!', $response['body']); } - public function testHook() + public function testHook(): void { $response = $this->client->call(Client::METHOD_GET, '/'); + $this->assertIsArray($response); $this->assertEquals('Hello World!', $response['body']); + $this->assertIsArray($response['headers']); $this->assertEquals('init-called', $response['headers']['x-init']); $response = $this->client->call(Client::METHOD_GET, '/chunked'); + $this->assertIsArray($response); $this->assertEquals('Hello World!', $response['body']); + $this->assertIsArray($response['headers']); $this->assertEquals('', ($response['headers']['x-init'] ?? '')); } } diff --git a/tests/e2e/server.php b/tests/e2e/server.php index 0663acc..8a5e291 100644 --- a/tests/e2e/server.php +++ b/tests/e2e/server.php @@ -8,9 +8,9 @@ use Utopia\Tests\TestPlatform; ini_set('memory_limit', '512M'); -ini_set('display_errors', 1); -ini_set('display_startup_errors', 1); -ini_set('display_socket_timeout', -1); +ini_set('display_errors', '1'); +ini_set('display_startup_errors', '1'); +ini_set('display_socket_timeout', '-1'); error_reporting(E_ALL); $platform = new TestPlatform(); diff --git a/tests/unit/ActionTest.php b/tests/unit/ActionTest.php new file mode 100644 index 0000000..22c4aa4 --- /dev/null +++ b/tests/unit/ActionTest.php @@ -0,0 +1,650 @@ +param( + key: 'userId', + default: null, + validator: new Text(100), + description: 'User ID', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'user_123', + model: 'user' + ); + } + }; + + $params = $action->getParams(); + + $this->assertArrayHasKey('userId', $params); + $this->assertEquals('user', $params['userId']['model']); + } + + /** + * Test that model parameter defaults to null when not provided + */ + public function testParamModelDefaultsToNull(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'name', + default: '', + validator: new Text(100), + description: 'Name field' + ); + } + }; + + $params = $action->getParams(); + + $this->assertArrayHasKey('name', $params); + $this->assertArrayHasKey('model', $params['name']); + $this->assertNull($params['name']['model']); + } + + /** + * Test that model is included in options array + */ + public function testParamModelInOptions(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'document', + default: null, + validator: new Text(1000), + description: 'Document object', + optional: true, + injections: [], + skipValidation: false, + deprecated: false, + example: '{"key": "value"}', + model: 'document' + ); + } + }; + + $options = $action->getOptions(); + + $this->assertArrayHasKey('param:document', $options); + $this->assertEquals('document', $options['param:document']['model']); + $this->assertEquals('param', $options['param:document']['type']); + } + + /** + * Test multiple params with different models + */ + public function testMultipleParamsWithDifferentModels(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'user', + default: null, + validator: new Text(100), + description: 'User object', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: '{}', + model: 'user' + ) + ->param( + key: 'project', + default: null, + validator: new Text(100), + description: 'Project object', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: '{}', + model: 'project' + ) + ->param( + key: 'simpleField', + default: '', + validator: new Text(50), + description: 'Simple text field' + ); + } + }; + + $params = $action->getParams(); + + $this->assertEquals('user', $params['user']['model']); + $this->assertEquals('project', $params['project']['model']); + $this->assertNull($params['simpleField']['model']); + } + + /** + * Test skipValidation parameter is stored correctly + */ + public function testParamSkipValidation(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'skipField', + default: null, + validator: new Text(100), + description: 'Field with skipped validation', + optional: false, + injections: [], + skipValidation: true + ) + ->param( + key: 'normalField', + default: null, + validator: new Text(100), + description: 'Field with normal validation', + optional: false, + injections: [], + skipValidation: false + ); + } + }; + + $params = $action->getParams(); + + $this->assertTrue($params['skipField']['skipValidation']); + $this->assertFalse($params['normalField']['skipValidation']); + } + + /** + * Test skipValidation defaults to false + */ + public function testParamSkipValidationDefaultsFalse(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'defaultField', + default: '', + validator: new Text(100), + description: 'Field with default skipValidation' + ); + } + }; + + $params = $action->getParams(); + + $this->assertArrayHasKey('skipValidation', $params['defaultField']); + $this->assertFalse($params['defaultField']['skipValidation']); + } + + /** + * Test deprecated parameter is stored correctly + */ + public function testParamDeprecated(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'oldField', + default: null, + validator: new Text(100), + description: 'Deprecated field', + optional: true, + injections: [], + skipValidation: false, + deprecated: true + ) + ->param( + key: 'newField', + default: null, + validator: new Text(100), + description: 'New field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false + ); + } + }; + + $params = $action->getParams(); + + $this->assertTrue($params['oldField']['deprecated']); + $this->assertFalse($params['newField']['deprecated']); + } + + /** + * Test deprecated defaults to false + */ + public function testParamDeprecatedDefaultsFalse(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'normalField', + default: '', + validator: new Text(100), + description: 'Normal field' + ); + } + }; + + $params = $action->getParams(); + + $this->assertArrayHasKey('deprecated', $params['normalField']); + $this->assertFalse($params['normalField']['deprecated']); + } + + /** + * Test example parameter is stored correctly + */ + public function testParamExample(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'email', + default: null, + validator: new Text(255), + description: 'User email address', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'user@example.com' + ); + } + }; + + $params = $action->getParams(); + + $this->assertEquals('user@example.com', $params['email']['example']); + } + + /** + * Test example defaults to empty string + */ + public function testParamExampleDefaultsToEmptyString(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'field', + default: '', + validator: new Text(100), + description: 'Field without example' + ); + } + }; + + $params = $action->getParams(); + + $this->assertArrayHasKey('example', $params['field']); + $this->assertEquals('', $params['field']['example']); + } + + /** + * Test example with various types of content + */ + public function testParamExampleWithVariousContent(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'jsonField', + default: null, + validator: new Text(1000), + description: 'JSON field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: '{"name": "John", "age": 30}' + ) + ->param( + key: 'urlField', + default: null, + validator: new Text(500), + description: 'URL field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'https://example.com/path?query=value' + ) + ->param( + key: 'numericString', + default: null, + validator: new Text(20), + description: 'Numeric string field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: '12345' + ); + } + }; + + $params = $action->getParams(); + + $this->assertEquals('{"name": "John", "age": 30}', $params['jsonField']['example']); + $this->assertEquals('https://example.com/path?query=value', $params['urlField']['example']); + $this->assertEquals('12345', $params['numericString']['example']); + } + + /** + * Test all new parameters together + */ + public function testAllNewParamsTogether(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'complexParam', + default: null, + validator: new Text(1000), + description: 'A complex parameter with all options', + optional: true, + injections: [], + skipValidation: true, + deprecated: true, + example: '{"id": "123", "data": "test"}', + model: 'complexModel' + ); + } + }; + + $params = $action->getParams(); + $options = $action->getOptions(); + + // Check params array + $this->assertArrayHasKey('complexParam', $params); + $this->assertEquals('complexModel', $params['complexParam']['model']); + $this->assertTrue($params['complexParam']['skipValidation']); + $this->assertTrue($params['complexParam']['deprecated']); + $this->assertEquals('{"id": "123", "data": "test"}', $params['complexParam']['example']); + $this->assertTrue($params['complexParam']['optional']); + $this->assertEquals('A complex parameter with all options', $params['complexParam']['description']); + + // Check options array + $this->assertArrayHasKey('param:complexParam', $options); + $this->assertEquals('complexModel', $options['param:complexParam']['model']); + $this->assertTrue($options['param:complexParam']['skipValidation']); + $this->assertTrue($options['param:complexParam']['deprecated']); + $this->assertEquals('{"id": "123", "data": "test"}', $options['param:complexParam']['example']); + $this->assertEquals('param', $options['param:complexParam']['type']); + } + + /** + * Test param method returns self for chaining + */ + public function testParamReturnsSelfForChaining(): void + { + $action = new class () extends Action { + public function __construct() + { + $result = $this + ->param( + key: 'first', + default: null, + validator: new Text(100), + description: 'First param', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'example1', + model: 'model1' + ); + + // Store reference for testing + $this->chainResult = $result; + } + + /** + * @var Action + */ + public $chainResult; + }; + + $this->assertInstanceOf(Action::class, $action->chainResult); + $this->assertSame($action, $action->chainResult); + } + + /** + * Test model parameter with empty string + */ + public function testParamModelWithEmptyString(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'emptyModelField', + default: null, + validator: new Text(100), + description: 'Field with empty model string', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'test', + model: '' + ); + } + }; + + $params = $action->getParams(); + + $this->assertEquals('', $params['emptyModelField']['model']); + } + + /** + * Test options contain all param properties + */ + public function testOptionsContainAllParamProperties(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'testParam', + default: 'defaultValue', + validator: new Text(100), + description: 'Test description', + optional: true, + injections: ['request'], + skipValidation: true, + deprecated: true, + example: 'exampleValue', + model: 'testModel' + ); + } + }; + + $options = $action->getOptions(); + $paramOptions = $options['param:testParam']; + + $this->assertEquals('defaultValue', $paramOptions['default']); + $this->assertInstanceOf(Text::class, $paramOptions['validator']); + $this->assertEquals('Test description', $paramOptions['description']); + $this->assertTrue($paramOptions['optional']); + $this->assertEquals(['request'], $paramOptions['injections']); + $this->assertTrue($paramOptions['skipValidation']); + $this->assertTrue($paramOptions['deprecated']); + $this->assertEquals('exampleValue', $paramOptions['example']); + $this->assertEquals('testModel', $paramOptions['model']); + $this->assertEquals('param', $paramOptions['type']); + } + + /** + * Test params and options consistency + */ + public function testParamsAndOptionsConsistency(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'consistencyTest', + default: null, + validator: new Text(50), + description: 'Consistency test field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'consistent', + model: 'consistentModel' + ); + } + }; + + $params = $action->getParams(); + $options = $action->getOptions(); + + // All param values should match between params and options (except 'type' which is only in options) + $this->assertEquals($params['consistencyTest']['model'], $options['param:consistencyTest']['model']); + $this->assertEquals($params['consistencyTest']['skipValidation'], $options['param:consistencyTest']['skipValidation']); + $this->assertEquals($params['consistencyTest']['deprecated'], $options['param:consistencyTest']['deprecated']); + $this->assertEquals($params['consistencyTest']['example'], $options['param:consistencyTest']['example']); + $this->assertEquals($params['consistencyTest']['default'], $options['param:consistencyTest']['default']); + $this->assertEquals($params['consistencyTest']['description'], $options['param:consistencyTest']['description']); + $this->assertEquals($params['consistencyTest']['optional'], $options['param:consistencyTest']['optional']); + $this->assertEquals($params['consistencyTest']['injections'], $options['param:consistencyTest']['injections']); + } + + /** + * Test model with special characters in name + */ + public function testModelWithSpecialCharacters(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'specialModelField', + default: null, + validator: new Text(100), + description: 'Field with special model name', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'test', + model: 'model.with.dots' + ) + ->param( + key: 'anotherSpecialField', + default: null, + validator: new Text(100), + description: 'Another field', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'test', + model: 'model-with-dashes' + ); + } + }; + + $params = $action->getParams(); + + $this->assertEquals('model.with.dots', $params['specialModelField']['model']); + $this->assertEquals('model-with-dashes', $params['anotherSpecialField']['model']); + } + + /** + * Test that overwriting a param with same key updates all values + */ + public function testOverwritingParamUpdatesAllValues(): void + { + $action = new class () extends Action { + public function __construct() + { + $this + ->param( + key: 'overwriteTest', + default: 'first', + validator: new Text(50), + description: 'First definition', + optional: false, + injections: [], + skipValidation: false, + deprecated: false, + example: 'first_example', + model: 'firstModel' + ) + ->param( + key: 'overwriteTest', + default: 'second', + validator: new Text(100), + description: 'Second definition', + optional: true, + injections: ['response'], + skipValidation: true, + deprecated: true, + example: 'second_example', + model: 'secondModel' + ); + } + }; + + $params = $action->getParams(); + $options = $action->getOptions(); + + // Should have the second definition's values + $this->assertEquals('second', $params['overwriteTest']['default']); + $this->assertEquals('Second definition', $params['overwriteTest']['description']); + $this->assertTrue($params['overwriteTest']['optional']); + $this->assertEquals(['response'], $params['overwriteTest']['injections']); + $this->assertTrue($params['overwriteTest']['skipValidation']); + $this->assertTrue($params['overwriteTest']['deprecated']); + $this->assertEquals('second_example', $params['overwriteTest']['example']); + $this->assertEquals('secondModel', $params['overwriteTest']['model']); + } +} diff --git a/tests/unit/GetEnvTest.php b/tests/unit/GetEnvTest.php index 0eb6059..f0a86e4 100644 --- a/tests/unit/GetEnvTest.php +++ b/tests/unit/GetEnvTest.php @@ -6,7 +6,7 @@ class GetEnvTest extends TestCase { - public function testGetEnv() + public function testGetEnv(): void { $platform = new Mock(); $this->assertEquals(3, $platform->getEnv('argc'));