diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 0d97b2bc..43625f99 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -12,5 +12,5 @@ jobs:
- name: Run Linter
run: |
- docker run --rm -v $PWD:/app composer sh -c \
+ docker run --rm -v $PWD:/app composer:2.6 sh -c \
"composer install --profile --ignore-platform-reqs && git config --global --add safe.directory /app && composer bench -- --progress=plain"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 126ef740..c6e9f877 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -12,6 +12,6 @@ jobs:
- name: Run CodeQL
run: |
- docker run --rm -v $PWD:/app composer sh -c \
+ docker run --rm -v $PWD:/app composer:2.8 sh -c \
"composer install --profile --ignore-platform-reqs && composer check"
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a6f1e642..6c84b7f1 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,11 +24,14 @@ jobs:
- name: Wait for Server to be ready
run: sleep 10
+ - name: Run unit Tests
+ run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml --testsuite=unit
+
- name: Run FPM Tests
- run: docker compose exec fpm vendor/bin/phpunit --configuration phpunit.xml
+ run: docker compose exec fpm vendor/bin/phpunit --configuration phpunit.xml --group=fpm
- name: Run Swoole Tests
- run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml
+ run: docker compose exec swoole vendor/bin/phpunit --configuration phpunit.xml --group=swoole
- name: Run Swoole Corotuine Tests
- run: docker compose exec swoole-coroutine vendor/bin/phpunit --configuration phpunit.xml
+ run: docker compose exec swoole-coroutine vendor/bin/phpunit --configuration phpunit.xml --group=swoole-coroutine
diff --git a/Dockerfile.fpm b/Dockerfile.fpm
index 54c948a6..c6299534 100644
--- a/Dockerfile.fpm
+++ b/Dockerfile.fpm
@@ -1,4 +1,4 @@
-FROM composer:2.0 AS step0
+FROM composer:2.8 AS step0
ARG TESTING=true
@@ -13,11 +13,11 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
-FROM php:8.0-cli-alpine as final
+FROM php:8.3-cli-alpine AS final
LABEL maintainer="team@appwrite.io"
ENV DEBIAN_FRONTEND=noninteractive \
- PHP_VERSION=8
+ PHP_VERSION=82
RUN \
apk add --no-cache --virtual .deps \
diff --git a/Dockerfile.swoole b/Dockerfile.swoole
index a930c97a..f4c442b1 100644
--- a/Dockerfile.swoole
+++ b/Dockerfile.swoole
@@ -1,4 +1,4 @@
-FROM composer:2.0 AS step0
+FROM composer:2.8 AS step0
ARG TESTING=true
ARG DEBUG=false
@@ -14,7 +14,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
-FROM appwrite/base:0.9.0 as final
+FROM appwrite/base:0.11.3 AS final
ARG TESTING=true
ARG DEBUG=false
diff --git a/Dockerfile.swoole_coroutines b/Dockerfile.swoole_coroutines
index a02852da..f8b56432 100644
--- a/Dockerfile.swoole_coroutines
+++ b/Dockerfile.swoole_coroutines
@@ -1,4 +1,4 @@
-FROM composer:2.0 AS step0
+FROM composer:2.8 AS step0
ARG TESTING=true
ARG DEBUG=false
@@ -14,7 +14,7 @@ RUN composer install --ignore-platform-reqs --optimize-autoloader \
--no-plugins --no-scripts --prefer-dist \
`if [ "$TESTING" != "true" ]; then echo "--no-dev"; fi`
-FROM appwrite/base:0.9.0 as final
+FROM appwrite/base:0.11.3 AS final
ARG TESTING=true
ARG DEBUG=false
diff --git a/README.md b/README.md
index e657986d..9487d581 100644
--- a/README.md
+++ b/README.md
@@ -275,7 +275,7 @@ To learn more about architecture and features for this library, check out more i
## System Requirements
-Utopia HTTP requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible.
+Utopia Framework requires PHP 8.1 or later. We recommend using the latest PHP version whenever possible.
## More from Utopia
diff --git a/composer.json b/composer.json
index 89bd5e54..1055fc31 100644
--- a/composer.json
+++ b/composer.json
@@ -23,21 +23,29 @@
"scripts": {
"lint": "vendor/bin/pint --test",
"format": "vendor/bin/pint",
- "check": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=256M",
+ "check": "vendor/bin/phpstan analyse -c phpstan.neon --memory-limit 512M",
"test": "vendor/bin/phpunit --configuration phpunit.xml",
"bench": "vendor/bin/phpbench run --report=benchmark"
},
"require": {
- "php": ">=8.0",
+ "php": ">=8.1",
"ext-swoole": "*",
- "utopia-php/servers": "0.1.*"
+ "utopia-php/servers": "0.1.*",
+ "utopia-php/compression": "0.1.*",
+ "utopia-php/telemetry": "0.1.*"
},
"require-dev": {
"ext-xdebug": "*",
"phpunit/phpunit": "^9.5.25",
"swoole/ide-helper": "4.8.3",
- "phpbench/phpbench": "^1.2",
- "laravel/pint": "1.*",
- "phpstan/phpstan": "1.*"
+ "laravel/pint": "^1.2",
+ "phpstan/phpstan": "1.*",
+ "phpbench/phpbench": "^1.2"
+ },
+ "config": {
+ "allow-plugins": {
+ "php-http/discovery": false,
+ "tbachert/spi": false
+ }
}
}
diff --git a/composer.lock b/composer.lock
index c2dd659d..bb426596 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,1910 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "245e0fcfb38d14be42162b53fcb59c7e",
+ "content-hash": "11b6587ef7f59f67e889d3869763a726",
"packages": [
+ {
+ "name": "brick/math",
+ "version": "0.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/math.git",
+ "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
+ "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpstan/phpstan": "2.1.22",
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Arbitrary-precision arithmetic library",
+ "keywords": [
+ "Arbitrary-precision",
+ "BigInteger",
+ "BigRational",
+ "arithmetic",
+ "bigdecimal",
+ "bignum",
+ "bignumber",
+ "brick",
+ "decimal",
+ "integer",
+ "math",
+ "mathematics",
+ "rational"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/math/issues",
+ "source": "https://github.com/brick/math/tree/0.14.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-29T12:40:03+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.4.4"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-20T19:15:30+00:00"
+ },
+ {
+ "name": "google/protobuf",
+ "version": "v4.32.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/protocolbuffers/protobuf-php.git",
+ "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb",
+ "reference": "c4ed1c1f9bbc1e91766e2cd6c0af749324fe87cb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": ">=5.0.0 <8.5.27"
+ },
+ "suggest": {
+ "ext-bcmath": "Need to support JSON deserialization"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Google\\Protobuf\\": "src/Google/Protobuf",
+ "GPBMetadata\\Google\\Protobuf\\": "src/GPBMetadata/Google/Protobuf"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "proto library for PHP",
+ "homepage": "https://developers.google.com/protocol-buffers/",
+ "keywords": [
+ "proto"
+ ],
+ "support": {
+ "source": "https://github.com/protocolbuffers/protobuf-php/tree/v4.32.1"
+ },
+ "time": "2025-09-14T05:14:52+00:00"
+ },
+ {
+ "name": "nyholm/psr7",
+ "version": "1.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7.git",
+ "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
+ "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "php-http/message-factory-implementation": "1.0",
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "http-interop/http-factory-tests": "^0.9",
+ "php-http/message-factory": "^1.0",
+ "php-http/psr7-integration-tests": "^1.0",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
+ "symfony/error-handler": "^4.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "A fast PHP7 implementation of PSR-7",
+ "homepage": "https://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7/issues",
+ "source": "https://github.com/Nyholm/psr7/tree/1.8.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-09-09T07:06:30+00:00"
+ },
+ {
+ "name": "nyholm/psr7-server",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nyholm/psr7-server.git",
+ "reference": "4335801d851f554ca43fa6e7d2602141538854dc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nyholm/psr7-server/zipball/4335801d851f554ca43fa6e7d2602141538854dc",
+ "reference": "4335801d851f554ca43fa6e7d2602141538854dc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "nyholm/nsa": "^1.1",
+ "nyholm/psr7": "^1.3",
+ "phpunit/phpunit": "^7.0 || ^8.5 || ^9.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Nyholm\\Psr7Server\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ },
+ {
+ "name": "Martijn van der Ven",
+ "email": "martijn@vanderven.se"
+ }
+ ],
+ "description": "Helper classes to handle PSR-7 server requests",
+ "homepage": "http://tnyholm.se",
+ "keywords": [
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/Nyholm/psr7-server/issues",
+ "source": "https://github.com/Nyholm/psr7-server/tree/1.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Zegnat",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nyholm",
+ "type": "github"
+ }
+ ],
+ "time": "2023-11-08T09:30:43+00:00"
+ },
+ {
+ "name": "open-telemetry/api",
+ "version": "1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/api.git",
+ "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/610b79ad9d6d97e8368bcb6c4d42394fbb87b522",
+ "reference": "610b79ad9d6d97e8368bcb6c4d42394fbb87b522",
+ "shasum": ""
+ },
+ "require": {
+ "open-telemetry/context": "^1.4",
+ "php": "^8.1",
+ "psr/log": "^1.1|^2.0|^3.0",
+ "symfony/polyfill-php82": "^1.26"
+ },
+ "conflict": {
+ "open-telemetry/sdk": "<=1.0.8"
+ },
+ "type": "library",
+ "extra": {
+ "spi": {
+ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [
+ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "1.7.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "Trace/functions.php"
+ ],
+ "psr-4": {
+ "OpenTelemetry\\API\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "API for OpenTelemetry PHP.",
+ "keywords": [
+ "Metrics",
+ "api",
+ "apm",
+ "logging",
+ "opentelemetry",
+ "otel",
+ "tracing"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/php",
+ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
+ "source": "https://github.com/open-telemetry/opentelemetry-php"
+ },
+ "time": "2025-10-02T23:44:28+00:00"
+ },
+ {
+ "name": "open-telemetry/context",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/context.git",
+ "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/d4c4470b541ce72000d18c339cfee633e4c8e0cf",
+ "reference": "d4c4470b541ce72000d18c339cfee633e4c8e0cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "symfony/polyfill-php82": "^1.26"
+ },
+ "suggest": {
+ "ext-ffi": "To allow context switching in Fibers"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "fiber/initialize_fiber_handler.php"
+ ],
+ "psr-4": {
+ "OpenTelemetry\\Context\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "Context implementation for OpenTelemetry PHP.",
+ "keywords": [
+ "Context",
+ "opentelemetry",
+ "otel"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/php",
+ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
+ "source": "https://github.com/open-telemetry/opentelemetry-php"
+ },
+ "time": "2025-09-19T00:05:49+00:00"
+ },
+ {
+ "name": "open-telemetry/exporter-otlp",
+ "version": "1.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/exporter-otlp.git",
+ "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
+ "reference": "196f3a1dbce3b2c0f8110d164232c11ac00ddbb2",
+ "shasum": ""
+ },
+ "require": {
+ "open-telemetry/api": "^1.0",
+ "open-telemetry/gen-otlp-protobuf": "^1.1",
+ "open-telemetry/sdk": "^1.0",
+ "php": "^8.1",
+ "php-http/discovery": "^1.14"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "_register.php"
+ ],
+ "psr-4": {
+ "OpenTelemetry\\Contrib\\Otlp\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "OTLP exporter for OpenTelemetry.",
+ "keywords": [
+ "Metrics",
+ "exporter",
+ "gRPC",
+ "http",
+ "opentelemetry",
+ "otel",
+ "otlp",
+ "tracing"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/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"
+ },
+ {
+ "name": "open-telemetry/gen-otlp-protobuf",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git",
+ "reference": "673af5b06545b513466081884b47ef15a536edde"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/673af5b06545b513466081884b47ef15a536edde",
+ "reference": "673af5b06545b513466081884b47ef15a536edde",
+ "shasum": ""
+ },
+ "require": {
+ "google/protobuf": "^3.22 || ^4.0",
+ "php": "^8.0"
+ },
+ "suggest": {
+ "ext-protobuf": "For better performance, when dealing with the protobuf format"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Opentelemetry\\Proto\\": "Opentelemetry/Proto/",
+ "GPBMetadata\\Opentelemetry\\": "GPBMetadata/Opentelemetry/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "PHP protobuf files for communication with OpenTelemetry OTLP collectors/servers.",
+ "keywords": [
+ "Metrics",
+ "apm",
+ "gRPC",
+ "logging",
+ "opentelemetry",
+ "otel",
+ "otlp",
+ "protobuf",
+ "tracing"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/php",
+ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
+ "source": "https://github.com/open-telemetry/opentelemetry-php"
+ },
+ "time": "2025-09-17T23:10:12+00:00"
+ },
+ {
+ "name": "open-telemetry/sdk",
+ "version": "1.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/sdk.git",
+ "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e",
+ "reference": "8986bcbcbea79cb1ba9e91c1d621541ad63d6b3e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "nyholm/psr7-server": "^1.1",
+ "open-telemetry/api": "^1.7",
+ "open-telemetry/context": "^1.4",
+ "open-telemetry/sem-conv": "^1.0",
+ "php": "^8.1",
+ "php-http/discovery": "^1.14",
+ "psr/http-client": "^1.0",
+ "psr/http-client-implementation": "^1.0",
+ "psr/http-factory-implementation": "^1.0",
+ "psr/http-message": "^1.0.1|^2.0",
+ "psr/log": "^1.1|^2.0|^3.0",
+ "ramsey/uuid": "^3.0 || ^4.0",
+ "symfony/polyfill-mbstring": "^1.23",
+ "symfony/polyfill-php82": "^1.26",
+ "tbachert/spi": "^1.0.5"
+ },
+ "suggest": {
+ "ext-gmp": "To support unlimited number of synchronous metric readers",
+ "ext-mbstring": "To increase performance of string operations",
+ "open-telemetry/sdk-configuration": "File-based OpenTelemetry SDK configuration"
+ },
+ "type": "library",
+ "extra": {
+ "spi": {
+ "OpenTelemetry\\API\\Configuration\\ConfigEnv\\EnvComponentLoader": [
+ "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderHttpConfig",
+ "OpenTelemetry\\API\\Instrumentation\\Configuration\\General\\ConfigEnv\\EnvComponentLoaderPeerConfig"
+ ],
+ "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\ResolverInterface": [
+ "OpenTelemetry\\SDK\\Common\\Configuration\\Resolver\\SdkConfigurationResolver"
+ ],
+ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [
+ "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "1.9.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "Common/Util/functions.php",
+ "Logs/Exporter/_register.php",
+ "Metrics/MetricExporter/_register.php",
+ "Propagation/_register.php",
+ "Trace/SpanExporter/_register.php",
+ "Common/Dev/Compatibility/_load.php",
+ "_autoload.php"
+ ],
+ "psr-4": {
+ "OpenTelemetry\\SDK\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "SDK for OpenTelemetry PHP.",
+ "keywords": [
+ "Metrics",
+ "apm",
+ "logging",
+ "opentelemetry",
+ "otel",
+ "sdk",
+ "tracing"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/php",
+ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
+ "source": "https://github.com/open-telemetry/opentelemetry-php"
+ },
+ "time": "2025-10-02T23:44:28+00:00"
+ },
+ {
+ "name": "open-telemetry/sem-conv",
+ "version": "1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opentelemetry-php/sem-conv.git",
+ "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/8da7ec497c881e39afa6657d72586e27efbd29a1",
+ "reference": "8da7ec497c881e39afa6657d72586e27efbd29a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenTelemetry\\SemConv\\": "."
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "opentelemetry-php contributors",
+ "homepage": "https://github.com/open-telemetry/opentelemetry-php/graphs/contributors"
+ }
+ ],
+ "description": "Semantic conventions for OpenTelemetry PHP.",
+ "keywords": [
+ "Metrics",
+ "apm",
+ "logging",
+ "opentelemetry",
+ "otel",
+ "semantic conventions",
+ "semconv",
+ "tracing"
+ ],
+ "support": {
+ "chat": "https://app.slack.com/client/T08PSQ7BQ/C01NFPCV44V",
+ "docs": "https://opentelemetry.io/docs/php",
+ "issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
+ "source": "https://github.com/open-telemetry/opentelemetry-php"
+ },
+ "time": "2025-09-03T12:08:10+00:00"
+ },
+ {
+ "name": "php-http/discovery",
+ "version": "1.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-http/discovery.git",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0|^2.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "nyholm/psr7": "<1.0",
+ "zendframework/zend-diactoros": "*"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "*",
+ "psr/http-factory-implementation": "*",
+ "psr/http-message-implementation": "*"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0.2|^2.0",
+ "graham-campbell/phpspec-skip-example-extension": "^5.0",
+ "php-http/httplug": "^1.0 || ^2.0",
+ "php-http/message-factory": "^1.0",
+ "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
+ "sebastian/comparator": "^3.0.5 || ^4.0.8",
+ "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Http\\Discovery\\Composer\\Plugin",
+ "plugin-optional": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Http\\Discovery\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/Composer/Plugin.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com"
+ }
+ ],
+ "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
+ "homepage": "http://php-http.org",
+ "keywords": [
+ "adapter",
+ "client",
+ "discovery",
+ "factory",
+ "http",
+ "message",
+ "psr17",
+ "psr7"
+ ],
+ "support": {
+ "issues": "https://github.com/php-http/discovery/issues",
+ "source": "https://github.com/php-http/discovery/tree/1.20.0"
+ },
+ "time": "2024-10-02T11:20:13+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "ramsey/collection",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/collection.git",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "captainhook/plugin-composer": "^5.3",
+ "ergebnis/composer-normalize": "^2.45",
+ "fakerphp/faker": "^1.24",
+ "hamcrest/hamcrest-php": "^2.0",
+ "jangregor/phpstan-prophecy": "^2.1",
+ "mockery/mockery": "^1.6",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpspec/prophecy-phpunit": "^2.3",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5",
+ "ramsey/coding-standard": "^2.3",
+ "ramsey/conventional-commits": "^1.6",
+ "roave/security-advisories": "dev-latest"
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ },
+ "ramsey/conventional-commits": {
+ "configFile": "conventional-commits.json"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Ramsey\\Collection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ben Ramsey",
+ "email": "ben@benramsey.com",
+ "homepage": "https://benramsey.com"
+ }
+ ],
+ "description": "A PHP library for representing and manipulating collections.",
+ "keywords": [
+ "array",
+ "collection",
+ "hash",
+ "map",
+ "queue",
+ "set"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/collection/issues",
+ "source": "https://github.com/ramsey/collection/tree/2.1.1"
+ },
+ "time": "2025-03-22T05:38:12+00:00"
+ },
+ {
+ "name": "ramsey/uuid",
+ "version": "4.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/uuid.git",
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
+ "php": "^8.0",
+ "ramsey/collection": "^1.2 || ^2.0"
+ },
+ "replace": {
+ "rhumsaa/uuid": "self.version"
+ },
+ "require-dev": {
+ "captainhook/captainhook": "^5.25",
+ "captainhook/plugin-composer": "^5.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "ergebnis/composer-normalize": "^2.47",
+ "mockery/mockery": "^1.6",
+ "paragonie/random-lib": "^2",
+ "php-mock/php-mock": "^2.6",
+ "php-mock/php-mock-mockery": "^1.5",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpbench/phpbench": "^1.2.14",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "slevomat/coding-standard": "^8.18",
+ "squizlabs/php_codesniffer": "^3.13"
+ },
+ "suggest": {
+ "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+ "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+ "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
+ "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+ "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Ramsey\\Uuid\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
+ "keywords": [
+ "guid",
+ "identifier",
+ "uuid"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/uuid/issues",
+ "source": "https://github.com/ramsey/uuid/tree/4.9.1"
+ },
+ "time": "2025-09-04T20:59:21+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:21:43+00:00"
+ },
+ {
+ "name": "symfony/http-client",
+ "version": "v7.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/4b62871a01c49457cf2a8e560af7ee8a94b87a62",
+ "reference": "4b62871a01c49457cf2a8e560af7ee8a94b87a62",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
+ "symfony/polyfill-php83": "^1.29",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "amphp/amp": "<2.5",
+ "amphp/socket": "<1.1",
+ "php-http/discovery": "<1.15",
+ "symfony/http-foundation": "<6.4"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/http-client": "^4.2.1|^5.0",
+ "amphp/http-tunnel": "^1.0|^2.0",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "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"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v7.3.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "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-09-11T10:12:26+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-29T11:18:49+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "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": "2024-12-23T08:48:59+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php82",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php82.git",
+ "reference": "5d2ed36f7734637dacc025f179698031951b1692"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692",
+ "reference": "5d2ed36f7734637dacc025f179698031951b1692",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php82\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php82/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "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": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php83\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "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-07-08T02:45:35+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-25T09:37:31+00:00"
+ },
+ {
+ "name": "tbachert/spi",
+ "version": "v1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Nevay/spi.git",
+ "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Nevay/spi/zipball/e7078767866d0a9e0f91d3f9d42a832df5e39002",
+ "reference": "e7078767866d0a9e0f91d3f9d42a832df5e39002",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0",
+ "composer/semver": "^1.0 || ^2.0 || ^3.0",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "composer/composer": "^2.0",
+ "infection/infection": "^0.27.9",
+ "phpunit/phpunit": "^10.5",
+ "psalm/phar": "^5.18"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Nevay\\SPI\\Composer\\Plugin",
+ "branch-alias": {
+ "dev-main": "1.0.x-dev"
+ },
+ "plugin-optional": true
+ },
+ "autoload": {
+ "psr-4": {
+ "Nevay\\SPI\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "description": "Service provider loading facility",
+ "keywords": [
+ "service provider"
+ ],
+ "support": {
+ "issues": "https://github.com/Nevay/spi/issues",
+ "source": "https://github.com/Nevay/spi/tree/v1.0.5"
+ },
+ "time": "2025-06-29T15:42:06+00:00"
+ },
+ {
+ "name": "utopia-php/compression",
+ "version": "0.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/compression.git",
+ "reference": "66f093557ba66d98245e562036182016c7dcfe8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/compression/zipball/66f093557ba66d98245e562036182016c7dcfe8a",
+ "reference": "66f093557ba66d98245e562036182016c7dcfe8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0"
+ },
+ "require-dev": {
+ "laravel/pint": "1.2.*",
+ "phpunit/phpunit": "^9.3",
+ "vimeo/psalm": "4.0.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\Compression\\": "src/Compression"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A simple Compression library to handle file compression",
+ "keywords": [
+ "compression",
+ "framework",
+ "php",
+ "upf",
+ "utopia"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/compression/issues",
+ "source": "https://github.com/utopia-php/compression/tree/0.1.3"
+ },
+ "time": "2025-01-15T15:15:51+00:00"
+ },
{
"name": "utopia-php/di",
"version": "0.1.0",
@@ -106,6 +2008,56 @@
"source": "https://github.com/utopia-php/servers/tree/0.1.1"
},
"time": "2024-09-06T02:25:56+00:00"
+ },
+ {
+ "name": "utopia-php/telemetry",
+ "version": "0.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/utopia-php/telemetry.git",
+ "reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/437f0021777f0e575dfb9e8a1a081b3aed75e33f",
+ "reference": "437f0021777f0e575dfb9e8a1a081b3aed75e33f",
+ "shasum": ""
+ },
+ "require": {
+ "ext-opentelemetry": "*",
+ "ext-protobuf": "*",
+ "nyholm/psr7": "^1.8",
+ "open-telemetry/exporter-otlp": "^1.1",
+ "open-telemetry/sdk": "^1.1",
+ "php": ">=8.0",
+ "symfony/http-client": "^7.1"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.2",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.5.25"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Utopia\\Telemetry\\": "src/Telemetry"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "keywords": [
+ "framework",
+ "php",
+ "upf"
+ ],
+ "support": {
+ "issues": "https://github.com/utopia-php/telemetry/issues",
+ "source": "https://github.com/utopia-php/telemetry/tree/0.1.1"
+ },
+ "time": "2025-03-17T11:57:52+00:00"
}
],
"packages-dev": [
@@ -183,6 +2135,7 @@
"issues": "https://github.com/doctrine/annotations/issues",
"source": "https://github.com/doctrine/annotations/tree/2.0.2"
},
+ "abandoned": true,
"time": "2024-09-05T10:17:24+00:00"
},
{
@@ -334,16 +2287,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.24.0",
+ "version": "v1.25.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a"
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/0345f3b05f136801af8c339f9d16ef29e6b4df8a",
- "reference": "0345f3b05f136801af8c339f9d16ef29e6b4df8a",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
+ "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
"shasum": ""
},
"require": {
@@ -354,9 +2307,9 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.82.2",
- "illuminate/view": "^11.45.1",
- "larastan/larastan": "^3.5.0",
+ "friendsofphp/php-cs-fixer": "^3.87.2",
+ "illuminate/view": "^11.46.0",
+ "larastan/larastan": "^3.7.1",
"laravel-zero/framework": "^11.45.0",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.1",
@@ -367,9 +2320,6 @@
],
"type": "project",
"autoload": {
- "files": [
- "overrides/Runner/Parallel/ProcessFactory.php"
- ],
"psr-4": {
"App\\": "app/",
"Database\\Seeders\\": "database/seeders/",
@@ -399,7 +2349,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2025-07-10T18:09:32+00:00"
+ "time": "2025-09-19T02:57:12+00:00"
},
{
"name": "myclabs/deep-copy",
@@ -788,16 +2738,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.12.28",
- "source": {
- "type": "git",
- "url": "https://github.com/phpstan/phpstan.git",
- "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9"
- },
+ "version": "1.12.32",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
- "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8",
+ "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8",
"shasum": ""
},
"require": {
@@ -842,7 +2787,7 @@
"type": "github"
}
],
- "time": "2025-07-17T17:15:39+00:00"
+ "time": "2025-09-30T10:16:31+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -1165,16 +3110,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.24",
+ "version": "9.6.29",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "ea49afa29aeea25ea7bf9de9fdd7cab163cc0701"
+ "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea49afa29aeea25ea7bf9de9fdd7cab163cc0701",
- "reference": "ea49afa29aeea25ea7bf9de9fdd7cab163cc0701",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
+ "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3",
"shasum": ""
},
"require": {
@@ -1199,7 +3144,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",
@@ -1248,7 +3193,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.24"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29"
},
"funding": [
{
@@ -1272,7 +3217,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-10T08:32:42+00:00"
+ "time": "2025-09-24T06:29:11+00:00"
},
{
"name": "psr/cache",
@@ -1316,115 +3261,12 @@
"keywords": [
"cache",
"psr",
- "psr-6"
- ],
- "support": {
- "source": "https://github.com/php-fig/cache/tree/3.0.0"
- },
- "time": "2021-02-03T23:26:27+00:00"
- },
- {
- "name": "psr/container",
- "version": "2.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/container.git",
- "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
- "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
- "shasum": ""
- },
- "require": {
- "php": ">=7.4.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Container\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common Container Interface (PHP FIG PSR-11)",
- "homepage": "https://github.com/php-fig/container",
- "keywords": [
- "PSR-11",
- "container",
- "container-interface",
- "container-interop",
- "psr"
- ],
- "support": {
- "issues": "https://github.com/php-fig/container/issues",
- "source": "https://github.com/php-fig/container/tree/2.0.2"
- },
- "time": "2021-11-05T16:47:00+00:00"
- },
- {
- "name": "psr/log",
- "version": "3.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/php-fig/log.git",
- "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
- "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
- "shasum": ""
- },
- "require": {
- "php": ">=8.0.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Psr\\Log\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for logging libraries",
- "homepage": "https://github.com/php-fig/log",
- "keywords": [
- "log",
- "psr",
- "psr-3"
+ "psr-6"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/3.0.2"
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
},
- "time": "2024-09-11T13:17:53+00:00"
+ "time": "2021-02-03T23:26:27+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1867,16 +3709,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": {
@@ -1932,15 +3774,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",
@@ -2533,16 +4387,16 @@
},
{
"name": "symfony/console",
- "version": "v7.3.2",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1"
+ "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/5f360ebc65c55265a74d23d7fe27f957870158a1",
- "reference": "5f360ebc65c55265a74d23d7fe27f957870158a1",
+ "url": "https://api.github.com/repos/symfony/console/zipball/2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
+ "reference": "2b9c5fafbac0399a20a2e82429e2bd735dcfb7db",
"shasum": ""
},
"require": {
@@ -2607,7 +4461,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.3.2"
+ "source": "https://github.com/symfony/console/tree/v7.3.4"
},
"funding": [
{
@@ -2627,74 +4481,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-30T17:13:41+00:00"
- },
- {
- "name": "symfony/deprecation-contracts",
- "version": "v3.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/contracts",
- "name": "symfony/contracts"
- },
- "branch-alias": {
- "dev-main": "3.6-dev"
- }
- },
- "autoload": {
- "files": [
- "function.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "A generic function and convention to trigger deprecation notices",
- "homepage": "https://symfony.com",
- "support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2024-09-25T14:21:43+00:00"
+ "time": "2025-09-22T15:31:00+00:00"
},
{
"name": "symfony/filesystem",
@@ -2836,16 +4623,16 @@
},
{
"name": "symfony/options-resolver",
- "version": "v7.3.2",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37"
+ "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/119bcf13e67dbd188e5dbc74228b1686f66acd37",
- "reference": "119bcf13e67dbd188e5dbc74228b1686f66acd37",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
+ "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d",
"shasum": ""
},
"require": {
@@ -2883,7 +4670,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v7.3.2"
+ "source": "https://github.com/symfony/options-resolver/tree/v7.3.3"
},
"funding": [
{
@@ -2903,11 +4690,11 @@
"type": "tidelift"
}
],
- "time": "2025-07-15T11:36:08+00:00"
+ "time": "2025-08-05T10:16:07+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -2966,7 +4753,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -2977,6 +4764,10 @@
"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"
@@ -2986,16 +4777,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe"
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
- "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
@@ -3044,7 +4835,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
@@ -3055,16 +4846,20 @@
"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": "2024-09-09T11:45:10+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -3125,7 +4920,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -3137,84 +4932,7 @@
"type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2024-09-09T11:45:10+00:00"
- },
- {
- "name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "shasum": ""
- },
- "require": {
- "ext-iconv": "*",
- "php": ">=7.2"
- },
- "provide": {
- "ext-mbstring": "*"
- },
- "suggest": {
- "ext-mbstring": "For best performance"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/polyfill",
- "name": "symfony/polyfill"
- }
- },
- "autoload": {
- "files": [
- "bootstrap.php"
- ],
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for the Mbstring extension",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "mbstring",
- "polyfill",
- "portable",
- "shim"
- ],
- "support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
@@ -3222,20 +4940,20 @@
"type": "tidelift"
}
],
- "time": "2024-12-23T08:48:59+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/process",
- "version": "v7.3.0",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af"
+ "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af",
- "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af",
+ "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b",
+ "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b",
"shasum": ""
},
"require": {
@@ -3267,7 +4985,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.3.0"
+ "source": "https://github.com/symfony/process/tree/v7.3.4"
},
"funding": [
{
@@ -3279,86 +4997,7 @@
"type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-04-17T09:11:12+00:00"
- },
- {
- "name": "symfony/service-contracts",
- "version": "v3.6.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/service-contracts.git",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "shasum": ""
- },
- "require": {
- "php": ">=8.1",
- "psr/container": "^1.1|^2.0",
- "symfony/deprecation-contracts": "^2.5|^3"
- },
- "conflict": {
- "ext-psr": "<1.1|>=2"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/contracts",
- "name": "symfony/contracts"
- },
- "branch-alias": {
- "dev-main": "3.6-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Contracts\\Service\\": ""
- },
- "exclude-from-classmap": [
- "/Test/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Generic abstractions related to writing services",
- "homepage": "https://symfony.com",
- "keywords": [
- "abstractions",
- "contracts",
- "decoupling",
- "interfaces",
- "interoperability",
- "standards"
- ],
- "support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
@@ -3366,20 +5005,20 @@
"type": "tidelift"
}
],
- "time": "2025-04-25T09:37:31+00:00"
+ "time": "2025-09-11T10:12:26+00:00"
},
{
"name": "symfony/string",
- "version": "v7.3.2",
+ "version": "v7.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca"
+ "reference": "f96476035142921000338bad71e5247fbc138872"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca",
- "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872",
+ "reference": "f96476035142921000338bad71e5247fbc138872",
"shasum": ""
},
"require": {
@@ -3394,7 +5033,6 @@
},
"require-dev": {
"symfony/emoji": "^7.1",
- "symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
@@ -3437,7 +5075,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.3.2"
+ "source": "https://github.com/symfony/string/tree/v7.3.4"
},
"funding": [
{
@@ -3457,7 +5095,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:47:49+00:00"
+ "time": "2025-09-11T14:36:48+00:00"
},
{
"name": "theseer/tokenizer",
@@ -3561,15 +5199,15 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=8.0",
+ "php": ">=8.1",
"ext-swoole": "*"
},
"platform-dev": {
"ext-xdebug": "*"
},
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/docker-compose.yml b/docker-compose.yml
index bd2952c7..5e396eb3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,8 +10,6 @@ services:
- ./tests:/usr/share/nginx/html/tests
networks:
- testing
- depends_on:
- - swoole
swoole:
build:
context: .
diff --git a/phpunit.xml b/phpunit.xml
index c87db0dc..c7210e5e 100755
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,19 +1,33 @@
-
+
-
- ./tests/e2e/Client.php
+
./tests/MockRequest.php
./tests/MockResponse.php
- ./tests/
+ ./tests/HookTest.php
+ ./tests/HttpTest.php
+ ./tests/RequestTest.php
+ ./tests/ResponseTest.php
+ ./tests/RouterTest.php
+ ./tests/RouteTest.php
+ ./tests/UtopiaFPMRequestTest.php
+ ./tests/Validator/
+
+
+ ./tests/e2e/Client.php
+ ./tests/e2e/BaseTest.php
+ ./tests/e2e/ResponseFPMTest.php
+ ./tests/e2e/ResponseSwooleTest.php
+ ./tests/e2e/ResponseSwooleCoroutineTest.php
-
\ No newline at end of file
+
diff --git a/src/Http/Adapter/FPM/Response.php b/src/Http/Adapter/FPM/Response.php
index 41a81436..6a049bca 100644
--- a/src/Http/Adapter/FPM/Response.php
+++ b/src/Http/Adapter/FPM/Response.php
@@ -25,10 +25,10 @@ public function write(string $content): bool
*
* Send optional content and end
*
- * @param string $content
+ * @param string|null $content
* @return void
*/
- public function end(string $content = ''): void
+ public function end(?string $content = null): void
{
if (!empty($content)) {
echo $content;
@@ -54,12 +54,18 @@ protected function sendStatus(int $statusCode, string $reason): void
* Output Header
*
* @param string $key
- * @param string $value
+ * @param string|array $value
* @return void
*/
- public function sendHeader(string $key, string $value): void
+ public function sendHeader(string $key, mixed $value): void
{
- \header($key.': '.$value);
+ if (\is_array($value)) {
+ foreach ($value as $v) {
+ \header($key.': '.$v, false);
+ }
+ } else {
+ \header($key.': '.$value);
+ }
}
/**
diff --git a/src/Http/Adapter/Swoole/Request.php b/src/Http/Adapter/Swoole/Request.php
index e2600648..1739ac42 100644
--- a/src/Http/Adapter/Swoole/Request.php
+++ b/src/Http/Adapter/Swoole/Request.php
@@ -280,9 +280,7 @@ public function getFiles($key): array
*/
public function getCookie(string $key, string $default = ''): string
{
- $key = strtolower($key);
-
- return $this->swoole->cookie[$key] ?? $default;
+ return $this->swoole->cookie[$key] ?? $this->swoole->cookie[strtolower($key)] ?? $default;
}
/**
@@ -381,6 +379,24 @@ protected function generateInput(): array
*/
protected function generateHeaders(): array
{
- return $this->swoole->header;
+ $headers = $this->swoole->header ?? [];
+
+ // Check if cookies are available in a separate property
+ if (!empty($this->swoole->cookie)) {
+ // Convert cookies back to Cookie header format
+ $cookiePairs = [];
+ foreach ($this->swoole->cookie as $name => $value) {
+ $cookiePairs[] = $name . '=' . $value;
+ }
+ if (!empty($cookiePairs)) {
+ $headers['cookie'] = implode('; ', $cookiePairs);
+ }
+ }
+
+ foreach ($headers as $key => $value) {
+ $headers[strtolower($key)] = $value;
+ }
+
+ return $headers;
}
}
diff --git a/src/Http/Adapter/Swoole/Response.php b/src/Http/Adapter/Swoole/Response.php
index 515fd103..a417b2f0 100644
--- a/src/Http/Adapter/Swoole/Response.php
+++ b/src/Http/Adapter/Swoole/Response.php
@@ -40,7 +40,7 @@ public function write(string $content): bool
* @param string $content
* @return void
*/
- public function end(string $content = ''): void
+ public function end(?string $content = null): void
{
$this->swoole->end($content);
}
@@ -61,10 +61,10 @@ protected function sendStatus(int $statusCode, string $reason = ''): void
* Send Header
*
* @param string $key
- * @param string $value
+ * @param string|array $value
* @return void
*/
- public function sendHeader(string $key, string $value): void
+ public function sendHeader(string $key, mixed $value): void
{
$this->swoole->header($key, $value);
}
diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php
index 7073520d..1a79e927 100755
--- a/src/Http/Adapter/Swoole/Server.php
+++ b/src/Http/Adapter/Swoole/Server.php
@@ -4,7 +4,6 @@
use Utopia\Http\Adapter;
use Swoole\Http\Server as SwooleServer;
-use Swoole\Runtime;
class Server extends Adapter
{
@@ -39,7 +38,6 @@ public function onStart(callable $callback)
public function start()
{
- Runtime::enableCoroutine();
return $this->server->start();
}
}
diff --git a/src/Http/Http.php b/src/Http/Http.php
index 2b2262dd..cc248cde 100755
--- a/src/Http/Http.php
+++ b/src/Http/Http.php
@@ -5,9 +5,14 @@
use Utopia\DI\Container;
use Utopia\DI\Dependency;
use Utopia\Servers\Base;
+use Utopia\Telemetry\Adapter as Telemetry;
+use Utopia\Telemetry\Adapter\None as NoTelemetry;
+use Utopia\Telemetry\Histogram;
+use Utopia\Telemetry\UpDownCounter;
class Http extends Base
{
+ public const COMPRESSION_MIN_SIZE_DEFAULT = 1024;
/**
* Request method constants
*/
@@ -41,6 +46,18 @@ class Http extends Base
*/
protected static ?Route $wildcardRoute = null;
+ /**
+ * Compression
+ */
+ protected bool $compression = false;
+ protected int $compressionMinSize = self::COMPRESSION_MIN_SIZE_DEFAULT;
+ protected mixed $compressionSupported = [];
+
+ private Histogram $requestDuration;
+ private UpDownCounter $activeRequests;
+ private Histogram $requestBodySize;
+ private Histogram $responseBodySize;
+
/**
* @var Adapter
*/
@@ -49,6 +66,14 @@ class Http extends Base
protected string|null $requestClass = null;
protected string|null $responseClass = null;
+ /**
+ * Matched Route
+ *
+ * During runtime $this->route might be overwritten with the wildcard route to keep custom functions working with
+ * paths not declared in the Router. Keep a copy of the original matched app route.
+ */
+ protected ?Route $matchedRoute = null;
+
/**
* Http
*
@@ -61,8 +86,61 @@ public function __construct(Adapter $server, Container $container, string $timez
$this->files = new Files();
$this->server = $server;
$this->container = $container;
+ $this->setTelemetry(new NoTelemetry());
+ }
+
+ /**
+ * Set Compression
+ */
+ public function setCompression(bool $compression): static
+ {
+ $this->compression = $compression;
+ return $this;
+ }
+
+ /**
+ * Set minimum compression size
+ */
+ public function setCompressionMinSize(int $compressionMinSize): static
+ {
+ $this->compressionMinSize = $compressionMinSize;
+ return $this;
+ }
+
+ /**
+ * Set supported compression algorithms
+ */
+ public function setCompressionSupported(mixed $compressionSupported): static
+ {
+ $this->compressionSupported = $compressionSupported;
+ return $this;
+ }
+
+ /**
+ * Set telemetry adapter.
+ *
+ * @param Telemetry $telemetry
+ * @return void
+ */
+ public function setTelemetry(Telemetry $telemetry): void
+ {
+ // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
+ $this->requestDuration = $telemetry->createHistogram(
+ 'http.server.request.duration',
+ 's',
+ null,
+ ['ExplicitBucketBoundaries' => [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]]
+ );
+
+ // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserveractive_requests
+ $this->activeRequests = $telemetry->createUpDownCounter('http.server.active_requests', '{request}');
+ // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestbodysize
+ $this->requestBodySize = $telemetry->createHistogram('http.server.request.body.size', 'By');
+ // https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverresponsebodysize
+ $this->responseBodySize = $telemetry->createHistogram('http.server.response.body.size', 'By');
}
+
/**
* Set Request Class
*/
@@ -177,31 +255,6 @@ public static function options(): Hook
return $hook;
}
- /**
- * Get Mode
- *
- * Get current mode
- *
- * @return string
- */
- public static function getMode(): string
- {
- return self::$mode;
- }
-
- /**
- * Set Mode
- *
- * Set current mode
- *
- * @param string $value
- * @return void
- */
- public static function setMode(string $value): void
- {
- self::$mode = $value;
- }
-
/**
* Get Routes
*
@@ -481,6 +534,41 @@ protected function lifecycle(Route $route, Request $request, Container $context)
return $this;
}
+ public function run(Container $context): static
+ {
+ $request = $context->get('request');
+ /** @var Request $request */
+ $response = $context->get('response');
+ /** @var Response $response */
+ $route = $this->match($request);
+ /** @var ?Route $route */
+ $this->matchedRoute = $route;
+
+ $this->activeRequests->add(1, [
+ 'http.request.method' => $request->getMethod(),
+ 'url.scheme' => $request->getProtocol(),
+ ]);
+ $start = microtime(true);
+ $result = $this->runInternal($context, $route);
+
+ $requestDuration = microtime(true) - $start;
+ $attributes = [
+ 'url.scheme' => $request->getProtocol(),
+ 'http.request.method' => $request->getMethod(),
+ 'http.route' => $route?->getPath() ?? '',
+ 'http.response.status_code' => $response->getStatusCode(),
+ ];
+ $this->requestDuration->record($requestDuration, $attributes);
+ $this->requestBodySize->record($request->getSize(), $attributes);
+ $this->responseBodySize->record($response->getSize(), $attributes);
+ $this->activeRequests->add(-1, [
+ 'http.request.method' => $request->getMethod(),
+ 'url.scheme' => $request->getProtocol(),
+ ]);
+ return $result;
+ }
+
+
/**
* Run
*
@@ -489,13 +577,19 @@ protected function lifecycle(Route $route, Request $request, Container $context)
*
* @param Container $context
*/
- public function run(Container $context): static
+ protected function runInternal(Container $context, ?Route $route): static
{
$request = $context->get('request');
/** @var Request $request */
$response = $context->get('response');
/** @var Response $response */
+ if ($this->compression) {
+ $response->setAcceptEncoding($request->getHeader('accept-encoding', ''));
+ $response->setCompressionMinSize($this->compressionMinSize);
+ $response->setCompressionSupported($this->compressionSupported);
+ }
+
if ($this->isFileLoaded($request->getURI())) {
$time = (60 * 60 * 24 * 365 * 2); // 45 days cache
@@ -509,7 +603,6 @@ public function run(Container $context): static
}
$method = $request->getMethod();
- $route = $this->match($request);
$groups = ($route instanceof Route) ? $route->getGroups() : [];
if (null === $route && null !== self::$wildcardRoute) {
diff --git a/src/Http/Request.php b/src/Http/Request.php
index 38151339..a90f86ce 100755
--- a/src/Http/Request.php
+++ b/src/Http/Request.php
@@ -504,32 +504,7 @@ public function setPayload(array $params): static
*
* @return array
*/
- protected function generateHeaders(): array
- {
- if (null === $this->headers) {
- /**
- * Fallback for older PHP versions
- * that do not support generateHeaders
- */
- if (!\function_exists('getallheaders')) {
- $headers = [];
-
- foreach ($_SERVER as $name => $value) {
- if (\substr($name, 0, 5) == 'HTTP_') {
- $headers[\str_replace(' ', '-', \strtolower(\str_replace('_', ' ', \substr($name, 5))))] = $value;
- }
- }
-
- $this->headers = $headers;
-
- return $this->headers;
- }
-
- $this->headers = array_change_key_case(getallheaders());
- }
-
- return $this->headers;
- }
+ abstract protected function generateHeaders(): array;
/**
* Generate input
diff --git a/src/Http/Response.php b/src/Http/Response.php
index cc156457..866e9452 100755
--- a/src/Http/Response.php
+++ b/src/Http/Response.php
@@ -2,6 +2,8 @@
namespace Utopia\Http;
+use Utopia\Compression\Compression;
+
abstract class Response
{
/**
@@ -42,92 +44,74 @@ abstract class Response
* HTTP response status codes
*/
public const STATUS_CODE_CONTINUE = 100;
-
public const STATUS_CODE_SWITCHING_PROTOCOLS = 101;
- public const STATUS_CODE_OK = 200;
+ public const STATUS_CODE_PROCESSING = 102;
- public const STATUS_CODE_CREATED = 201;
+ public const STATUS_CODE_EARLY_HINTS = 103;
+ public const STATUS_CODE_OK = 200;
+ public const STATUS_CODE_CREATED = 201;
public const STATUS_CODE_ACCEPTED = 202;
-
public const STATUS_CODE_NON_AUTHORITATIVE_INFORMATION = 203;
-
public const STATUS_CODE_NOCONTENT = 204;
-
public const STATUS_CODE_RESETCONTENT = 205;
-
public const STATUS_CODE_PARTIALCONTENT = 206;
+ public const STATUS_CODE_MULTI_STATUS = 207;
+ public const STATUS_CODE_ALREADY_REPORTED = 208;
+ public const STATUS_CODE_IM_USED = 226;
public const STATUS_CODE_MULTIPLE_CHOICES = 300;
-
public const STATUS_CODE_MOVED_PERMANENTLY = 301;
-
public const STATUS_CODE_FOUND = 302;
-
public const STATUS_CODE_SEE_OTHER = 303;
-
public const STATUS_CODE_NOT_MODIFIED = 304;
-
public const STATUS_CODE_USE_PROXY = 305;
-
public const STATUS_CODE_UNUSED = 306;
-
public const STATUS_CODE_TEMPORARY_REDIRECT = 307;
+ public const STATUS_CODE_PERMANENT_REDIRECT = 308;
public const STATUS_CODE_BAD_REQUEST = 400;
-
public const STATUS_CODE_UNAUTHORIZED = 401;
-
public const STATUS_CODE_PAYMENT_REQUIRED = 402;
-
public const STATUS_CODE_FORBIDDEN = 403;
-
public const STATUS_CODE_NOT_FOUND = 404;
-
public const STATUS_CODE_METHOD_NOT_ALLOWED = 405;
-
public const STATUS_CODE_NOT_ACCEPTABLE = 406;
-
public const STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED = 407;
-
public const STATUS_CODE_REQUEST_TIMEOUT = 408;
-
public const STATUS_CODE_CONFLICT = 409;
-
public const STATUS_CODE_GONE = 410;
-
public const STATUS_CODE_LENGTH_REQUIRED = 411;
-
public const STATUS_CODE_PRECONDITION_FAILED = 412;
-
public const STATUS_CODE_REQUEST_ENTITY_TOO_LARGE = 413;
-
public const STATUS_CODE_REQUEST_URI_TOO_LONG = 414;
-
public const STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415;
-
public const STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
-
public const STATUS_CODE_EXPECTATION_FAILED = 417;
-
+ public const STATUS_CODE_IM_A_TEAPOT = 418;
+ public const STATUS_CODE_MISDIRECTED_REQUEST = 421;
+ public const STATUS_CODE_UNPROCESSABLE_ENTITY = 422;
+ public const STATUS_CODE_LOCKED = 423;
+ public const STATUS_CODE_FAILED_DEPENDENCY = 424;
public const STATUS_CODE_TOO_EARLY = 425;
-
+ public const STATUS_CODE_UPGRADE_REQUIRED = 426;
+ public const STATUS_CODE_PRECONDITION_REQUIRED = 428;
public const STATUS_CODE_TOO_MANY_REQUESTS = 429;
-
+ public const STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
public const STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS = 451;
public const STATUS_CODE_INTERNAL_SERVER_ERROR = 500;
-
public const STATUS_CODE_NOT_IMPLEMENTED = 501;
-
public const STATUS_CODE_BAD_GATEWAY = 502;
-
public const STATUS_CODE_SERVICE_UNAVAILABLE = 503;
-
public const STATUS_CODE_GATEWAY_TIMEOUT = 504;
-
public const STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505;
+ public const STATUS_CODE_VARIANT_ALSO_NEGOTIATES = 506;
+ public const STATUS_CODE_INSUFFICIENT_STORAGE = 507;
+ public const STATUS_CODE_LOOP_DETECTED = 508;
+ public const STATUS_CODE_NOT_EXTENDED = 510;
+ public const STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED = 511;
/**
* @var array
@@ -135,6 +119,8 @@ abstract class Response
protected $statusCodes = [
self::STATUS_CODE_CONTINUE => 'Continue',
self::STATUS_CODE_SWITCHING_PROTOCOLS => 'Switching Protocols',
+ self::STATUS_CODE_PROCESSING => 'Processing',
+ self::STATUS_CODE_EARLY_HINTS => 'Early Hints',
self::STATUS_CODE_OK => 'OK',
self::STATUS_CODE_CREATED => 'Created',
self::STATUS_CODE_ACCEPTED => 'Accepted',
@@ -142,6 +128,9 @@ abstract class Response
self::STATUS_CODE_NOCONTENT => 'No Content',
self::STATUS_CODE_RESETCONTENT => 'Reset Content',
self::STATUS_CODE_PARTIALCONTENT => 'Partial Content',
+ self::STATUS_CODE_MULTI_STATUS => 'Multi-Status',
+ self::STATUS_CODE_ALREADY_REPORTED => 'Already Reported',
+ self::STATUS_CODE_IM_USED => 'IM Used',
self::STATUS_CODE_MULTIPLE_CHOICES => 'Multiple Choices',
self::STATUS_CODE_MOVED_PERMANENTLY => 'Moved Permanently',
self::STATUS_CODE_FOUND => 'Found',
@@ -150,6 +139,7 @@ abstract class Response
self::STATUS_CODE_USE_PROXY => 'Use Proxy',
self::STATUS_CODE_UNUSED => '(Unused)',
self::STATUS_CODE_TEMPORARY_REDIRECT => 'Temporary Redirect',
+ self::STATUS_CODE_PERMANENT_REDIRECT => 'Permanent Redirect',
self::STATUS_CODE_BAD_REQUEST => 'Bad Request',
self::STATUS_CODE_UNAUTHORIZED => 'Unauthorized',
self::STATUS_CODE_PAYMENT_REQUIRED => 'Payment Required',
@@ -168,8 +158,16 @@ abstract class Response
self::STATUS_CODE_UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
self::STATUS_CODE_REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
self::STATUS_CODE_EXPECTATION_FAILED => 'Expectation Failed',
+ self::STATUS_CODE_IM_A_TEAPOT => 'I\'m a teapot',
+ self::STATUS_CODE_MISDIRECTED_REQUEST => 'Misdirected Request',
+ self::STATUS_CODE_UNPROCESSABLE_ENTITY => 'Unprocessable Entity',
+ self::STATUS_CODE_LOCKED => 'Locked',
+ self::STATUS_CODE_FAILED_DEPENDENCY => 'Failed Dependency',
self::STATUS_CODE_TOO_EARLY => 'Too Early',
+ self::STATUS_CODE_UPGRADE_REQUIRED => 'Upgrade Required',
+ self::STATUS_CODE_PRECONDITION_REQUIRED => 'Precondition Required',
self::STATUS_CODE_TOO_MANY_REQUESTS => 'Too Many Requests',
+ self::STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large',
self::STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons',
self::STATUS_CODE_INTERNAL_SERVER_ERROR => 'Internal Server Error',
self::STATUS_CODE_NOT_IMPLEMENTED => 'Not Implemented',
@@ -177,6 +175,11 @@ abstract class Response
self::STATUS_CODE_SERVICE_UNAVAILABLE => 'Service Unavailable',
self::STATUS_CODE_GATEWAY_TIMEOUT => 'Gateway Timeout',
self::STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',
+ self::STATUS_CODE_VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',
+ self::STATUS_CODE_INSUFFICIENT_STORAGE => 'Insufficient Storage',
+ self::STATUS_CODE_LOOP_DETECTED => 'Loop Detected',
+ self::STATUS_CODE_NOT_EXTENDED => 'Not Extended',
+ self::STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required',
];
/**
@@ -184,17 +187,71 @@ abstract class Response
*
* @var array
*/
- protected $compressed = [
+ private static $compressible = [
+ // Text
+ 'text/html' => true,
+ 'text/richtext' => true,
'text/plain' => true,
'text/css' => true,
- 'text/javascript' => true,
+ 'text/x-script' => true,
+ 'text/x-component' => true,
+ 'text/x-java-source' => true,
+ 'text/x-markdown' => true,
+
+ // JavaScript
'application/javascript' => true,
- 'text/html' => true,
- 'text/html; charset=UTF-8' => true,
+ 'application/x-javascript' => true,
+ 'text/javascript' => true,
+ 'text/js' => true,
+
+ // Icons
+ 'image/x-icon' => true,
+ 'image/vnd.microsoft.icon' => true,
+
+ // Scripts
+ 'application/x-perl' => true,
+ 'application/x-httpd-cgi' => true,
+
+ // XML and JSON
+ 'text/xml' => true,
+ 'application/xml' => true,
+ 'application/rss+xml' => true,
+ 'application/vnd.api+json' => true,
+ 'application/x-protobuf' => true,
'application/json' => true,
- 'application/json; charset=UTF-8' => true,
+ 'application/manifest+json' => true,
+ 'application/ld+json' => true,
+ 'application/graphql+json' => true,
+ 'application/geo+json' => true,
+
+ // Multipart
+ 'multipart/bag' => true,
+ 'multipart/mixed' => true,
+
+ // XHTML
+ 'application/xhtml+xml' => true,
+
+ // Fonts
+ 'font/ttf' => true,
+ 'font/otf' => true,
+ 'font/x-woff' => true,
'image/svg+xml' => true,
- 'application/xml+rss' => true,
+ 'application/vnd.ms-fontobject' => true,
+ 'application/ttf' => true,
+ 'application/x-ttf' => true,
+ 'application/otf' => true,
+ 'application/x-otf' => true,
+ 'application/truetype' => true,
+ 'application/opentype' => true,
+ 'application/x-opentype' => true,
+ 'application/font-woff' => true,
+ 'application/eot' => true,
+ 'application/font' => true,
+ 'application/font-sfnt' => true,
+
+ // WebAssembly
+ 'application/wasm' => true,
+ 'application/javascript-binast' => true,
];
public const COOKIE_SAMESITE_NONE = 'None';
@@ -226,7 +283,7 @@ abstract class Response
protected bool $sent = false;
/**
- * @var array
+ * @var array>
*/
protected array $headers = [];
@@ -245,6 +302,21 @@ abstract class Response
*/
protected int $size = 0;
+ /**
+ * @var string
+ */
+ protected string $acceptEncoding = '';
+
+ /**
+ * @var int
+ */
+ protected int $compressionMinSize = Http::COMPRESSION_MIN_SIZE_DEFAULT;
+
+ /**
+ * @var mixed
+ */
+ protected mixed $compressionSupported = [];
+
/**
* Response constructor.
*
@@ -255,6 +327,18 @@ public function __construct(float $time = 0)
$this->startTime = (!empty($time)) ? $time : \microtime(true);
}
+ private function isCompressible(?string $contentType): bool
+ {
+ if (!$contentType) {
+ return false;
+ }
+
+ // Strip any parameters (e.g. ;charset=utf-8)
+ $contentType = strtolower(trim(explode(';', $contentType)[0]));
+
+ return isset(self::$compressible[$contentType]);
+ }
+
/**
* Set content type
*
@@ -270,6 +354,43 @@ public function setContentType(string $type, string $charset = ''): static
return $this;
}
+ /**
+ * Set accept encoding
+ *
+ * Set HTTP accept encoding header.
+ *
+ * @param string $acceptEncoding
+ */
+ public function setAcceptEncoding(string $acceptEncoding): static
+ {
+ $this->acceptEncoding = $acceptEncoding;
+ return $this;
+ }
+
+ /**
+ * Set min compression size
+ *
+ * Set minimum size for compression to be applied in bytes.
+ *
+ * @param int $compressionMinSize
+ */
+ public function setCompressionMinSize(int $compressionMinSize): static
+ {
+ $this->compressionMinSize = $compressionMinSize;
+ return $this;
+ }
+
+ /**
+ * Set supported compression algorithms
+ *
+ * @param mixed $compressionSupported
+ */
+ public function setCompressionSupported(mixed $compressionSupported): static
+ {
+ $this->compressionSupported = $compressionSupported;
+ return $this;
+ }
+
/**
* Get content type
*
@@ -363,10 +484,24 @@ public function enablePayload(): static
*
* @param string $key
* @param string $value
+ * @param bool $override
*/
- public function addHeader(string $key, ?string $value): static
+ public function addHeader(string $key, string $value, bool $override = true): static
{
- $this->headers[$key] = $value;
+ if ($override) {
+ $this->headers[$key] = $value;
+ return $this;
+ }
+
+ if (\array_key_exists($key, $this->headers)) {
+ if (\is_array($this->headers[$key])) {
+ $this->headers[$key][] = $value;
+ } else {
+ $this->headers[$key] = [$this->headers[$key], $value];
+ }
+ } else {
+ $this->headers[$key] = $value;
+ }
return $this;
}
@@ -392,7 +527,7 @@ public function removeHeader(string $key): static
*
* Return array of all response headers
*
- * @return array
+ * @return array>
*/
public function getHeaders(): array
{
@@ -405,13 +540,13 @@ public function getHeaders(): array
* Add an HTTP cookie to response header
*
* @param string $name
- * @param string $value
- * @param int $expire
- * @param string $path
- * @param string $domain
- * @param bool $secure
- * @param bool $httponly
- * @param string $sameSite
+ * @param string|null $value
+ * @param int|null $expire
+ * @param string|null $path
+ * @param string|null $domain
+ * @param bool|null $secure
+ * @param bool|null $httponly
+ * @param string|null $sameSite
*/
public function addCookie(string $name, ?string $value = null, ?int $expire = null, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httponly = null, ?string $sameSite = null): static
{
@@ -473,13 +608,13 @@ public function send(string $body = ''): void
return;
}
- $this->sent = true;
+ $serverHeader = $this->headers['Server'] ?? 'Utopia/Http';
+ $this->addHeader('Server', $serverHeader, override: true);
$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()
->appendHeaders();
@@ -487,12 +622,21 @@ public function send(string $body = ''): void
if (!$this->disablePayload) {
$length = strlen($body);
- $this->size = $this->size + strlen(implode("\n", $this->headers)) + $length;
+ $headersSize = 0;
+ foreach ($this->headers as $name => $values) {
+ if (\is_array($values)) {
+ foreach ($values as $value) {
+ $headersSize += \strlen($name . ': ' . $value);
+ }
+ $headersSize += (\count($values) - 1) * 2; // linebreaks
+ } else {
+ $headersSize += \strlen($name . ': ' . $values);
+ }
+ }
+ $headersSize += (\count($this->headers) - 1) * 2; // linebreaks
+ $this->size = $this->size + $headersSize + $length;
- if (array_key_exists(
- $this->contentType,
- $this->compressed
- ) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb
+ if ($this->isCompressible($this->contentType) && ($length <= self::CHUNK_SIZE)) { // Dont compress with GZIP / Brotli if header is not listed and size is bigger than 2mb
$this->end($body);
} else {
for ($i = 0; $i < ceil($length / self::CHUNK_SIZE); $i++) {
@@ -501,11 +645,11 @@ public function send(string $body = ''): void
$this->end();
}
-
- $this->disablePayload();
- } else {
- $this->end();
}
+
+ $this->sent = true;
+
+ $this->disablePayload();
}
/**
@@ -523,10 +667,10 @@ abstract public function write(string $content): bool;
*
* Send optional content and end
*
- * @param string $content
+ * @param string|null $content
* @return void
*/
- abstract public function end(string $content = ''): void;
+ abstract public function end(?string $content = null): void;
/**
* Output response
@@ -548,7 +692,7 @@ public function chunk(string $body = '', bool $end = false): void
$this->sent = true;
}
- $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime));
+ $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime), override: true);
$this
->appendCookies()
@@ -578,7 +722,7 @@ protected function appendHeaders(): static
// Send content type header
if (!empty($this->contentType)) {
- $this->addHeader('Content-Type', $this->contentType);
+ $this->addHeader('Content-Type', $this->contentType, override: true);
}
// Set application headers
@@ -604,10 +748,10 @@ abstract protected function sendStatus(int $statusCode, string $reason): void;
* Output Header
*
* @param string $key
- * @param string $value
+ * @param string|array $value
* @return void
*/
- abstract public function sendHeader(string $key, string $value): void;
+ abstract public function sendHeader(string $key, mixed $value): void;
/**
* Send Cookie
@@ -668,7 +812,7 @@ public function redirect(string $url, int $statusCode = 301): void
}
$this
- ->addHeader('Location', $url)
+ ->addHeader('Location', $url, override: true)
->setStatusCode($statusCode)
->send('');
}
diff --git a/src/Http/Route.php b/src/Http/Route.php
index f2dd40ae..17c3dbbf 100755
--- a/src/Http/Route.php
+++ b/src/Http/Route.php
@@ -28,7 +28,7 @@ class Route extends Hook
/**
* Path params.
*
- * @var array
+ * @var array>
*/
protected array $pathParams = [];
@@ -46,6 +46,8 @@ class Route extends Hook
*/
protected int $order;
+ protected string $matchedPath = '';
+
public function __construct(string $method, string $path)
{
$this->path($path);
@@ -53,6 +55,17 @@ public function __construct(string $method, string $path)
$this->order = ++self::$counter;
}
+ public function setMatchedPath(string $path): self
+ {
+ $this->matchedPath = $path;
+ return $this;
+ }
+
+ public function getMatchedPath(): string
+ {
+ return $this->matchedPath;
+ }
+
/**
* Get Route Order ID
*
@@ -139,9 +152,9 @@ public function getHook(): bool
* @param int $index
* @return void
*/
- public function setPathParam(string $key, int $index): void
+ public function setPathParam(string $key, int $index, string $path = ''): void
{
- $this->pathParams[$key] = $index;
+ $this->pathParams[$path][$key] = $index;
}
/**
@@ -150,12 +163,18 @@ public function setPathParam(string $key, int $index): void
* @param \Utopia\Http\Request $request
* @return array
*/
- public function getPathValues(Request $request): array
+ public function getPathValues(Request $request, string $path = ''): array
{
$pathValues = [];
$parts = explode('/', ltrim($request->getURI(), '/'));
- foreach ($this->pathParams as $key => $index) {
+ if (empty($path)) {
+ $pathParams = $this->pathParams[$path] ?? \array_values($this->pathParams)[0] ?? [];
+ } else {
+ $pathParams = $this->pathParams[$path] ?? [];
+ }
+
+ foreach ($pathParams as $key => $index) {
if (array_key_exists($index, $parts)) {
$pathValues[$key] = $parts[$index];
}
diff --git a/src/Http/Router.php b/src/Http/Router.php
index f855abc8..9bc2a969 100644
--- a/src/Http/Router.php
+++ b/src/Http/Router.php
@@ -86,7 +86,7 @@ public static function addRoute(Route $route): void
}
foreach ($params as $key => $index) {
- $route->setPathParam($key, $index);
+ $route->setPathParam($key, $index, $path);
}
self::$routes[$route->getMethod()][$path] = $route;
@@ -101,12 +101,16 @@ public static function addRoute(Route $route): void
*/
public static function addRouteAlias(string $path, Route $route): void
{
- [$alias] = self::preparePath($path);
+ [$alias, $params] = self::preparePath($path);
if (array_key_exists($alias, self::$routes[$route->getMethod()]) && !self::$allowOverride) {
throw new Exception("Route for ({$route->getMethod()}:{$alias}) already registered.");
}
+ foreach ($params as $key => $index) {
+ $route->setPathParam($key, $index, $alias);
+ }
+
self::$routes[$route->getMethod()][$alias] = $route;
}
@@ -138,7 +142,9 @@ public static function match(string $method, string $path): Route|null
);
if (array_key_exists($match, self::$routes[$method])) {
- return self::$routes[$method][$match];
+ $route = self::$routes[$method][$match];
+ $route->setMatchedPath($match);
+ return $route;
}
}
@@ -147,7 +153,9 @@ public static function match(string $method, string $path): Route|null
*/
$match = self::WILDCARD_TOKEN;
if (array_key_exists($match, self::$routes[$method])) {
- return self::$routes[$method][$match];
+ $route = self::$routes[$method][$match];
+ $route->setMatchedPath($match);
+ return $route;
}
/**
@@ -157,7 +165,9 @@ public static function match(string $method, string $path): Route|null
$current = ($current ?? '') . "{$part}/";
$match = $current . self::WILDCARD_TOKEN;
if (array_key_exists($match, self::$routes[$method])) {
- return self::$routes[$method][$match];
+ $route = self::$routes[$method][$match];
+ $route->setMatchedPath($match);
+ return $route;
}
}
@@ -192,7 +202,7 @@ protected static function combinations(array $set): iterable
* @param string $path
* @return array
*/
- protected static function preparePath(string $path): array
+ public static function preparePath(string $path): array
{
$parts = array_values(array_filter(explode('/', $path)));
$prepare = '';
diff --git a/src/Http/Validator/Domain.php b/src/Http/Validator/Domain.php
index 13479fbc..5c3fe907 100644
--- a/src/Http/Validator/Domain.php
+++ b/src/Http/Validator/Domain.php
@@ -13,6 +13,31 @@
*/
class Domain extends Validator
{
+ /**
+ * Helper for creating domain restriction rule.
+ * Such rules prevent validation from passing, so this behaves as deny-list.
+ *
+ * @param string $hostname A domain base, such as top-level domain or subdomain. Restriction is only applied if domain matches this hostname
+ * @param int $levels Specify what level (top-level, subdomain, sub-subdomain, ..) domain must be. Example: "stage.appwrite.io" is level 3
+ * @param array $prefixDenyList Disallowed beginning of domain, useful for reserved behaviours, such as prefixing "branch-" for preview domains
+ *
+ */
+ public static function createRestriction(string $hostname, ?int $levels = null, array $prefixDenyList = [])
+ {
+ return [
+ 'hostname' => $hostname,
+ 'levels' => $levels,
+ 'prefixDenyList' => $prefixDenyList,
+ ];
+ }
+
+ /**
+ * @param array $restrictions Set of conditions that prevent validation from passing
+ */
+ public function __construct(protected array $restrictions = [])
+ {
+ }
+
/**
* Get Description
*
@@ -53,6 +78,35 @@ public function isValid($value): bool
return false;
}
+ foreach ($this->restrictions as $restriction) {
+ $hostname = $restriction['hostname'];
+ $levels = $restriction['levels'];
+ $prefixDenyList = $restriction['prefixDenyList'];
+
+ // Only apply restriction rules to relevant domains
+ if (!\str_ends_with($value, $hostname)) {
+ continue;
+ }
+
+ // Domain-level restriction
+ if (!is_null($levels)) {
+ $expectedPartsCount = $levels;
+ $partsCount = \count(\explode('.', $value, $expectedPartsCount + 1));
+ if ($partsCount !== $expectedPartsCount) {
+ return false;
+ }
+ }
+
+ // Domain prefix (beginning) restriction
+ if (!empty($prefixDenyList)) {
+ foreach ($prefixDenyList as $deniedPrefix) {
+ if (\str_starts_with($value, $deniedPrefix)) {
+ return false;
+ }
+ }
+ }
+ }
+
return true;
}
diff --git a/tests/HttpTest.php b/tests/HttpTest.php
index 9b7d0401..b42f9ad7 100755
--- a/tests/HttpTest.php
+++ b/tests/HttpTest.php
@@ -10,6 +10,7 @@
use Utopia\Http\Tests\MockResponse as Response;
use Utopia\Http\Validator\Text;
use Utopia\Http\Adapter\FPM\Server;
+use Utopia\Http\Tests\UtopiaFPMRequestTest;
class HttpTest extends TestCase
{
@@ -790,4 +791,64 @@ public function testWildcardRoute(): void
$_SERVER['REQUEST_METHOD'] = $method;
$_SERVER['REQUEST_URI'] = $uri;
}
+
+ public function testCallableStringParametersNotExecuted(): void
+ {
+ // Test that callable strings (like function names) are not executed
+ $route = new Route('GET', '/test-callable-string');
+
+ $route
+ ->param('callback', 'phpinfo', new Text(200), 'callback param', true)
+ ->action(function ($callback) {
+ // If the string 'phpinfo' was executed as a function,
+ // it would output PHP info. Instead, it should just be the string.
+ echo 'callback-value: ' . $callback;
+ });
+
+ $context = clone $this->context;
+ \ob_start();
+ $this->http->execute($route, new Request(), $context);
+ $result = \ob_get_contents();
+ \ob_end_clean();
+
+ $this->assertEquals('callback-value: phpinfo', $result);
+
+ // Test with request parameter that is a callable string
+ $route2 = new Route('GET', '/test-callable-string-param');
+
+ $route2
+ ->param('func', 'default', new Text(200), 'func param', false)
+ ->action(function ($func) {
+ echo 'func-value: ' . $func;
+ });
+
+ \ob_start();
+ $context = clone $this->context;
+ $request = new UtopiaFPMRequestTest();
+ $request::_setParams(['func' => 'system']);
+ $this->http->execute($route2, $request, $context);
+ $result = \ob_get_contents();
+ \ob_end_clean();
+
+ $this->assertEquals('func-value: system', $result);
+
+ // Test callable closure still works
+ $route3 = new Route('GET', '/test-callable-closure');
+
+ $route3
+ ->param('generated', function () {
+ return 'generated-value';
+ }, new Text(200), 'generated param', true)
+ ->action(function ($generated) {
+ echo 'generated: ' . $generated;
+ });
+
+ \ob_start();
+ $context = clone $this->context;
+ $this->http->execute($route3, new Request(), $context);
+ $result = \ob_get_contents();
+ \ob_end_clean();
+
+ $this->assertEquals('generated: generated-value', $result);
+ }
}
diff --git a/tests/UtopiaFPMRequestTest.php b/tests/UtopiaFPMRequestTest.php
new file mode 100644
index 00000000..d15c7a68
--- /dev/null
+++ b/tests/UtopiaFPMRequestTest.php
@@ -0,0 +1,77 @@
+
+ * @version 1.0 RC4
+ * @license The MIT License (MIT)
+ */
+
namespace Utopia\Http\Validator;
use PHPUnit\Framework\TestCase;
@@ -45,4 +57,30 @@ public function testIsValid()
$this->assertEquals(false, $this->domain->isValid(1));
$this->assertEquals(false, $this->domain->isValid(1.2));
}
+
+ public function testRestrictions()
+ {
+ $validator = new Domain([
+ Domain::createRestriction('appwrite.network', 3, ['preview-', 'branch-']),
+ Domain::createRestriction('fra.appwrite.run', 4),
+ ]);
+
+ $this->assertEquals(true, $validator->isValid('google.com'));
+ $this->assertEquals(true, $validator->isValid('stage.google.com'));
+ $this->assertEquals(true, $validator->isValid('shard4.stage.google.com'));
+
+ $this->assertEquals(false, $validator->isValid('appwrite.network'));
+ $this->assertEquals(false, $validator->isValid('preview-a.appwrite.network'));
+ $this->assertEquals(false, $validator->isValid('branch-a.appwrite.network'));
+ $this->assertEquals(true, $validator->isValid('google.appwrite.network'));
+ $this->assertEquals(false, $validator->isValid('stage.google.appwrite.network'));
+ $this->assertEquals(false, $validator->isValid('shard4.stage.google.appwrite.network'));
+
+ $this->assertEquals(false, $validator->isValid('fra.appwrite.run'));
+ $this->assertEquals(true, $validator->isValid('appwrite.run'));
+ $this->assertEquals(true, $validator->isValid('google.fra.appwrite.run'));
+ $this->assertEquals(false, $validator->isValid('shard4.google.fra.appwrite.run'));
+ $this->assertEquals(true, $validator->isValid('branch-google.fra.appwrite.run'));
+ $this->assertEquals(true, $validator->isValid('preview-google.fra.appwrite.run'));
+ }
}
diff --git a/tests/Validator/HostTest.php b/tests/Validator/HostTest.php
index 4162a5b3..7805adda 100644
--- a/tests/Validator/HostTest.php
+++ b/tests/Validator/HostTest.php
@@ -1,5 +1,17 @@
+ * @version 1.0 RC4
+ * @license The MIT License (MIT)
+ */
+
namespace Utopia\Http\Validator;
use PHPUnit\Framework\TestCase;
diff --git a/tests/Validator/IPTest.php b/tests/Validator/IPTest.php
index 074a8f68..ac0ae1e5 100644
--- a/tests/Validator/IPTest.php
+++ b/tests/Validator/IPTest.php
@@ -1,5 +1,16 @@
+ * @version 1.0 RC4
+ * @license The MIT License (MIT)
+ */
+
namespace Utopia\Http\Validator;
use PHPUnit\Framework\TestCase;
diff --git a/tests/Validator/URLTest.php b/tests/Validator/URLTest.php
index de530cd1..f939eeb6 100644
--- a/tests/Validator/URLTest.php
+++ b/tests/Validator/URLTest.php
@@ -1,5 +1,17 @@
+ * @version 1.0 RC4
+ * @license The MIT License (MIT)
+ */
+
namespace Utopia\Http\Validator;
use PHPUnit\Framework\TestCase;
diff --git a/tests/e2e/BaseTest.php b/tests/e2e/BaseTest.php
index f7be2237..03db3319 100644
--- a/tests/e2e/BaseTest.php
+++ b/tests/e2e/BaseTest.php
@@ -80,4 +80,50 @@ public function testNotFound()
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringStartsWith('Not Found on ', $response['body']);
}
+
+ public function testCookie()
+ {
+ // One cookie
+ $cookie = 'cookie1=value1';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Two cookiees
+ $cookie = 'cookie1=value1; cookie2=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ /**
+ * Cookie response always expecting space in multiple cookie
+ * as RFC 6265 (https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1) recommends it
+ */
+
+ // Two cookies without optional space
+ $cookie = 'cookie1=value1;cookie2=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('cookie1=value1; cookie2=value2', $response['body']);
+
+ // Cookie with "=" in value
+ $cookie = 'cookie1=value1=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Case sensitivity for cookie names
+ $cookie = 'cookie1=v1;Cookie1=v2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('cookie1=v1; Cookie1=v2', $response['body']);
+ }
+
+ public function testSetCookie()
+ {
+ $response = $this->client->call(Client::METHOD_GET, '/set-cookie');
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('value1', $response['cookies']['key1']);
+ $this->assertEquals('value2', $response['cookies']['key2']);
+ }
}
diff --git a/tests/e2e/Client.php b/tests/e2e/Client.php
index fda4a2c1..79a3e837 100644
--- a/tests/e2e/Client.php
+++ b/tests/e2e/Client.php
@@ -55,6 +55,7 @@ public function __construct(string $baseUrl = 'http://fpm')
public function call(string $method, string $path = '', array $headers = [], array $params = [])
{
usleep(50000);
+ $url = $this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : '');
$ch = curl_init($this->baseUrl.$path.(($method == self::METHOD_GET && !empty($params)) ? '?'.http_build_query($params) : ''));
$responseHeaders = [];
$responseStatus = -1;
@@ -65,14 +66,34 @@ public function call(string $method, string $path = '', array $headers = [], arr
curl_setopt($ch, CURLOPT_NOBODY, true);
}
+ $cookies = [];
+
+ $query = match ($headers['content-type'] ?? '') {
+ 'application/json' => \json_encode($params),
+ 'text/plain' => $params,
+ default => \http_build_query($params),
+ };
+
+ $formattedHeaders = [];
+ foreach ($headers as $key => $value) {
+ if (strtolower($key) === 'accept-encoding') {
+ curl_setopt($ch, CURLOPT_ENCODING, $value);
+ continue;
+ } else {
+ $formattedHeaders[] = $key . ': ' . $value;
+ }
+ }
+
+ curl_setopt($ch, CURLOPT_PATH_AS_IS, 1);
+
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
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, $formattedHeaders);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
- curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
+ curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders, &$cookies) {
$len = strlen($header);
$header = explode(':', $header, 2);
@@ -80,11 +101,21 @@ public function call(string $method, string $path = '', array $headers = [], arr
return $len;
}
+ if (strtolower(trim($header[0])) == 'set-cookie') {
+ $parsed = $this->parseCookie((string)trim($header[1]));
+ $name = array_key_first($parsed);
+ $cookies[$name] = $parsed[$name];
+ }
+
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
});
+ if ($method !== self::METHOD_GET) {
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
+ }
+
$responseBody = curl_exec($ch);
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
@@ -103,6 +134,22 @@ public function call(string $method, string $path = '', array $headers = [], arr
return [
'headers' => $responseHeaders,
'body' => $responseBody,
+ 'cookies' => $cookies,
];
}
+
+ /**
+ * Parse Cookie String
+ *
+ * @param string $cookie
+ * @return array
+ */
+ public function parseCookie(string $cookie): array
+ {
+ $cookies = [];
+
+ parse_str(strtr($cookie, ['&' => '%26', '+' => '%2B', ';' => '&']), $cookies);
+
+ return $cookies;
+ }
}
diff --git a/tests/e2e/ResponseFPMTest.php b/tests/e2e/ResponseFPMTest.php
index 88cff38e..b0f140c2 100644
--- a/tests/e2e/ResponseFPMTest.php
+++ b/tests/e2e/ResponseFPMTest.php
@@ -5,6 +5,10 @@
use PHPUnit\Framework\TestCase;
use Tests\E2E\Client;
+/**
+ * @group fpm
+ * @group e2e
+ */
class ResponseFPMTest extends TestCase
{
use BaseTest;
@@ -14,4 +18,41 @@ public function setUp(): void
{
$this->client = new Client('http://fpm');
}
+
+ /**
+ * Override cookie test for FPM specific behavior
+ * FPM preserves original cookie format while Swoole normalizes it
+ */
+ public function testCookie()
+ {
+ // One cookie
+ $cookie = 'cookie1=value1';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Two cookies with space (FPM preserves original format)
+ $cookie = 'cookie1=value1; cookie2=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Two cookies without space (FPM preserves original format)
+ $cookie = 'cookie1=value1;cookie2=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Cookie with "=" in value
+ $cookie = 'cookie1=value1=value2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+
+ // Case sensitivity for cookie names
+ $cookie = 'cookie1=v1; Cookie1=v2';
+ $response = $this->client->call(Client::METHOD_GET, '/cookies', [ 'Cookie' => $cookie ]);
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals($cookie, $response['body']);
+ }
}
diff --git a/tests/e2e/ResponseSwooleCoroutineTest.php b/tests/e2e/ResponseSwooleCoroutineTest.php
index d1811e37..ad97fa66 100644
--- a/tests/e2e/ResponseSwooleCoroutineTest.php
+++ b/tests/e2e/ResponseSwooleCoroutineTest.php
@@ -5,6 +5,10 @@
use PHPUnit\Framework\TestCase;
use Tests\E2E\Client;
+/**
+ * @group swoole-coroutine
+ * @group e2e
+ */
class ResponseSwooleCoroutineTest extends TestCase
{
use BaseTest;
diff --git a/tests/e2e/ResponseSwooleTest.php b/tests/e2e/ResponseSwooleTest.php
index 6d793b90..741cc2ba 100755
--- a/tests/e2e/ResponseSwooleTest.php
+++ b/tests/e2e/ResponseSwooleTest.php
@@ -5,6 +5,10 @@
use PHPUnit\Framework\TestCase;
use Tests\E2E\Client;
+/**
+ * @group swoole
+ * @group e2e
+ */
class ResponseSwooleTest extends TestCase
{
use BaseTest;
diff --git a/tests/e2e/init.php b/tests/e2e/init.php
index 540891e6..003fb29c 100644
--- a/tests/e2e/init.php
+++ b/tests/e2e/init.php
@@ -4,6 +4,7 @@
use Swoole\Database\PDOPool;
use Utopia\DI\Dependency;
use Utopia\Http\Http;
+use Utopia\Http\Request;
use Utopia\Http\Response;
use Utopia\Http\Validator\Text;
@@ -71,6 +72,23 @@
$response->send($value);
});
+
+Http::get('/cookies')
+ ->inject('request')
+ ->inject('response')
+ ->action(function (Request $request, Response $response) {
+ $response->send($request->getHeaders()['cookie'] ?? '');
+ });
+
+Http::get('/set-cookie')
+ ->inject('request')
+ ->inject('response')
+ ->action(function (Request $request, Response $response) {
+ $response->addHeader('Set-Cookie', 'key1=value1', false);
+ $response->addHeader('Set-Cookie', 'key2=value2', false);
+ $response->send('OK');
+ });
+
Http::get('/chunked')
->inject('response')
->action(function (Response $response) {