diff --git a/composer.json b/composer.json index 40e0392d..833b4cc3 100644 --- a/composer.json +++ b/composer.json @@ -10,17 +10,17 @@ ], "license": "MIT", "minimum-stability": "stable", - "autoload": { + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "autoload-dev": { "psr-4": { - "Utopia\\": "src/", - "Tests\\E2E\\": "tests/e2e" - } - }, - "autoload-dev": { - "psr-4": { - "Utopia\\Http\\Tests\\": "tests/" - } - }, + "Utopia\\Http\\Tests\\": "tests/", + "Tests\\E2E\\": "tests/e2e" + } + }, "scripts": { "lint": "vendor/bin/pint --test", "format": "vendor/bin/pint", @@ -30,9 +30,12 @@ }, "require": { "php": ">=8.2", + "ext-swoole": "*", "utopia-php/di": "0.3.*", - "utopia-php/validators": "0.2.*", - "ext-swoole": "*" + "utopia-php/servers": "0.3.*", + "utopia-php/compression": "0.1.*", + "utopia-php/telemetry": "0.2.*", + "utopia-php/validators": "0.2.*" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index c854c2ab..6932a584 100644 --- a/composer.lock +++ b/composer.lock @@ -4,35 +4,1769 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5afd948989df91d546b1694354e2f9d2", + "content-hash": "836a13c7945c3bf4de3cd30991d29bf0", "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "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.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+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.33.6", + "source": { + "type": "git", + "url": "https://github.com/protocolbuffers/protobuf-php.git", + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/84b008c23915ed94536737eae46f41ba3bccfe67", + "reference": "84b008c23915ed94536737eae46f41ba3bccfe67", + "shasum": "" + }, + "require": { + "php": ">=8.1.0" + }, + "require-dev": { + "phpunit/phpunit": ">=10.5.62 <11.0.0" + }, + "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.33.6" + }, + "time": "2026-03-18T17:32:05+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.8.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/api.git", + "reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/df5197c6fd0ddd8e9883b87de042d9341300e2ad", + "reference": "df5197c6fd0ddd8e9883b87de042d9341300e2ad", + "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.11" + }, + "type": "library", + "extra": { + "spi": { + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\HookManagerInterface": [ + "OpenTelemetry\\API\\Instrumentation\\AutoInstrumentation\\ExtensionHookManager" + ] + }, + "branch-alias": { + "dev-main": "1.8.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/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-01-21T04:14:03+00:00" + }, + { + "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.4.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/exporter-otlp.git", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/exporter-otlp/zipball/283a0d66522f2adc6d8d7debfd7686be91c282be", + "reference": "283a0d66522f2adc6d8d7debfd7686be91c282be", + "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/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-02-05T09:44:52+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.13.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sdk.git", + "reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/c76f91203bf7ef98ab3f4e0a82ca21699af185e1", + "reference": "c76f91203bf7ef98ab3f4e0a82ca21699af185e1", + "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.12.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/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-01-28T11:38:11+00:00" + }, + { + "name": "open-telemetry/sem-conv", + "version": "1.38.0", + "source": { + "type": "git", + "url": "https://github.com/opentelemetry-php/sem-conv.git", + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/e613bc640a407def4991b8a936a9b27edd9a3240", + "reference": "e613bc640a407def4991b8a936a9b27edd9a3240", + "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/languages/php", + "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", + "source": "https://github.com/open-telemetry/opentelemetry-php" + }, + "time": "2026-01-21T04:14:03+00:00" + }, + { + "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" + "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.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^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.2" + }, + "time": "2025-12-14T04:43:48+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.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/1010624285470eb60e88ed10035102c75b4ea6af", + "reference": "1010624285470eb60e88ed10035102c75b4ea6af", + "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/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "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.4.7" + }, + "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": "2026-03-05T11:16:58+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.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { - "php": ">=7.4.0" + "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-master": "2.0.x-dev" + "dev-main": "3.6-dev" } }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" - } + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -40,24 +1774,144 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, - "time": "2021-11-05T16:47:00+00:00" + "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-15T11:30:57+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.4", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/compression.git", + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/compression/zipball/68045cb9d714c1259582d2dfd0e76bd34f83e713", + "reference": "68045cb9d714c1259582d2dfd0e76bd34f83e713", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "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.4" + }, + "time": "2026-02-17T05:53:40+00:00" }, { "name": "utopia-php/di", @@ -110,6 +1964,115 @@ }, "time": "2026-03-13T05:47:23+00:00" }, + { + "name": "utopia-php/servers", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/servers.git", + "reference": "235be31200df9437fc96a1c270ffef4c64fafe52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/servers/zipball/235be31200df9437fc96a1c270ffef4c64fafe52", + "reference": "235be31200df9437fc96a1c270ffef4c64fafe52", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "utopia-php/di": "0.3.*", + "utopia-php/validators": "0.*" + }, + "require-dev": { + "laravel/pint": "^0.2.3", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Servers\\": "src/Servers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Team Appwrite", + "email": "team@appwrite.io" + } + ], + "description": "A base library for building Utopia style servers.", + "keywords": [ + "framework", + "php", + "servers", + "upf", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/servers/issues", + "source": "https://github.com/utopia-php/servers/tree/0.3.0" + }, + "time": "2026-03-13T11:31:42+00:00" + }, + { + "name": "utopia-php/telemetry", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/telemetry.git", + "reference": "9997ebf59bb77920a7223ad73d834a76b09152c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/9997ebf59bb77920a7223ad73d834a76b09152c3", + "reference": "9997ebf59bb77920a7223ad73d834a76b09152c3", + "shasum": "" + }, + "require": { + "ext-opentelemetry": "*", + "ext-protobuf": "*", + "nyholm/psr7": "1.*", + "open-telemetry/exporter-otlp": "1.*", + "open-telemetry/sdk": "1.*", + "php": ">=8.0", + "symfony/http-client": "7.*" + }, + "require-dev": { + "laravel/pint": "1.*", + "phpbench/phpbench": "1.*", + "phpstan/phpstan": "2.*", + "phpunit/phpunit": "11.*", + "swoole/ide-helper": "6.*" + }, + "suggest": { + "ext-sockets": "Required for the Swoole transport implementation", + "ext-swoole": "Required for the Swoole transport implementation" + }, + "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.2.0" + }, + "time": "2025-12-17T07:56:38+00:00" + }, { "name": "utopia-php/validators", "version": "0.2.0", @@ -1328,57 +3291,8 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "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/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", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { @@ -1387,12 +3301,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1405,17 +3319,16 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common interface for caching libraries", "keywords": [ - "log", + "cache", "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", @@ -2624,73 +4537,6 @@ ], "time": "2026-03-06T14:06:22+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/filesystem", "version": "v8.0.6", @@ -3150,91 +4996,6 @@ ], "time": "2024-09-09T11:45:10+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/process", "version": "v8.0.5", @@ -3300,93 +5061,6 @@ ], "time": "2026-01-26T15:08:38+00:00" }, - { - "name": "symfony/service-contracts", - "version": "v3.6.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", - "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.1" - }, - "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-15T11:30:57+00:00" - }, { "name": "symfony/string", "version": "v8.0.6", @@ -3587,5 +5261,5 @@ "ext-swoole": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/src/Http/Adapter/FPM/Request.php b/src/Http/Adapter/FPM/Request.php index d095445f..5c486c48 100644 --- a/src/Http/Adapter/FPM/Request.php +++ b/src/Http/Adapter/FPM/Request.php @@ -68,9 +68,26 @@ public function setServer(string $key, string $value): static */ public function getIP(): string { - $ips = explode(',', $this->getHeader('HTTP_X_FORWARDED_FOR', $this->getServer('REMOTE_ADDR') ?? '0.0.0.0')); + $remoteAddr = $this->getServer('REMOTE_ADDR') ?? '0.0.0.0'; - return trim($ips[0] ?? ''); + foreach ($this->trustedIpHeaders as $header) { + $headerValue = $this->getHeader($header); + + if (empty($headerValue)) { + continue; + } + + // Leftmost IP address is the address of the originating client + $ips = \explode(',', $headerValue); + $ip = \trim($ips[0]); + + // Validate IP format (supports both IPv4 and IPv6) + if (\filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + + return $remoteAddr; } /** diff --git a/src/Http/Adapter/Swoole/Request.php b/src/Http/Adapter/Swoole/Request.php index d5bd799c..41ac541d 100644 --- a/src/Http/Adapter/Swoole/Request.php +++ b/src/Http/Adapter/Swoole/Request.php @@ -73,9 +73,26 @@ public function setServer(string $key, string $value): static */ public function getIP(): string { - $ips = explode(',', $this->getHeader('x-forwarded-for', $this->getServer('remote_addr') ?? '0.0.0.0')); + $remoteAddr = $this->getServer('remote_addr') ?? '0.0.0.0'; - return trim($ips[0] ?? ''); + foreach ($this->trustedIpHeaders as $header) { + $headerValue = $this->getHeader($header); + + if (empty($headerValue)) { + continue; + } + + // Leftmost IP address is the address of the originating client + $ips = explode(',', $headerValue); + $ip = trim($ips[0]); + + // Validate IP format (supports both IPv4 and IPv6) + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + + return $remoteAddr; } /** @@ -259,9 +276,12 @@ public function getCookie(string $key, string $default = ''): string $cookies = \explode(';', $this->getHeader('cookie', '')); foreach ($cookies as $cookie) { $cookie = \trim($cookie); - [$cookieKey, $cookieValue] = \explode('=', $cookie, 2); - $cookieKey = \trim($cookieKey); - $cookieValue = \trim($cookieValue); + if ($cookie === '') { + continue; + } + $parts = \explode('=', $cookie, 2); + $cookieKey = \trim($parts[0]); + $cookieValue = isset($parts[1]) ? \trim($parts[1]) : ''; if ($cookieKey === $key) { return $cookieValue; } diff --git a/src/Http/Adapter/Swoole/Response.php b/src/Http/Adapter/Swoole/Response.php index be48a027..3eb20417 100644 --- a/src/Http/Adapter/Swoole/Response.php +++ b/src/Http/Adapter/Swoole/Response.php @@ -86,14 +86,14 @@ public function sendHeader(string $key, mixed $value): void protected function sendCookie(string $name, string $value, array $options): void { $this->swoole->cookie( - name: $name, - value: $value, - expires: $options['expire'] ?? 0, - path: $options['path'] ?? '', - domain: $options['domain'] ?? '', - secure: $options['secure'] ?? false, - httponly: $options['httponly'] ?? false, - samesite: $options['samesite'] ?? false, + $name, + $value, + $options['expire'] ?? 0, + $options['path'] ?? '', + $options['domain'] ?? '', + $options['secure'] ?? false, + $options['httponly'] ?? false, + $options['samesite'] ?? false, ); } } diff --git a/src/Http/Adapter/Swoole/Server.php b/src/Http/Adapter/Swoole/Server.php index 57b4f5fb..14d93bb8 100755 --- a/src/Http/Adapter/Swoole/Server.php +++ b/src/Http/Adapter/Swoole/Server.php @@ -5,23 +5,20 @@ use Swoole\Coroutine; use Utopia\Http\Adapter; use Utopia\DI\Container; -use Swoole\Coroutine\Http\Server as SwooleServer; +use Swoole\Http\Server as SwooleServer; use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Response as SwooleResponse; -use function Swoole\Coroutine\run; - class Server extends Adapter { protected SwooleServer $server; protected const REQUEST_CONTAINER_CONTEXT_KEY = '__utopia_http_request_container'; protected Container $container; - public function __construct(string $host, ?string $port = null, array $settings = [], ?Container $container = null) + public function __construct(string $host, ?string $port = null, array $settings = [], int $mode = SWOOLE_PROCESS, ?Container $container = null) { - $this->server = new SwooleServer($host, $port); + $this->server = new SwooleServer($host, (int) $port, $mode); $this->server->set(\array_merge($settings, [ - 'enable_coroutine' => true, 'http_parse_cookie' => false, ])); $this->container = $container ?? new Container(); @@ -29,37 +26,42 @@ public function __construct(string $host, ?string $port = null, array $settings public function onRequest(callable $callback) { - $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { + $this->server->on('request', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { $requestContainer = new Container($this->container); $requestContainer->set('swooleRequest', fn () => $request); $requestContainer->set('swooleResponse', fn () => $response); Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; - $utopiaRequest = new Request($request); - $utopiaResponse = new Response($response); - - \call_user_func($callback, $utopiaRequest, $utopiaResponse); + \call_user_func($callback, new Request($request), new Response($response)); }); } public function getContainer(): Container { - return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + if (Coroutine::getCid() !== -1) { + return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + } + + return $this->container; } - public function onStart(callable $callback) + public function getServer(): SwooleServer { + return $this->server; + } - \call_user_func($callback, $this); + public function onStart(callable $callback) + { + $this->server->on('start', function () use ($callback) { + go(function () use ($callback) { + \call_user_func($callback, $this); + }); + }); } public function start() { - if (Coroutine::getCid() === -1) { - run(fn () => $this->server->start()); - } else { - $this->server->start(); - } + return $this->server->start(); } } diff --git a/src/Http/Adapter/SwooleCoroutine/Request.php b/src/Http/Adapter/SwooleCoroutine/Request.php new file mode 100644 index 00000000..a2aa49a5 --- /dev/null +++ b/src/Http/Adapter/SwooleCoroutine/Request.php @@ -0,0 +1,9 @@ +server = new SwooleServer($host, $port, false, true); + $this->server->set(\array_merge($settings, [ + 'http_parse_cookie' => false, + ])); + $this->container = $container ?? new Container(); + } + + public function onRequest(callable $callback) + { + $this->server->handle('/', function (SwooleRequest $request, SwooleResponse $response) use ($callback) { + go(function () use ($request, $response, $callback) { + $requestContainer = new Container($this->container); + $requestContainer->set('swooleRequest', fn () => $request); + $requestContainer->set('swooleResponse', fn () => $response); + + Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] = $requestContainer; + + \call_user_func($callback, new Request($request), new Response($response)); + }); + }); + } + + public function getContainer(): Container + { + if (Coroutine::getCid() !== -1) { + return Coroutine::getContext()[self::REQUEST_CONTAINER_CONTEXT_KEY] ?? $this->container; + } + + return $this->container; + } + + public function getServer(): SwooleServer + { + return $this->server; + } + + public function onStart(callable $callback) + { + $this->onStartCallback = $callback; + } + + public function start() + { + go(function () { + if ($this->onStartCallback) { + \call_user_func($this->onStartCallback, $this); + } + $this->server->start(); + }); + } +} diff --git a/src/Http/Hook.php b/src/Http/Hook.php deleted file mode 100644 index 0f177dc3..00000000 --- a/src/Http/Hook.php +++ /dev/null @@ -1,282 +0,0 @@ -action = function (): void { - }; - } - - /** - * Add Description - * - * @param string $desc - * @return static - */ - public function desc(string $desc): static - { - $this->desc = $desc; - - return $this; - } - - /** - * Get Description - * - * @return string - */ - public function getDesc(): string - { - return $this->desc; - } - - /** - * Add Group - * - * @param array $groups - * @return static - */ - public function groups(array $groups): static - { - $this->groups = $groups; - - return $this; - } - - /** - * Get Groups - * - * @return array - */ - public function getGroups(): array - { - return $this->groups; - } - - /** - * Add Label - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function label(string $key, mixed $value): static - { - $this->labels[$key] = $value; - - return $this; - } - - /** - * Get Label - * - * Return given label value or default value if label doesn't exists - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getLabel(string $key, mixed $default): mixed - { - return (isset($this->labels[$key])) ? $this->labels[$key] : $default; - } - - /** - * Add Action - * - * @param callable $action - * @return static - */ - public function action(callable $action): static - { - $this->action = $action; - - return $this; - } - - /** - * Get Action - * - * @return callable - */ - public function getAction() - { - return $this->action; - } - - /** - * Get Injections - * - * @return array - */ - public function getInjections(): array - { - return $this->injections; - } - - /** - * Inject - * - * @param string $injection - * @return static - * - * @throws Exception - */ - public function inject(string $injection): static - { - if (array_key_exists($injection, $this->injections)) { - throw new Exception('Injection already declared for '.$injection); - } - - $this->injections[$injection] = [ - 'name' => $injection, - 'order' => count($this->params) + count($this->injections), - ]; - - return $this; - } - - /** - * Add Param - * - * @param string $key - * @param mixed $default - * @param Validator|callable $validator - * @param string $description - * @param bool $optional - * @param array $injections - * @param bool $skipValidation - * @return static - */ - public function param(string $key, mixed $default, Validator|callable $validator, string $description = '', bool $optional = false, array $injections = [], bool $skipValidation = false): static - { - $this->params[$key] = [ - 'default' => $default, - 'validator' => $validator, - 'description' => $description, - 'optional' => $optional, - 'injections' => $injections, - 'skipValidation' => $skipValidation, - 'value' => null, - 'order' => count($this->params) + count($this->injections), - ]; - - return $this; - } - - /** - * Get Params - * - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * Get Param Values - * - * @return array - */ - public function getParamsValues(): array - { - $values = []; - - foreach ($this->params as $key => $param) { - $values[$key] = $param['value']; - } - - return $values; - } - - /** - * Set Param Value - * - * @param string $key - * @param mixed $value - * @return static - * - * @throws Exception - */ - public function setParamValue(string $key, mixed $value): static - { - if (!isset($this->params[$key])) { - throw new Exception('Unknown key'); - } - - $this->params[$key]['value'] = $value; - - return $this; - } - - /** - * Get Param Value - * - * @param string $key - * @return mixed - * - * @throws Exception - */ - public function getParamValue(string $key): mixed - { - if (!isset($this->params[$key])) { - throw new Exception('Unknown key'); - } - - return $this->params[$key]['value']; - } -} diff --git a/src/Http/Http.php b/src/Http/Http.php index d3556a38..b42cbdf8 100755 --- a/src/Http/Http.php +++ b/src/Http/Http.php @@ -2,11 +2,20 @@ namespace Utopia\Http; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Utopia\DI\Container; +use Utopia\Servers\Hook; +use Utopia\Telemetry\Adapter as Telemetry; +use Utopia\Telemetry\Adapter\None as NoTelemetry; +use Utopia\Telemetry\Histogram; +use Utopia\Telemetry\UpDownCounter; use Utopia\Validator; class Http { + public const COMPRESSION_MIN_SIZE_DEFAULT = 1024; + /** * Request method constants */ @@ -116,6 +125,23 @@ class Http */ protected static ?Route $wildcardRoute = null; + /** + * Compression + */ + protected bool $compression = false; + + protected int $compressionMinSize = Http::COMPRESSION_MIN_SIZE_DEFAULT; + + protected mixed $compressionSupported = []; + + private Histogram $requestDuration; + + private UpDownCounter $activeRequests; + + private Histogram $requestBodySize; + + private Histogram $responseBodySize; + /** * @var Adapter */ @@ -132,6 +158,56 @@ public function __construct(Adapter $server, string $timezone) \date_default_timezone_set($timezone); $this->files = new Files(); $this->server = $server; + $this->container = $server->getContainer(); + $this->setTelemetry(new NoTelemetry()); + } + + /** + * 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 Compression + */ + public function setCompression(bool $compression): void + { + $this->compression = $compression; + } + + /** + * Set minimum compression size + */ + public function setCompressionMinSize(int $compressionMinSize): void + { + $this->compressionMinSize = $compressionMinSize; + } + + /** + * Set supported compression algorithms + */ + public function setCompressionSupported(mixed $compressionSupported): void + { + $this->compressionSupported = $compressionSupported; } /** @@ -352,7 +428,7 @@ public function getResource(string $name): mixed { try { return $this->server->getContainer()->get($name); - } catch (\Throwable $e) { + } catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) { // Normalize DI container errors to the Http layer's "resource" terminology. $message = \str_replace('dependency', 'resource', $e->getMessage()); @@ -389,6 +465,16 @@ public function getResources(array $list): array * @param string[] $injections */ public function setResource(string $name, callable $callback, array $injections = []): void + { + $this->container->set($name, $callback, $injections); + } + + /** + * Set a request-scoped resource on the current request's container. + * + * @param string[] $injections + */ + protected function setRequestResource(string $name, callable $callback, array $injections = []): void { $this->server->getContainer()->set($name, $callback, $injections); } @@ -610,7 +696,9 @@ public function execute(Route $route, Request $request): static { $arguments = []; $groups = $route->getGroups(); - $pathValues = $route->getPathValues($request); + + $preparedPath = Router::preparePath($route->getMatchedPath()); + $pathValues = $route->getPathValues($request, $preparedPath[0]); try { if ($route->getHook()) { @@ -652,7 +740,7 @@ public function execute(Route $route, Request $request): static } } } catch (\Throwable $e) { - $this->setResource('error', fn () => $e, []); + $this->setRequestResource('error', fn () => $e, []); foreach ($groups as $group) { foreach (self::$errors as $error) { // Group error hooks @@ -702,7 +790,7 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) $arg = $existsInRequest ? $requestParams[$key] : $param['default']; if (\is_callable($arg) && !\is_string($arg)) { - $arg = \call_user_func_array($arg, $this->getResources($param['injections'])); + $arg = \call_user_func_array($arg, \array_values($this->getResources($param['injections']))); } $value = $existsInValues ? $values[$key] : $arg; @@ -728,7 +816,38 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) } /** - * Run + * Run: wrapper function to record telemetry. All domain logic should happen in `runInternal`. + */ + public function run(Request $request, Response $response): static + { + $this->activeRequests->add(1, [ + 'http.request.method' => $request->getMethod(), + 'url.scheme' => $request->getProtocol(), + ]); + + $start = microtime(true); + $result = $this->runInternal($request, $response); + + $requestDuration = microtime(true) - $start; + $attributes = [ + 'url.scheme' => $request->getProtocol(), + 'http.request.method' => $request->getMethod(), + 'http.route' => $this->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 internal * * This is the place to initialize any pre routing logic. * This is where you might want to parse the application current URL by any desired logic @@ -736,10 +855,16 @@ protected function getArguments(Hook $hook, array $values, array $requestParams) * @param Request $request * @param Response $response; */ - public function run(Request $request, Response $response): static + private function runInternal(Request $request, Response $response): static { - $this->setResource('request', fn () => $request); - $this->setResource('response', fn () => $response); + if ($this->compression) { + $response->setAcceptEncoding($request->getHeader('accept-encoding', '')); + $response->setCompressionMinSize($this->compressionMinSize); + $response->setCompressionSupported($this->compressionSupported); + } + + $this->setRequestResource('request', fn () => $request); + $this->setRequestResource('response', fn () => $response); try { foreach (self::$requestHooks as $hook) { @@ -747,7 +872,7 @@ public function run(Request $request, Response $response): static \call_user_func_array($hook->getAction(), $arguments); } } catch (\Exception $e) { - $this->setResource('error', fn () => $e, []); + $this->setRequestResource('error', fn () => $e, []); foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { @@ -777,7 +902,7 @@ public function run(Request $request, Response $response): static $route = $this->match($request); $groups = ($route instanceof Route) ? $route->getGroups() : []; - $this->setResource('route', fn () => $route, []); + $this->setRequestResource('route', fn () => $route, []); if (self::REQUEST_METHOD_HEAD == $method) { $method = self::REQUEST_METHOD_GET; @@ -805,7 +930,7 @@ public function run(Request $request, Response $response): static foreach (self::$errors as $error) { // Global error hooks /** @var Hook $error */ if (\in_array('*', $error->getGroups())) { - $this->setResource('error', function () use ($e) { + $this->setRequestResource('error', function () use ($e) { return $e; }, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); @@ -822,7 +947,7 @@ public function run(Request $request, Response $response): static $path = \parse_url($request->getURI(), PHP_URL_PATH); $route->path($path); - $this->setResource('route', fn () => $route, []); + $this->setRequestResource('route', fn () => $route, []); } if (null !== $route) { @@ -845,7 +970,7 @@ public function run(Request $request, Response $response): static } catch (\Throwable $e) { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setResource('error', function () use ($e) { + $this->setRequestResource('error', function () use ($e) { return $e; }, []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); @@ -855,7 +980,7 @@ public function run(Request $request, Response $response): static } else { foreach (self::$errors as $error) { // Global error hooks if (\in_array('*', $error->getGroups())) { - $this->setResource('error', fn () => new Exception('Not Found', 404), []); + $this->setRequestResource('error', fn () => new Exception('Not Found', 404), []); \call_user_func_array($error->getAction(), $this->getArguments($error, [], $request->getParams())); } } @@ -886,7 +1011,7 @@ protected function validate(string $key, array $param, mixed $value): void $validator = $param['validator']; // checking whether the class exists if (\is_callable($validator)) { - $validator = \call_user_func_array($validator, $this->getResources($param['injections'])); + $validator = \call_user_func_array($validator, \array_values($this->getResources($param['injections']))); } if (!$validator instanceof Validator) { // is the validator object an instance of the Validator class diff --git a/src/Http/Request.php b/src/Http/Request.php index c9762123..1bc9db0b 100755 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -46,6 +46,8 @@ abstract class Request */ protected $headers = null; + protected array $trustedIpHeaders = []; + /** * Get Param * @@ -137,6 +139,24 @@ abstract public function getServer(string $key, ?string $default = null): ?strin */ abstract public function setServer(string $key, string $value): static; + /** + * Set Trusted IP Headers + * + * Set which headers to trust for determining client IP address. + * Headers are checked in order; the first one found with a valid IP is used. + * + * @param array $headers + * @return static + */ + public function setTrustedIpHeaders(array $headers): static + { + $normalized = \array_map('strtolower', $headers); + $trimmed = \array_map('trim', $normalized); + $this->trustedIpHeaders = \array_filter($trimmed); + + return $this; + } + /** * Get IP * diff --git a/src/Http/Response.php b/src/Http/Response.php index 444bc4f8..9d5edf0d 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 { /** @@ -183,17 +185,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'; @@ -246,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. * @@ -256,6 +327,58 @@ 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 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; + } + /** * Set content type * @@ -482,50 +605,75 @@ public function send(string $body = ''): void return; } - $this->sent = true; + $this->appendCookies(); - $this->addHeader('X-Debug-Speed', (string) (\microtime(true) - $this->startTime)); + $hasContentEncoding = false; + foreach ($this->headers as $name => $values) { + if (\strtolower($name) === 'content-encoding') { + $hasContentEncoding = true; + break; + } + } - $this - ->appendCookies() - ->appendHeaders(); + // Compress body only if all conditions are met: + if ( + !$hasContentEncoding && + !empty($this->acceptEncoding) && + $this->isCompressible($this->contentType) && + strlen($body) > $this->compressionMinSize + ) { + $algorithm = Compression::fromAcceptEncoding($this->acceptEncoding, $this->compressionSupported); + + if ($algorithm) { + $body = $algorithm->compress($body); + $this->removeHeader('Content-Length'); + $this->addHeader('Content-Length', (string) \strlen($body)); + $this->addHeader('Content-Encoding', $algorithm->getContentEncoding()); + $this->addHeader('X-Utopia-Compression', 'true'); + $this->addHeader('Vary', 'Accept-Encoding'); + } + } - $this->headersSent = true; + $this->addHeader('X-Debug-Speed', (string) (microtime(true) - $this->startTime)); + $this->appendHeaders(); - if (!$this->disablePayload) { - $length = strlen($body); - - $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); + // Send response + if ($this->disablePayload) { + $this->end(); + $this->sent = true; + + return; + } + + $headersSize = 0; + foreach ($this->headers as $name => $values) { + if (\is_array($values)) { + foreach ($values as $value) { + $headersSize += \strlen($name . ': ' . $value); } - } - $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 - $this->end($body); + $headersSize += (\count($values) - 1) * 2; // linebreaks } else { - for ($i = 0; $i < ceil($length / self::CHUNK_SIZE); $i++) { - $this->write(substr($body, ($i * self::CHUNK_SIZE), min(self::CHUNK_SIZE, $length - ($i * self::CHUNK_SIZE)))); - } - - $this->end(); + $headersSize += \strlen($name . ': ' . $values); } + } + $headersSize += (\count($this->headers) - 1) * 2; // linebreaks + + $bodyLength = strlen($body); + $this->size += $headersSize + $bodyLength; - $this->disablePayload(); + if ($bodyLength <= self::CHUNK_SIZE) { + $this->end($body); } else { + $chunks = str_split($body, self::CHUNK_SIZE); + foreach ($chunks as $chunk) { + $this->write($chunk); + } $this->end(); } + + $this->sent = true; + + $this->disablePayload(); } /** diff --git a/src/Http/Route.php b/src/Http/Route.php index 8a7d0620..1ce2f111 100755 --- a/src/Http/Route.php +++ b/src/Http/Route.php @@ -2,6 +2,8 @@ namespace Utopia\Http; +use Utopia\Servers\Hook; + class Route extends Hook { /** @@ -28,7 +30,7 @@ class Route extends Hook /** * Path params. * - * @var array + * @var array> */ protected array $pathParams = []; @@ -46,13 +48,25 @@ class Route extends Hook */ protected int $order; + protected string $matchedPath = ''; + public function __construct(string $method, string $path) { + parent::__construct(); $this->path($path); $this->method = $method; $this->order = ++self::$counter; - $this->action = function (): void { - }; + } + + public function setMatchedPath(string $path): self + { + $this->matchedPath = $path; + return $this; + } + + public function getMatchedPath(): string + { + return $this->matchedPath; } /** @@ -141,23 +155,30 @@ 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; } /** * Get path params. * * @param \Utopia\Http\Request $request + * @param string $path * @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 d3324553..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; } @@ -123,7 +127,7 @@ public static function match(string $method, string $path): Route|null return null; } - $parts = array_values(array_filter(explode('/', $path))); + $parts = array_values(array_filter(explode('/', $path), fn ($segment) => $segment !== '')); $length = count($parts) - 1; $filteredParams = array_filter(self::$params, fn ($i) => $i <= $length); @@ -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/View.php b/src/Http/View.php new file mode 100644 index 00000000..5a223409 --- /dev/null +++ b/src/Http/View.php @@ -0,0 +1,342 @@ +setPath($path) method + * + * @param string $path + * + * @throws Exception + */ + public function __construct(string $path = '') + { + $this->setPath($path); + + $this + ->addFilter(self::FILTER_ESCAPE, function (string $value) { + return \htmlentities($value, ENT_QUOTES, 'UTF-8'); + }) + ->addFilter(self::FILTER_NL2P, function (string $value) { + $paragraphs = ''; + + foreach (\explode("\n\n", $value) as $line) { + if (\trim($line)) { + $paragraphs .= '

'.$line.'

'; + } + } + + $paragraphs = \str_replace("\n", '
', $paragraphs); + + return $paragraphs; + }); + } + + /** + * Set param + * + * Assign a parameter by key + * + * @param string $key + * @param mixed $value + * + * @throws Exception + */ + public function setParam(string $key, mixed $value, bool $escapeHtml = true): static + { + if (\strpos($key, '.') !== false) { + throw new Exception('$key can\'t contain a dot "." character'); + } + + if (is_string($value) && $escapeHtml) { + $value = \htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + + $this->params[$key] = $value; + + return $this; + } + + /** + * Set parent View object conatining this object + * + * @param self $view + */ + public function setParent(self $view): static + { + $this->parent = $view; + + return $this; + } + + /** + * Return a View instance of the parent view containing this view + * + * @return self|null + */ + public function getParent(): ?self + { + if (!empty($this->parent)) { + return $this->parent; + } + + return null; + } + + /** + * Get param + * + * Returns an assigned parameter by its key or $default if param key doesn't exists + * + * @param string $path + * @param mixed $default (optional) + * @return mixed + */ + public function getParam(string $path, mixed $default = null): mixed + { + $path = \explode('.', $path); + $temp = $this->params; + + foreach ($path as $key) { + $temp = (isset($temp[$key])) ? $temp[$key] : null; + + if (null !== $temp) { + $value = $temp; + } else { + return $default; + } + } + + return $value; + } + + /** + * Set path + * + * Set object template path that will be used to render view output + * + * @param string $path + * + * @throws Exception + */ + public function setPath(string $path): static + { + $this->path = $path; + + return $this; + } + + /** + * Set rendered + * + * By enabling rendered state to true, the object will not render its template and will return an empty string instead + * + * @param bool $state + */ + public function setRendered(bool $state = true): static + { + $this->rendered = $state; + + return $this; + } + + /** + * Is rendered + * + * Return whether current View rendering state is set to true or false + * + * @return bool + */ + public function isRendered(): bool + { + return $this->rendered; + } + + /** + * Add Filter + * + * @param string $name + * @param callable $callback + */ + public function addFilter(string $name, callable $callback): static + { + $this->filters[$name] = $callback; + + return $this; + } + + /** + * Output and filter value + * + * @param mixed $value + * @param string|array $filter + * @return mixed + * + * @throws Exception + */ + public function print(mixed $value, string|array $filter = ''): mixed + { + if (!empty($filter)) { + if (\is_array($filter)) { + foreach ($filter as $callback) { + if (!isset($this->filters[$callback])) { + throw new Exception('Filter "'.$callback.'" is not registered'); + } + + $value = $this->filters[$callback]($value); + } + } else { + if (!isset($this->filters[$filter])) { + throw new Exception('Filter "'.$filter.'" is not registered'); + } + + $value = $this->filters[$filter]($value); + } + } + + return $value; + } + + /** + * Render + * + * Render view .phtml template file if template has not been set as rendered yet using $this->setRendered(true). + * In case path is not readable throws Exception. + * + * @param bool $minify + * @return string + * + * @throws Exception + */ + public function render(bool $minify = true): string + { + if ($this->rendered) { // Don't render any template + return ''; + } + + \ob_start(); //Start of build + + if (\is_readable($this->path)) { + /** + * Include template file + * + * @psalm-suppress UnresolvableInclude + */ + include $this->path; + } else { + \ob_end_clean(); + throw new Exception('"'.$this->path.'" view template is not readable'); + } + + $html = \ob_get_contents(); + + \ob_end_clean(); //End of build + + if ($minify) { + // Searching textarea and pre + \preg_match_all('#\.*\<\/textarea\>#Uis', $html, $foundTxt); + \preg_match_all('#\.*\<\/pre\>#Uis', $html, $foundPre); + + // replacing both with /
$index
+ $html = \str_replace($foundTxt[0], \array_map(function ($el) { + return ''; + }, \array_keys($foundTxt[0])), $html); + $html = \str_replace($foundPre[0], \array_map(function ($el) { + return '
'.$el.'
'; + }, \array_keys($foundPre[0])), $html); + + // your stuff + $search = [ + '/\>[^\S ]+/s', // strip whitespaces after tags, except space + '/[^\S ]+\', + '<', + '\\1', + ]; + + $html = \preg_replace($search, $replace, $html) ?? $html; + + // Replacing back with content + $html = \str_replace(\array_map(function ($el) { + return ''; + }, \array_keys($foundTxt[0])), $foundTxt[0], $html); + $html = \str_replace(\array_map(function ($el) { + return '
'.$el.'
'; + }, \array_keys($foundPre[0])), $foundPre[0], $html); + } + + return $html; + } + + /* View Helpers */ + + /** + * Exec + * + * Exec child View components + * + * @param array|self $view + * @return string + * + * @throws Exception + */ + public function exec($view): string + { + $output = ''; + + if (\is_array($view)) { + foreach ($view as $node) { /* @var $node self */ + if ($node instanceof self) { + $node->setParent($this); + $output .= $node->render(); + } + } + } + + if ($view instanceof self) { + $view->setParent($this); + $output = $view->render(); + } + + return $output; + } +} diff --git a/tests/HookTest.php b/tests/HookTest.php deleted file mode 100644 index 49a0afd5..00000000 --- a/tests/HookTest.php +++ /dev/null @@ -1,101 +0,0 @@ -hook = new Hook(); - } - - public function testDescriptionCanBeSet() - { - $this->assertEquals('', $this->hook->getDesc()); - - $this->hook->desc('new hook'); - - $this->assertEquals('new hook', $this->hook->getDesc()); - } - - public function testGroupsCanBeSet() - { - $this->assertEquals([], $this->hook->getGroups()); - - $this->hook->groups(['api', 'homepage']); - - $this->assertEquals(['api', 'homepage'], $this->hook->getGroups()); - } - - public function testActionCanBeSet() - { - $this->assertEquals(function () { - }, $this->hook->getAction()); - - $this->hook->action(fn () => 'hello world'); - - $this->assertEquals('hello world', $this->hook->getAction()()); - } - - public function testParamCanBeSet() - { - $this->assertEquals([], $this->hook->getParams()); - - $this->hook - ->param('x', '', new Text(10)) - ->param('y', '', new Text(10)); - - $this->assertCount(2, $this->hook->getParams()); - } - - public function testResourcesCanBeInjected() - { - $this->assertEquals([], $this->hook->getInjections()); - - $this->hook - ->inject('user') - ->inject('time') - ->action(function () { - }); - - $this->assertCount(2, $this->hook->getInjections()); - $this->assertEquals('user', $this->hook->getInjections()['user']['name']); - $this->assertEquals('time', $this->hook->getInjections()['time']['name']); - } - - public function testParamValuesCanBeSet() - { - $this->assertEquals([], $this->hook->getParams()); - - $values = [ - 'x' => 'hello', - 'y' => 'world', - ]; - - $this->hook - ->param('x', '', new Numeric()) - ->param('y', '', new Numeric()); - - foreach ($this->hook->getParams() as $key => $param) { - $this->hook->setParamValue($key, $values[$key]); - } - - $this->assertCount(2, $this->hook->getParams()); - $this->assertEquals('hello', $this->hook->getParams()['x']['value']); - $this->assertEquals('world', $this->hook->getParams()['y']['value']); - } - - public function tearDown(): void - { - $this->hook = null; - } -} diff --git a/tests/e2e/server-swoole.php b/tests/e2e/server-swoole.php index e15e1be4..f3a6b073 100755 --- a/tests/e2e/server-swoole.php +++ b/tests/e2e/server-swoole.php @@ -7,8 +7,6 @@ use Utopia\Http\Adapter\Swoole\Server; use Utopia\Http\Http; -use function Swoole\Coroutine\run; - Http::delete('/swoole-test') ->inject('swooleRequest') ->inject('swooleResponse') @@ -24,6 +22,4 @@ $server = new Server('0.0.0.0', '80'); $http = new Http($server, 'UTC'); -run(function () use ($http) { - $http->start(); -}); +$http->start();