diff --git a/Dockerfile b/Dockerfile index 2b7cbf919..661c96de0 100755 --- a/Dockerfile +++ b/Dockerfile @@ -12,8 +12,6 @@ RUN composer install \ --no-scripts \ --prefer-dist -COPY compat/composer.json compat/composer.lock /usr/local/legacy/ -RUN composer install --working-dir=/usr/local/legacy --ignore-platform-reqs --no-dev --no-plugins --no-scripts --prefer-dist FROM php:8.5.8-cli-alpine AS compile @@ -106,7 +104,6 @@ ENV DEBUG=$DEBUG WORKDIR /usr/src/code COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor -COPY --from=composer /usr/local/legacy/vendor /opt/database-legacy/vendor COPY --from=swoole /ext/swoole.so /ext/swoole.so COPY --from=redis /ext/redis.so /ext/redis.so COPY --from=pcov /ext/pcov.so /ext/pcov.so diff --git a/compat/composer.json b/compat/composer.json deleted file mode 100644 index c777c4a7e..000000000 --- a/compat/composer.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "utopia-php/database-bridge-compatibility", - "description": "Isolated stable cache-5/database-8 runtime for rolling migration tests", - "require": { - "utopia-php/database": "8.3.15", - "utopia-php/cache": "5.2.0" - }, - "config": { - "allow-plugins": false - } -} diff --git a/compat/composer.lock b/compat/composer.lock deleted file mode 100644 index 5ac82a7f7..000000000 --- a/compat/composer.lock +++ /dev/null @@ -1,2512 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "526b53034e04411168d76b88dbdf86b3", - "packages": [ - { - "name": "brick/math", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "2effe05d2177c451b86c6a073196a4034c02f211" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/2effe05d2177c451b86c6a073196a4034c02f211", - "reference": "2effe05d2177c451b86c6a073196a4034c02f211", - "shasum": "" - }, - "require": { - "php": "^8.2" - }, - "require-dev": { - "phpstan/phpstan": "2.2.13", - "phpstan/phpstan-phpunit": "2.0.18", - "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/1.0.0" - }, - "funding": [ - { - "url": "https://github.com/BenMorel", - "type": "github" - } - ], - "time": "2026-09-12T10:28:18+00:00" - }, - { - "name": "composer/semver", - "version": "3.6.0", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/d92543b7ede9c4ec6b59853cefb061e39866d2cf", - "reference": "d92543b7ede9c4ec6b59853cefb061e39866d2cf", - "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": "Version comparison 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.6.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2026-10-29T09:13:34+00:00" - }, - { - "name": "google/protobuf", - "version": "v5.36.2", - "source": { - "type": "git", - "url": "https://github.com/protocolbuffers/protobuf-php.git", - "reference": "020bc8e831611869ac387ddd38fbbadd78d15195" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/protocolbuffers/protobuf-php/zipball/020bc8e831611869ac387ddd38fbbadd78d15195", - "reference": "020bc8e831611869ac387ddd38fbbadd78d15195", - "shasum": "" - }, - "require": { - "php": ">=8.2.0" - }, - "require-dev": { - "phpunit/phpunit": ">=11.5.50 <12.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/v5.36.2" - }, - "time": "2026-09-17T15:56:42+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.10.1", - "source": { - "type": "git", - "url": "https://github.com/opentelemetry-php/api.git", - "reference": "b2bc26bf8bd6b2b6447b3626b2a1b5733873aa0b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/api/zipball/b2bc26bf8bd6b2b6447b3626b2a1b5733873aa0b", - "reference": "b2bc26bf8bd6b2b6447b3626b2a1b5733873aa0b", - "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-09-16T01:59:09+00:00" - }, - { - "name": "open-telemetry/context", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/opentelemetry-php/context.git", - "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/context/zipball/3c414b246e0dabb7d6145404e6a5e4536ca18d07", - "reference": "3c414b246e0dabb7d6145404e6a5e4536ca18d07", - "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/languages/php", - "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", - "source": "https://github.com/open-telemetry/opentelemetry-php" - }, - "time": "2025-10-19T06:44:33+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.10.0", - "source": { - "type": "git", - "url": "https://github.com/opentelemetry-php/gen-otlp-protobuf.git", - "reference": "66f04d0e448ad333033bfc7baae1aa56330be088" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/gen-otlp-protobuf/zipball/66f04d0e448ad333033bfc7baae1aa56330be088", - "reference": "66f04d0e448ad333033bfc7baae1aa56330be088", - "shasum": "" - }, - "require": { - "google/protobuf": "^3.22 || ^4.0 || ^5.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/languages/php", - "issues": "https://github.com/open-telemetry/opentelemetry-php/issues", - "source": "https://github.com/open-telemetry/opentelemetry-php" - }, - "time": "2026-06-17T12:06:32+00:00" - }, - { - "name": "open-telemetry/sdk", - "version": "1.15.0", - "source": { - "type": "git", - "url": "https://github.com/opentelemetry-php/sdk.git", - "reference": "77e1aa73850154abb86937d52a70883edc3b4547" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/77e1aa73850154abb86937d52a70883edc3b4547", - "reference": "77e1aa73850154abb86937d52a70883edc3b4547", - "shasum": "" - }, - "require": { - "ext-json": "*", - "nyholm/psr7-server": "^1.1", - "open-telemetry/api": "^1.8", - "open-telemetry/context": "^1.4", - "open-telemetry/sem-conv": "^1.38.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\\ConfigEnv\\Trace\\SpanSuppressionStrategySemConv", - "OpenTelemetry\\SDK\\ConfigEnv\\Trace\\SpanSuppressionStrategySpanKind", - "OpenTelemetry\\SDK\\ConfigEnv\\Distribution\\DistributionConfigurationSdk" - ], - "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.14.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-07-14T13:09:54+00:00" - }, - { - "name": "open-telemetry/sem-conv", - "version": "1.44.0", - "source": { - "type": "git", - "url": "https://github.com/opentelemetry-php/sem-conv.git", - "reference": "c94bc7ebacef039cda4f4c9270807dcc58b4e3af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opentelemetry-php/sem-conv/zipball/c94bc7ebacef039cda4f4c9270807dcc58b4e3af", - "reference": "c94bc7ebacef039cda4f4c9270807dcc58b4e3af", - "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-09-09T22:06:17+00:00" - }, - { - "name": "opis/closure", - "version": "4.5.0", - "source": { - "type": "git", - "url": "https://github.com/opis/closure.git", - "reference": "b97e42b95bb72d87507f5e2d137ceb239aea8d6b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/b97e42b95bb72d87507f5e2d137ceb239aea8d6b", - "reference": "b97e42b95bb72d87507f5e2d137ceb239aea8d6b", - "shasum": "" - }, - "require": { - "php": "^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "Opis\\Closure\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary data.", - "homepage": "https://opis.io/closure", - "keywords": [ - "anonymous classes", - "anonymous functions", - "closure", - "function", - "serializable", - "serialization", - "serialize" - ], - "support": { - "issues": "https://github.com/opis/closure/issues", - "source": "https://github.com/opis/closure/tree/4.5.0" - }, - "time": "2026-03-05T13:32:42+00:00" - }, - { - "name": "php-http/discovery", - "version": "1.20.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/discovery.git", - "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", - "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": "^7.1 || ^8.0" - }, - "conflict": { - "nyholm/psr7": "<1.0", - "zendframework/zend-diactoros": "*" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "*", - "psr/http-factory-implementation": "*", - "psr/http-message-implementation": "*" - }, - "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "graham-campbell/phpspec-skip-example-extension": "^5.0", - "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0", - "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "sebastian/comparator": "^3.0.5 || ^4.0.8", - "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" - }, - "type": "composer-plugin", - "extra": { - "class": "Http\\Discovery\\Composer\\Plugin", - "plugin-optional": true - }, - "autoload": { - "psr-4": { - "Http\\Discovery\\": "src/" - }, - "exclude-from-classmap": [ - "src/Composer/Plugin.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", - "homepage": "http://php-http.org", - "keywords": [ - "adapter", - "client", - "discovery", - "factory", - "http", - "message", - "psr17", - "psr7" - ], - "support": { - "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.20.0" - }, - "time": "2024-10-02T11:20:13+00:00" - }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, - { - "name": "psr/http-client", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", - "shasum": "" - }, - "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", - "keywords": [ - "http", - "http-client", - "psr", - "psr-18" - ], - "support": { - "source": "https://github.com/php-fig/http-client" - }, - "time": "2023-09-23T14:17:50+00:00" - }, - { - "name": "psr/http-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", - "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-factory" - }, - "time": "2024-04-15T12:06:14+00:00" - }, - { - "name": "psr/http-message", - "version": "2.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", - "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" - ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" - }, - "time": "2023-04-04T09:54:51+00:00" - }, - { - "name": "psr/log", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" - }, - "time": "2024-09-11T13:17:53+00:00" - }, - { - "name": "ramsey/collection", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/ramsey/collection.git", - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "require-dev": { - "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.45", - "fakerphp/faker": "^1.24", - "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^2.1", - "mockery/mockery": "^1.6", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4", - "phpspec/prophecy-phpunit": "^2.3", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-mockery": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5", - "ramsey/coding-standard": "^2.3", - "ramsey/conventional-commits": "^1.6", - "roave/security-advisories": "dev-latest" - }, - "type": "library", - "extra": { - "captainhook": { - "force-install": true - }, - "ramsey/conventional-commits": { - "configFile": "conventional-commits.json" - } - }, - "autoload": { - "psr-4": { - "Ramsey\\Collection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - } - ], - "description": "A PHP library for representing and manipulating collections.", - "keywords": [ - "array", - "collection", - "hash", - "map", - "queue", - "set" - ], - "support": { - "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.1" - }, - "time": "2025-03-22T05:38:12+00:00" - }, - { - "name": "ramsey/uuid", - "version": "4.9.4", - "source": { - "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2", - "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2", - "shasum": "" - }, - "require": { - "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", - "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.4" - }, - "time": "2026-09-16T11:39:30+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", - "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-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.7.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": "2026-06-05T06:23:12+00:00" - }, - { - "name": "symfony/http-client", - "version": "v7.4.22", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "9dd7ba9dc326b37d7f6e19588de79b5d91b20d66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/9dd7ba9dc326b37d7f6e19588de79b5d91b20d66", - "reference": "9dd7ba9dc326b37d7f6e19588de79b5d91b20d66", - "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.22" - }, - "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-10-26T21:52:50+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/35be0019e2c2c9fba80f9dc033290a5240f7b44f", - "reference": "35be0019e2c2c9fba80f9dc033290a5240f7b44f", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.7-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.7.3" - }, - "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-08-04T08:41:16+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.43.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/122889ed24dc000165bcbfd77ec412f15cbdfc6a", - "reference": "122889ed24dc000165bcbfd77ec412f15cbdfc6a", - "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.43.3" - }, - "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-10-28T09:35:49+00:00" - }, - { - "name": "symfony/polyfill-php82", - "version": "v1.43.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php82.git", - "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", - "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", - "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.43.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": "2026-09-25T15:50:05+00:00" - }, - { - "name": "symfony/polyfill-php83", - "version": "v1.43.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce", - "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce", - "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.43.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": "2026-09-25T15:50:05+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.7.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", - "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", - "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.7-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.7.3" - }, - "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-07-27T15:39:01+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/async", - "version": "0.2.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/async.git", - "reference": "c7925a2ad7cf0fceebb3f93200c95a269126f692" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/async/zipball/c7925a2ad7cf0fceebb3f93200c95a269126f692", - "reference": "c7925a2ad7cf0fceebb3f93200c95a269126f692", - "shasum": "" - }, - "require": { - "opis/closure": "4.*", - "php": ">=8.1" - }, - "require-dev": { - "amphp/amp": "3.*", - "amphp/parallel": "2.*", - "amphp/process": "^2.0", - "laravel/pint": "1.*", - "phpstan/phpstan": "2.*", - "phpunit/phpunit": "11.5.45", - "react/child-process": "0.*", - "react/event-loop": "1.*", - "swoole/ide-helper": "*" - }, - "suggest": { - "amphp/amp": "Required for Amp promise adapter", - "amphp/parallel": "Required for Amp parallel adapter", - "ext-ev": "Required for ReactPHP event loop (recommended for best performance)", - "ext-parallel": "Required for parallel adapter (requires PHP ZTS build)", - "ext-sockets": "Required for Swoole Process adapter", - "ext-swoole": "Required for Swoole Thread and Process adapters (recommended for best performance)", - "react/child-process": "Required for ReactPHP parallel adapter", - "react/event-loop": "Required for ReactPHP promise and parallel adapters" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Async\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Appwrite Team", - "email": "team@appwrite.io" - } - ], - "description": "High-performance concurrent + parallel library with Promise and Parallel execution support for PHP.", - "support": { - "issues": "https://github.com/utopia-php/async/issues", - "source": "https://github.com/utopia-php/async/tree/0.2.0" - }, - "time": "2026-09-29T14:07:22+00:00" - }, - { - "name": "utopia-php/cache", - "version": "5.2.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/cache.git", - "reference": "05a631796066a7cf96da289df358909fa2190ebd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/05a631796066a7cf96da289df358909fa2190ebd", - "reference": "05a631796066a7cf96da289df358909fa2190ebd", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=8.4", - "utopia-php/circuit-breaker": "^0.4", - "utopia-php/pools": "^2.0", - "utopia-php/telemetry": "^0.4" - }, - "require-dev": { - "swoole/ide-helper": "^6.0" - }, - "suggest": { - "ext-igbinary": "Required for the Igbinary codec.", - "ext-memcached": "Required for the Memcached and Hazelcast adapters.", - "ext-redis": "Required for the Redis, RedisCluster and Sharding adapters.", - "ext-swoole": "Required for the multiplexing Redis adapter (>=6.0)." - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Team Appwrite", - "email": "team@appwrite.io" - } - ], - "description": "A simple cache library to manage application cache storing, loading and purging", - "keywords": [ - "cache", - "framework", - "php", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/5.2.0" - }, - "time": "2026-10-19T05:47:09+00:00" - }, - { - "name": "utopia-php/circuit-breaker", - "version": "0.4.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/circuit-breaker.git", - "reference": "51c9b9b0de6754af4da6364da0f3a5d7e940ef2c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/circuit-breaker/zipball/51c9b9b0de6754af4da6364da0f3a5d7e940ef2c", - "reference": "51c9b9b0de6754af4da6364da0f3a5d7e940ef2c", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "require-dev": { - "utopia-php/telemetry": "^0.4.6" - }, - "suggest": { - "ext-opentelemetry": "Required by utopia-php/telemetry when using OpenTelemetry metrics.", - "ext-protobuf": "Required by utopia-php/telemetry when using OpenTelemetry metrics.", - "ext-redis": "Required when using Utopia\\CircuitBreaker\\Adapter\\Redis with the phpredis extension.", - "ext-swoole": "Required when using Utopia\\CircuitBreaker\\Adapter\\SwooleTable.", - "utopia-php/telemetry": "Required when passing telemetry adapters or running the local telemetry demo." - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\CircuitBreaker\\": "src/CircuitBreaker/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Team Appwrite", - "email": "team@appwrite.io" - } - ], - "description": "Light & simple Circuit Breaker for PHP to prevent cascading failures in distributed systems.", - "keywords": [ - "circuit-breaker", - "fault-tolerance", - "framework", - "php", - "resilience", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/circuit-breaker/issues", - "source": "https://github.com/utopia-php/circuit-breaker/tree/0.4.1" - }, - "time": "2026-09-16T10:12:28+00:00" - }, - { - "name": "utopia-php/console", - "version": "0.1.1", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/console.git", - "reference": "d298e43960780e6d76e66de1228c75dc81220e3e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/console/zipball/d298e43960780e6d76e66de1228c75dc81220e3e", - "reference": "d298e43960780e6d76e66de1228c75dc81220e3e", - "shasum": "" - }, - "require": { - "php": ">=8.0" - }, - "require-dev": { - "laravel/pint": "1.2.*", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.3", - "squizlabs/php_codesniffer": "^3.6", - "swoole/ide-helper": "4.8.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Console helpers for logging, prompting, and executing commands", - "keywords": [ - "cli", - "console", - "php", - "terminal", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/console/issues", - "source": "https://github.com/utopia-php/console/tree/0.1.1" - }, - "time": "2026-02-10T10:20:29+00:00" - }, - { - "name": "utopia-php/database", - "version": "8.3.15", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/database.git", - "reference": "bc00297bd34c1a1311cebb723ab3810563324908" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/bc00297bd34c1a1311cebb723ab3810563324908", - "reference": "bc00297bd34c1a1311cebb723ab3810563324908", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "ext-mongodb": "*", - "ext-pdo": "*", - "ext-redis": "*", - "php": ">=8.5", - "utopia-php/async": "^0.2", - "utopia-php/cache": "^5.2", - "utopia-php/console": "^0.1 || ^0.2", - "utopia-php/mongo": "^1.5.4", - "utopia-php/pools": "^2.0", - "utopia-php/query": "^0.7.2", - "utopia-php/validators": "^1.0 || ^2.0" - }, - "require-dev": { - "brianium/paratest": "^7.20", - "fakerphp/faker": "^1.23", - "laravel/pint": "^1.29", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^12.5", - "swoole/ide-helper": "5.1.3", - "utopia-php/cli": "^0.24.4" - }, - "suggest": { - "ext-pdo": "Needed to support MariaDB, MySQL or SQLite Database Adapter", - "ext-redis": "Needed to support Redis Cache Adapter", - "ext-swoole": "Needed to scope lifecycle hook silencing per coroutine and to detect lost connections" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "8.0.x-dev", - "dev-feat-query-lib": "8.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Utopia\\Database\\": "src/Database" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A simple library to manage application persistence using multiple database adapters", - "keywords": [ - "database", - "framework", - "php", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/8.3.15" - }, - "time": "2026-10-23T00:11:16+00:00" - }, - { - "name": "utopia-php/mongo", - "version": "1.5.4", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/mongo.git", - "reference": "34ccfd6e81d76fd6ee9f26313bfad42c95b9ce88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/34ccfd6e81d76fd6ee9f26313bfad42c95b9ce88", - "reference": "34ccfd6e81d76fd6ee9f26313bfad42c95b9ce88", - "shasum": "" - }, - "require": { - "ext-mongodb": "^2.1.0", - "php": ">=8.0", - "ramsey/uuid": "4.9.*" - }, - "require-dev": { - "fakerphp/faker": "1.*", - "laravel/pint": "*", - "phpstan/phpstan": "*", - "phpunit/phpunit": "9.*", - "swoole/ide-helper": "5.1.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Mongo\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eldad Fux", - "email": "eldad@appwrite.io" - }, - { - "name": "Wess", - "email": "wess@appwrite.io" - } - ], - "description": "A simple library to manage Mongo database", - "keywords": [ - "database", - "mongo", - "php", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.5.4" - }, - "time": "2026-09-09T05:42:37+00:00" - }, - { - "name": "utopia-php/pools", - "version": "2.1.2", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/pools.git", - "reference": "a45d867e56de82a2eb6e1eb3ea78d4c2f6059f4a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/pools/zipball/a45d867e56de82a2eb6e1eb3ea78d4c2f6059f4a", - "reference": "a45d867e56de82a2eb6e1eb3ea78d4c2f6059f4a", - "shasum": "" - }, - "require": { - "php": ">=8.4", - "utopia-php/telemetry": "^0.4.6" - }, - "require-dev": { - "swoole/ide-helper": "6.*" - }, - "suggest": { - "ext-mongodb": "Needed to support MongoDB database pools", - "ext-pdo": "Needed to support MariaDB, MySQL or SQLite database pools", - "ext-redis": "Needed to support Redis cache pools", - "ext-swoole": "Needed to support Swoole based pool adapter" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Pools\\": "src/Pools" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Team Appwrite", - "email": "team@appwrite.io" - } - ], - "description": "A simple library to manage connection pools", - "keywords": [ - "framework", - "php", - "pools", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/pools/issues", - "source": "https://github.com/utopia-php/pools/tree/2.1.2" - }, - "time": "2026-09-07T07:02:57+00:00" - }, - { - "name": "utopia-php/query", - "version": "0.7.2", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/query.git", - "reference": "945ef1c9f1c863b0e6e8bbac4c81c1b667dbfc9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/query/zipball/945ef1c9f1c863b0e6e8bbac4c81c1b667dbfc9c", - "reference": "945ef1c9f1c863b0e6e8bbac4c81c1b667dbfc9c", - "shasum": "" - }, - "require": { - "php": ">=8.4" - }, - "require-dev": { - "mongodb/mongodb": "^2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Query\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A simple library providing a query abstraction for filtering, ordering, and pagination", - "keywords": [ - "framework", - "php", - "query", - "upf", - "utopia" - ], - "support": { - "issues": "https://github.com/utopia-php/query/issues", - "source": "https://github.com/utopia-php/query/tree/0.7.2" - }, - "time": "2026-10-16T10:52:31+00:00" - }, - { - "name": "utopia-php/telemetry", - "version": "0.4.12", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/telemetry.git", - "reference": "e628707fe262dc0ca418fbe6f5e421f2db719907" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/telemetry/zipball/e628707fe262dc0ca418fbe6f5e421f2db719907", - "reference": "e628707fe262dc0ca418fbe6f5e421f2db719907", - "shasum": "" - }, - "require": { - "nyholm/psr7": "1.*", - "open-telemetry/exporter-otlp": "1.*", - "open-telemetry/sdk": "1.*", - "php": ">=8.0", - "symfony/http-client": "7.*" - }, - "require-dev": { - "swoole/ide-helper": "6.*" - }, - "suggest": { - "ext-protobuf": "For better performance, when dealing with the protobuf format", - "ext-sockets": "Required for the Swoole transport implementation", - "ext-swoole": "Required for the Swoole transport implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Utopia\\Telemetry\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A lite & fast telemetry library, with adapters for OpenTelemetry", - "keywords": [ - "framework", - "php", - "upf" - ], - "support": { - "issues": "https://github.com/utopia-php/telemetry/issues", - "source": "https://github.com/utopia-php/telemetry/tree/0.4.12" - }, - "time": "2026-09-28T10:14:23+00:00" - }, - { - "name": "utopia-php/validators", - "version": "2.0.0", - "source": { - "type": "git", - "url": "https://github.com/utopia-php/validators.git", - "reference": "4c6e421fb84333eec58d28595e645a77d4ed8ac5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/4c6e421fb84333eec58d28595e645a77d4ed8ac5", - "reference": "4c6e421fb84333eec58d28595e645a77d4ed8ac5", - "shasum": "" - }, - "require": { - "php": ">=8.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/compat.php" - ], - "psr-4": { - "Utopia\\Validator\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A lightweight collection of reusable validators for Utopia projects", - "keywords": [ - "php", - "utopia", - "validation", - "validator" - ], - "support": { - "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/2.0.0" - }, - "time": "2026-10-08T14:16:57+00:00" - } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.9.0" -} diff --git a/compat/runtime.php b/compat/runtime.php deleted file mode 100644 index 87ba0ed23..000000000 --- a/compat/runtime.php +++ /dev/null @@ -1,48 +0,0 @@ -connect(getenv('DATABASE_TEST_CACHE_HOST') ?: 'redis', (int) (getenv('DATABASE_TEST_CACHE_PORT') ?: 6379)); -$cache = new Cache(new RedisCache($redis)); -$database = new Database(new SQLite(new PDO('sqlite:'.$arguments['path'])), $cache); -$database->setDatabase('bridge')->setNamespace($arguments['namespace']); -$database->setQueryCache(new QueryCache($cache)); - -$result = match ($arguments['action']) { - 'init' => (function () use ($database): bool { - $database->create(); - $database->createCollection(Collection::create(id: 'users', attributes: [Attribute::string(key: 'name')], permissions: [Permission::read(Role::any()), Permission::create(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())])); - $database->createDocument('users', new Document(['$id' => 'user', 'name' => 'original'])); - return true; - })(), - 'get' => $database->getDocument('users', $arguments['id'] ?? 'user')->getAttribute('name'), - 'find' => array_map(fn (Document $document) => $document->getAttribute('name'), $database->find('users')), - 'update' => $database->updateDocument('users', 'user', new Document(['name' => $arguments['value']]))->getAttribute('name'), - 'create' => $database->createDocument('users', new Document(['$id' => $arguments['id'], 'name' => $arguments['value']]))->getAttribute('name'), - 'delete' => $database->deleteDocument('users', $arguments['id']), - 'attributes' => array_map(fn (Document $attribute) => $attribute->getId(), $database->getCollection('users')->getAttribute('attributes')), - 'hold' => $database->withTransaction(function () use ($database, $arguments): string { - $database->updateDocument('users', 'user', new Document(['name' => $arguments['value']])); - echo "ready\n"; - fflush(STDOUT); - if (fgets(STDIN) !== "commit\n") { - throw new RuntimeException('Commit signal missing'); - } - return $arguments['value']; - }), -}; -echo json_encode($result, JSON_THROW_ON_ERROR)."\n"; diff --git a/composer.json b/composer.json index 14d7d38fa..aaf0f5c82 100755 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ "ext-redis": "*", "utopia-php/validators": "^1.0 || ^2.0", "utopia-php/console": "^0.1 || ^0.2", - "utopia-php/cache": "^6.1.3", + "utopia-php/cache": "^6.1.3 || ^7.0", "utopia-php/pools": "^2.0", "utopia-php/mongo": "^1.5.4", "utopia-php/query": "^0.7.2", diff --git a/composer.lock b/composer.lock index cfd9d71a3..538f4e99f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b7512f500bd39b9dfd1ae09d62503d71", + "content-hash": "c0320da830c616dea4462aac1f346f07", "packages": [ { "name": "brick/math", diff --git a/docs/cache-invalidation-state.md b/docs/cache-invalidation-state.md index 6e9a09d8f..d1eb9dd16 100644 --- a/docs/cache-invalidation-state.md +++ b/docs/cache-invalidation-state.md @@ -4,28 +4,14 @@ The collection's CAS version is its epoch. Its single evictable record contains The same opaque versions fence document, definition and query fills. CAS returns the exact identity it committed, or false for a version conflict. Errors and open circuit breakers throw; they are never treated as eviction. Expiry checks use the caller's writer timeout; a reader with a shorter timeout does not delete another caller's live registration. Later writer completion still retires fills made after its timeout. -This requires cache 6.x; the old generation API and compatibility protocol are removed. Redis, Redis multiplexing and Memory support versioning. Database document/query/withCache caching is bypassed on unsupported adapters; ordinary database operations continue. The old cache-5 compatibility CI tier is removed because it is no longer a supported dependency. - -A direct upgrade to 9.0 requires draining legacy writers and clearing disposable cache data. For a rolling migration without a regional stop, use the explicit bridge below instead. No additional Dragonfly or coordination DSN is needed. +This requires the opaque-version API in cache 6.1.3 or 7.x; the old generation API and compatibility protocol are removed. Redis, Redis multiplexing and Memory support versioning. Database document/query/withCache caching is bypassed on unsupported adapters; ordinary database operations continue. The old cache-5 compatibility CI tier is removed because it is no longer a supported dependency. A successful post-commit invalidation remains necessary. Durable replay of completions lost during a cache outage is separate work. A cache timeout/OOM must never be interpreted as an empty writer record. -## Rolling migration (database 9.1 / cache 6.1.1) - -Use cache 6.1.1 or later: ordinary legacy registrations must bypass the conditional-fill grace window. Pass the same `Cache\Bridge` instance to Database and its query cache. `Bridge::wrap($cache, $mode)` accepts an empty mode for the ordinary v2 path, or one of three migration phases: - -| Phase | Reads | Mutation registration/completion and purges | -| --- | --- | --- | -| `legacy` | Existing cache-5 keyspace and wire format | Both protocols | -| `dual` | Isolated `v2:` keyspace | Both protocols | -| `v2` | Same `v2:` keyspace | V2 only | - -Roll every cache user through `legacy` before enabling `dual`. Old APIs, enterprise/storage pools, realtime/MQTT, workers, schedules and one-off scripts must have terminated gracefully, including their in-flight SQL transactions. Readiness alone does not prove that barrier. Never enable a v2 reader alongside an unmodified legacy-only writer sharing its cache domain. The wrapper does not discover fleet membership; the deployment operator must verify each barrier. - -During `dual`, either bridge read mode can coexist and rollback to `legacy` remains safe. Once every reader uses v2 and the rollback window closes, roll `v2`. After the first v2-only writer, rolling back to legacy readers is unsafe because their entries are no longer maintained; rollback to `dual` with v2 reads remains possible. Preserve the `v2:` namespace during eventual bridge removal. +## Completing the rolling migration -No additional Dragonfly, whole-cache flush or regional shutdown is needed. Use API surge capacity, `maxUnavailable: 0`, readiness and graceful draining; hand workers over using their normal acknowledgement/retry semantics. Warm the new namespace gradually and budget memory for both keyspaces. Watch SQL load, cache evictions, errors, latency and queue age. Cross-region semantic purge delivery must stay available and be tested in both directions. +The temporary database 9.1 bridge, legacy writer bookkeeping and released cache-5 compatibility runtime are removed. Finish that migration before upgrading: every reader must already use v2, and all legacy-reading Jobs and draining processes must have exited. -Compatibility is deliberately limited to the Redis wire protocol (including multiplexing, pools, sharding and circuit breakers). Legacy fills retain numeric leases only inside the explicit facade; v2 remains opaque CAS with no fallback. No opaque version fields are written into legacy owner hashes. Required failures throw, and both post-write completions are attempted even if one fails. Definitions, negative documents, queries and caller-owned `withCache` regions are invalidated in both keyspaces. Do not mix separate raw and bridge caches in one Database/query-cache pair. +Keep the existing key namespace when replacing the bridge. A cache view under `v2:` must share the same backing cache, hash fields and CAS identities with the released bridge in v2 mode; use that same view for Database and its query cache. Do not clear the cache or switch back to unprefixed keys. This is a rolling code cleanup, with no regional stop or extra Dragonfly. -The compatibility test runtime installs pinned stable database 8.3.15/cache 5.2.0 under `compat/` and runs in a separate process against the same Redis and SQLite database. Locally, run `composer install --working-dir=compat --no-dev` before the unit suite; the CI image installs it automatically. +Leave migration flags in configuration until the complete old bridge fleet has been replaced; removing them earlier could return an old process to legacy reads. Remove those flags only when rollback targets also use v2-only code. If rolling back to a bridge image, restore its explicit v2 mode before starting any old process. After contraction, rollback only to code that continues v2 reads. Observe cache hits, memory, evictions, SQL load, latency, queue age and cross-region purge catchup during the rollout. diff --git a/src/Database/Cache/Bridge.php b/src/Database/Cache/Bridge.php deleted file mode 100644 index d85e96699..000000000 --- a/src/Database/Cache/Bridge.php +++ /dev/null @@ -1,205 +0,0 @@ - dual -> v2 in separate passes. - * All cache users must finish each pass before the next starts. - */ -final class Bridge extends Cache -{ - private readonly Cache $legacy; - - public function __construct(private readonly Cache $cache, private readonly string $mode) - { - if (! \in_array($mode, ['legacy', 'dual', 'v2'], true)) { - throw new InvalidArgumentException('Cache migration mode must be legacy, dual or v2'); - } - parent::__construct($cache->adapter); - $this->setCaseSensitivity($cache->caseSensitive); - $this->legacy = $cache->legacy(); - } - - public static function wrap(Cache $cache, string $mode = ''): Cache - { - return $mode === '' ? $cache : new self($cache, $mode); - } - - #[\Override] - public function supportsVersioning(): bool - { - return $this->cache->supportsVersioning(); - } - - private function reader(): Cache - { - return $this->mode === 'legacy' ? $this->legacy : $this->cache; - } - - private function key(string $key): string - { - return $this->mode === 'legacy' ? $key : 'v2:'.$key; - } - - #[\Override] - public function load(string $key, int $ttl, string $hash = ''): mixed - { - return $this->reader()->load($this->key($key), $ttl, $hash); - } - - #[\Override] - public function save(string $key, mixed $data, string $hash = '', int $ttl = 0): bool|string|array - { - return $this->reader()->save($this->key($key), $data, $hash, $ttl); - } - - #[\Override] - public function touch(string $key, string $hash = ''): bool - { - return $this->reader()->touch($this->key($key), $hash); - } - - /** @return string[] */ - #[\Override] - public function list(string $key): array - { - return $this->reader()->list($this->key($key)); - } - - /** @param string[] $fields - * @return array - */ - #[\Override] - public function loadMany(string $key, int $ttl, array $fields = []): array - { - return $this->reader()->loadMany($this->key($key), $ttl, $fields); - } - - /** @param array $data - * @return array|false - */ - #[\Override] - public function saveMany(string $key, array $data, int $ttl = 0): array|false - { - return $this->reader()->saveMany($this->key($key), $data, $ttl); - } - - /** @return array{version: string, value: mixed} */ - #[\Override] - public function loadVersioned(string $key): array - { - return $this->cache->loadVersioned('v2:'.$key); - } - - /** @param array|string $data */ - #[\Override] - public function compareAndSwap(string $key, array|string $data, string $version): string|false - { - return $this->cache->compareAndSwap('v2:'.$key, $data, $version); - } - - #[\Override] - public function getLease(string $key): string - { - return $this->reader()->getLease($this->key($key)); - } - - #[\Override] - public function saveWithLease(string $key, array|string $data, string $hash, string $version): bool|string|array - { - return $this->reader()->saveWithLease($this->key($key), $data, $hash, $version); - } - - #[\Override] - public function purge(string $key, string $hash = ''): bool - { - $this->recordPurge($key, $hash); - $result = false; - $this->both( - function () use ($key, $hash, &$result): void { - $result = $this->legacy->purge($key, $hash) || $result; - }, - function () use ($key, $hash, &$result): void { - $result = $this->cache->purge('v2:'.$key, $hash) || $result; - }, - ); - - return $result; - } - - /** Rotate caller-owned withCache regions in both keyspaces, including when one is cold. */ - public function rotate(string $key, int $ttl = 0): void - { - $this->both( - fn () => $this->rotateIn($this->legacy, $key, $ttl), - fn () => $this->rotateIn($this->cache, 'v2:'.$key, $ttl), - ); - } - - private function rotateIn(Cache $cache, string $key, int $ttl): void - { - $cache->purge($key); - if ($cache->save($key, \bin2hex(\random_bytes(16)), ttl: $ttl) === false) { - throw new Unavailable('Failed to rotate cache region'); - } - } - - public function epoch(string $key, int $timeout, ?int $ttl = null): Epoch - { - if ($this->mode !== 'legacy') { - return (new Mutation($this->cache))->read('v2:'.$key, $timeout); - } - $legacy = new Legacy($this->legacy); - - return $ttl === null ? $legacy->document($key, $timeout) : new Epoch($legacy->query($key, $ttl, $timeout)); - } - - public function begin(string $key, string $token, int $timeout): void - { - // Establish both barriers before SQL. Do not undo one after an uncertain failure. - if ($this->mode !== 'v2') { - (new Legacy($this->legacy))->begin($key, $token); - } - (new Mutation($this->cache))->begin('v2:'.$key, $token, $timeout); - } - - public function finish(string $key, string $token, int $timeout): void - { - $this->both( - fn () => (new Legacy($this->legacy))->finish($key, $token, $timeout), - fn () => (new Mutation($this->cache))->finish('v2:'.$key, $token, $timeout), - ); - } - - /** Attempt both completions even when one fails; never disguise failure as a cache miss. */ - private function both(callable $legacy, callable $versioned): void - { - $failure = null; - if ($this->mode !== 'v2') { - try { - $legacy(); - } catch (Throwable $error) { - $failure = $error; - } - } - try { - $versioned(); - } catch (Throwable $error) { - $failure ??= $error; - } - if ($failure !== null) { - throw $failure; - } - } - - #[\Override] - public function flush(): bool - { - return $this->cache->flush(); - } -} diff --git a/src/Database/Cache/Legacy.php b/src/Database/Cache/Legacy.php deleted file mode 100644 index fa6212fd2..000000000 --- a/src/Database/Cache/Legacy.php +++ /dev/null @@ -1,257 +0,0 @@ -cache->load($key.'#epoch', self::PERMANENT); - if (\is_string($value) && $value !== '') { - $separator = \strrpos($value, '@'); - $marker = $separator === false ? $value : \substr($value, 0, $separator); - $stamp = $separator === false ? '' : \substr($value, $separator + 1); - if (\str_starts_with($value, 'blocked:')) { - $blocked = \ctype_digit($stamp) ? (int) $stamp : 0; - - return $blocked + $timeout > $now - ? new Epoch(blockedAt: $blocked) - : new Epoch('lapsed:'.\substr($value, 8).'@'.$this->cache->getLease($key.'#finished')); - } - $started = $this->cache->getLease($key.'#started'); - if (($separator !== false && $started === $stamp) || $started === $this->cache->getLease($key.'#finished')) { - return new Epoch($marker); - } - } - $started = $this->cache->getLease($key.'#started'); - if ($started !== $this->cache->getLease($key.'#finished')) { - if ($this->cache->save($key.'#epoch', 'blocked:'.$now.'.'.\bin2hex(\random_bytes(16)).'@'.$now) === false) { - throw new Unavailable('Failed to restore legacy tombstone'); - } - - return new Epoch(blockedAt: $now); - } - return new Epoch($this->publish($key, $started)); - } - - public function begin(string $key, string $token): void - { - if ($this->cache->save($key.'#owners', $token, $token) === false) { - throw new Unavailable("Failed to register legacy cache owner for '{$key}'"); - } - - if ($this->cache->save(($key.'#epoch'), self::BLOCKED_PREFIX.$token.self::SEPARATOR.\time()) === false) { - throw new Unavailable("Failed to block legacy cache epoch for '{$key}'"); - } - - $this->cache->purge(($key.'#started')); - } - - /** - * Replace this mutation's shared tombstone with a fresh usable epoch once no - * other mutation of the collection is in progress. - * - * @param int $writerTimeout Seconds after which another writer's registration is treated as abandoned - */ - public function finish(string $key, string $token, int $writerTimeout): void - { - $owners = $key.'#owners'; - $owner = $this->cache->load($owners, self::PERMANENT, $token); - if ($owner !== false && $owner !== null && $owner !== $token) { - throw new RuntimeException("Invalid legacy cache owner for '{$key}'"); - } - $owned = $owner === $token; - if ($owned && ! $this->cache->purge($owners, $token)) { - $owner = $this->cache->load($owners, self::PERMANENT, $token); - if ($owner !== false && $owner !== null) { - throw new Unavailable("Failed to release legacy cache owner for '{$key}'"); - } - $owned = false; - } - - $epochKey = ($key.'#epoch'); - $startedKey = ($key.'#started'); - $finishedKey = ($key.'#finished'); - $started = $this->cache->getLease($startedKey); - $finished = $this->cache->getLease($finishedKey); - $current = $this->cache->load($epochKey, self::PERMANENT); - $ours = $this->isTombstoneOf($current, $token); - - if ($started === $finished) { - if ($ours || ! $this->isTombstone($current)) { - $this->publish($key, $finished); - } - - return; - } - - if (! $owned && ! $ours) { - if ($this->isActive($current)) { - $this->publish($key, $started); - } - - return; - } - - $this->cache->purge($finishedKey); - $nextStarted = $this->cache->getLease($startedKey); - $nextFinished = $this->cache->getLease($finishedKey); - - if ($nextStarted === $nextFinished) { - $this->publish($key, $nextFinished); - - return; - } - - if ($nextFinished === $finished && $this->isTombstoneOf($this->cache->load($epochKey, self::PERMANENT), $token)) { - throw new Unavailable("Failed to finish legacy cache invalidation for '{$key}'"); - } - - if ($this->releaseAbandonedOwners($owners, $writerTimeout)) { - $this->publish($key, $nextStarted); - } - } - - public function query(string $key, int $ttl, int $writerTimeout): ?string - { - $value = $this->cache->load(($key.'#epoch'), self::PERMANENT); - - if ($value === false || $value === null) { - return $this->cache->getLease($key) === self::NEVER_STARTED - ? self::INITIAL_EPOCH - : null; - } - - if (! \is_string($value) || $value === '') { - return null; - } - - $separator = \strrpos($value, self::SEPARATOR); - if ($separator === false) { - return null; - } - $marker = \substr($value, 0, $separator); - $stamp = \substr($value, $separator + 1); - - if (\str_starts_with($value, self::BLOCKED_PREFIX)) { - return $this->getLapsedEpoch($key, $ttl, $value, (int) $stamp, $writerTimeout); - } - - if (! \str_starts_with($value, self::ACTIVE_PREFIX)) { - return null; - } - - $started = $this->cache->getLease(($key.'#started')); - if ($started !== $stamp && $started !== $this->cache->getLease(($key.'#finished'))) { - return null; - } - - return $marker; - } - - /** - * A tombstone lapses after the region TTL once no write is counted in flight, and after the writer - * timeout while one is, since its writer may have died before activating. The lapsed epoch belongs - * to this tombstone and the finished generation, so nothing filled before the block, or before a - * later activation, is served under it. - */ - private function getLapsedEpoch(string $key, int $ttl, string $tombstone, int $stamp, int $writerTimeout): ?string - { - $now = \time(); - if ($stamp + \min($ttl, $writerTimeout) > $now) { - return null; - } - - $started = $this->cache->getLease(($key.'#started')); - $finished = $this->cache->getLease(($key.'#finished')); - if ($stamp + ($started === $finished ? $ttl : $writerTimeout) > $now) { - return null; - } - - return self::LAPSED_PREFIX.\substr($tombstone, \strlen(self::BLOCKED_PREFIX)).self::SEPARATOR.$finished; - } - - /** - * Release every other writer still registered when all of them are older than the writer timeout. - * A token without a creation time counts as live. - */ - private function releaseAbandonedOwners(string $owners, int $writerTimeout): bool - { - $now = \time(); - $abandoned = []; - foreach ($this->cache->list($owners) as $token) { - $created = $this->getTokenTime($token); - if ($created === null || $created + $writerTimeout > $now) { - return false; - } - - $abandoned[] = $token; - } - - foreach ($abandoned as $token) { - $this->cache->purge($owners, $token); - } - - return true; - } - - private function getTokenTime(string $token): ?int - { - $separator = \strpos($token, self::TOKEN_SEPARATOR); - $time = $separator === false ? '' : \substr($token, 0, $separator); - - return \ctype_digit($time) ? (int) $time : null; - } - - private function isActive(mixed $value): bool - { - return \is_string($value) && \str_starts_with($value, self::ACTIVE_PREFIX); - } - - private function isTombstone(mixed $value): bool - { - return \is_string($value) && \str_starts_with($value, self::BLOCKED_PREFIX); - } - - private function isTombstoneOf(mixed $value, string $token): bool - { - return \is_string($value) && \str_starts_with($value, self::BLOCKED_PREFIX.$token.self::SEPARATOR); - } - - private function publish(string $key, string $finished): string - { - $epoch = self::ACTIVE_PREFIX.\bin2hex(\random_bytes(16)); - - if ($this->cache->save(($key.'#epoch'), $epoch.self::SEPARATOR.$finished) === false) { - throw new Unavailable("Failed to activate legacy cache for '{$key}'"); - } - - return $epoch; - } - - - - - - - -} diff --git a/src/Database/Cache/Mutation.php b/src/Database/Cache/Mutation.php index fdb15a9db..883e2f156 100644 --- a/src/Database/Cache/Mutation.php +++ b/src/Database/Cache/Mutation.php @@ -16,29 +16,19 @@ public function __construct(private Cache $cache) { } - public function read(string $key, int $timeout, ?int $ttl = null): Epoch + public function read(string $key, int $timeout): Epoch { - return $this->cache instanceof Bridge - ? $this->cache->epoch($key, $timeout, $ttl) - : $this->change($key, $timeout); + return $this->change($key, $timeout); } public function begin(string $key, string $token, int $timeout): void { - if ($this->cache instanceof Bridge) { - $this->cache->begin($key, $token, $timeout); - } else { - $this->change($key, $timeout, $token, true); - } + $this->change($key, $timeout, $token, true); } public function finish(string $key, string $token, int $timeout): void { - if ($this->cache instanceof Bridge) { - $this->cache->finish($key, $token, $timeout); - } else { - $this->change($key, $timeout, $token); - } + $this->change($key, $timeout, $token); } private function change(string $key, int $timeout, ?string $token = null, bool $begin = false): Epoch diff --git a/src/Database/Cache/Query.php b/src/Database/Cache/Query.php index 705c5d924..1199472e0 100644 --- a/src/Database/Cache/Query.php +++ b/src/Database/Cache/Query.php @@ -76,7 +76,7 @@ public function getEntry(Scope $scope, string $collection, array $queries, strin } $key = $this->getCollectionKey($scope, $collection); - $epoch = (new Mutation($this->cache))->read($key, $scope->writerTimeout, $this->getRegion($collection)->ttl)->value; + $epoch = (new Mutation($this->cache))->read($key, $scope->writerTimeout)->value; if ($epoch === null) { return null; } diff --git a/src/Database/Trait/Documents.php b/src/Database/Trait/Documents.php index ab3e13590..ceac70b12 100644 --- a/src/Database/Trait/Documents.php +++ b/src/Database/Trait/Documents.php @@ -3866,14 +3866,8 @@ public function purgeCachedQueries(string $collection, ?string $namespace = null $epochKey = $this->getQueryCacheKey($collection, $namespace).'#epoch'; try { - if ($this->cache instanceof \Utopia\Database\Cache\Bridge) { - $this->cache->rotate($epochKey); - $rotated = true; - } else { - $existing = $this->cache->load($epochKey, self::TTL); - $rotated = ($existing === false || $existing === null || $this->cache->purge($epochKey)) - && $this->cache->save($epochKey, \bin2hex(\random_bytes(16))) !== false; - } + $this->cache->purge($epochKey); + $rotated = $this->cache->save($epochKey, \bin2hex(\random_bytes(16))) !== false; } catch (Exception $error) { Console::warning('Warning: Failed to purge the cached queries: '.$error->getMessage()); $rotated = false; diff --git a/tests/unit/Cache/BridgeTest.php b/tests/unit/Cache/BridgeTest.php deleted file mode 100644 index 9c90990b7..000000000 --- a/tests/unit/Cache/BridgeTest.php +++ /dev/null @@ -1,331 +0,0 @@ -redis = new Redis(); - $this->redis->connect(\getenv('DATABASE_TEST_CACHE_HOST') ?: 'redis', (int) (\getenv('DATABASE_TEST_CACHE_PORT') ?: 6379)); - $this->cache = new Cache(new RedisCache($this->redis)); - $this->namespace = 'bridge_'.\bin2hex(\random_bytes(8)); - $path = \tempnam(\sys_get_temp_dir(), 'cache-bridge-'); - $this->assertNotFalse($path); - $this->path = $path; - $this->assertTrue($this->old('init')); - } - - #[\Override] - protected function tearDown(): void - { - if (isset($this->redis, $this->namespace)) { - $cursor = null; - do { - $keys = $this->redis->scan($cursor, '*'.$this->namespace.'*', 1000); - if ($keys !== false && $keys !== []) { - $this->redis->del($keys); - } - } while ($cursor !== 0); - $this->redis->close(); - } - if (isset($this->path)) { - \unlink($this->path); - } - } - - public function testFirstPassKeepsOldAndBridgeReadersWarmAndInvalidatesBothDirections(): void - { - $database = $this->database('legacy'); - $this->assertSame('original', $this->old('get')); - $this->assertSame(['original'], $this->old('find')); - $this->assertSame('original', $database->getDocument('users', 'user')->getAttribute('name')); - $database->setProfiling(true); - $database->getDocument('users', 'user'); - $profiler = $database->getProfiler(); - $this->assertNotNull($profiler); - $profiler->reset(); - $database->getDocument('users', 'user'); - $this->assertSame(0, $profiler->getQueryCount(), 'The legacy pass keeps the hot path cached'); - $database->updateDocument('users', 'user', new Document(['name' => 'bridge'])); - $this->assertSame('bridge', $this->old('get')); - $this->assertSame(['bridge'], $this->old('find')); - $this->old('update', ['value' => 'old']); - $this->assertSame('old', $database->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('old', $database->find('users')[0]->getAttribute('name')); - $key = $database->getCacheBaseKeys('users')[0]; - $this->assertFalse($this->redis->hExists(\strtolower($key.'#owners'), '__utopia_version__'), 'The bridge must not pollute v1 owner hashes with v2 internal fields'); - $this->assertSame([], $this->cache->legacy()->list($key.'#owners')); - } - - public function testSecondAndThirdPassesInvalidateDocumentsQueriesAndNegativeEntries(): void - { - $legacy = $this->database('legacy'); - $dual = $this->database('dual'); - $this->assertSame('original', $legacy->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('original', $dual->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('original', $dual->find('users')[0]->getAttribute('name')); - $this->assertTrue($legacy->getDocument('users', 'missing')->isEmpty()); - $this->assertTrue($dual->getDocument('users', 'missing')->isEmpty()); - $legacy->createDocument('users', new Document(['$id' => 'missing', 'name' => 'created'])); - $this->assertSame('created', $dual->getDocument('users', 'missing')->getAttribute('name')); - $dual->updateDocument('users', 'user', new Document(['name' => 'dual'])); - $this->assertSame('dual', $legacy->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('dual', $this->old('get'), 'Rollback readers remain valid during the dual-write window'); - $this->assertContains('dual', \array_map(fn (Document $document) => $document->getAttribute('name'), $legacy->find('users'))); - $v2 = $this->database('v2'); - $v2->updateDocument('users', 'user', new Document(['name' => 'final'])); - $this->assertSame('final', $dual->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('final', $v2->getDocument('users', 'user')->getAttribute('name')); - $this->assertContains('final', \array_map(fn (Document $document) => $document->getAttribute('name'), $dual->find('users'))); - } - - public function testRapidWritesWithCloudsGraceWindowKeepBothProtocolsUsable(): void - { - $cache = new Cache((new RedisCache($this->redis))->setLeaseGraceWindow(1000)); - foreach (['legacy', 'dual', 'v2'] as $mode) { - $database = $this->database($mode, $cache); - $database->updateDocuments('users', new Document(['name' => $mode.'-first'])); - $database->updateDocuments('users', new Document(['name' => $mode.'-second'])); - $this->assertSame($mode.'-second', $database->getDocument('users', 'user')->getAttribute('name')); - if ($mode !== 'v2') { - $this->assertSame($mode.'-second', $this->old('get'), 'Legacy readers remain valid during dual invalidation'); - } - } - } - - public function testApplicationListCacheSupportsExpiringVersionsAndBatchFillsInEveryPhase(): void - { - foreach (['legacy', 'dual', 'v2'] as $mode) { - $cache = new Bridge($this->cache, $mode); - $key = $this->namespace.':lists:'.$mode; - $this->assertSame('version', $cache->save($key.':version', 'version', ttl: 60)); - $this->assertSame('version', $cache->load($key.':version', 60)); - $wireKey = $mode === 'legacy' ? $key : 'v2:'.$key; - $this->assertGreaterThan(0, $this->redis->ttl($wireKey.':version')); - $cache->rotate($key.':version', 60); - $this->assertNotSame('version', $cache->load($key.':version', 60)); - $this->assertGreaterThan(0, $this->redis->ttl($wireKey.':version')); - $fields = ['documents' => ['row'], 'total' => '1']; - $this->assertSame($fields, $cache->saveMany($key, $fields)); - $this->assertSame($fields, $cache->loadMany($key, 60)); - $cache->purge($key); - $this->assertSame([], $cache->loadMany($key, 60)); - } - } - - public function testSlotZeroRemainsCacheableAcrossEveryPhase(): void - { - foreach (['', 'legacy', 'dual', 'v2'] as $mode) { - $cache = Bridge::wrap($this->cache, $mode); - $query = new QueryCache($cache, slots: 1); - $scope = new Scope(namespace: $this->namespace.$mode); - $entry = $query->getEntry($scope, 'users', []); - $this->assertNotNull($entry); - $this->assertTrue($query->set($entry, [new Document(['$id' => 'warm'])], $query->getLease($entry))); - $next = $query->getEntry($scope, 'users', []); - $this->assertNotNull($next); - $documents = $query->get($next); - $this->assertNotNull($documents, 'The zero slot must load the same field that the lease filled'); - $this->assertSame('warm', $documents[0]->getId()); - } - } - - public function testSchemaInvalidationRetiresDefinitionsInBothKeyspaces(): void - { - $legacy = $this->database('legacy'); - $dual = $this->database('dual'); - $legacy->getCollection('users'); - $dual->getCollection('users'); - $dual->createAttribute('users', Attribute::string(key: 'extra')); - $attributes = function (Database $database): array { - $items = $database->getCollection('users')->getAttribute('attributes'); - $this->assertIsArray($items); - - return \array_map(function (mixed $attribute): string { - $this->assertInstanceOf(Document::class, $attribute); - - return $attribute->getId(); - }, $items); - }; - $this->assertContains('extra', $attributes($legacy)); - $this->assertContains('extra', $attributes($dual)); - $oldAttributes = $this->old('attributes'); - $this->assertIsArray($oldAttributes); - $this->assertContains('extra', $oldAttributes); - } - - public function testAnOldTransactionFinishesBeforeTheReadSwitch(): void - { - [$process, $pipes] = $this->startOld('hold', ['value' => 'committed']); - try { - $this->assertSame("ready\n", \fgets($pipes[1])); - $legacy = $this->database('legacy'); - $this->assertSame('original', $legacy->getDocument('users', 'user')->getAttribute('name')); - \fwrite($pipes[0], "commit\n"); - \fclose($pipes[0]); - $this->assertSame('committed', \json_decode((string) \stream_get_contents($pipes[1]), true, flags: JSON_THROW_ON_ERROR)); - $this->assertSame('', \stream_get_contents($pipes[2])); - $this->assertSame(0, \proc_close($process)); - $process = null; - $this->assertSame('committed', $legacy->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('committed', $this->database('dual')->getDocument('users', 'user')->getAttribute('name')); - } finally { - if (\is_resource($process)) { - \proc_terminate($process); - \proc_close($process); - } - } - } - - public function testEvictionDuringDualWritesRetiresFillsAndPreservesNewerWriters(): void - { - $bridge = new Bridge($this->cache, 'dual'); - $mutation = new Mutation($bridge); - $key = $this->namespace; - $first = \time().'.first'; - $second = \time().'.second'; - $mutation->begin($key, $first, 3600); - $this->redis->del('v2:'.$key.'#state'); - $cold = $mutation->read($key, 3600)->value; - $this->assertNotNull($cold); - $mutation->begin($key, $second, 3600); - $mutation->finish($key, $first, 3600); - $this->assertNull($mutation->read($key, 3600)->value); - $mutation->finish($key, $second, 3600); - $this->assertNotSame($cold, $mutation->read($key, 3600)->value); - } - - public function testWithCacheInvalidationAndPurgeRecordingsCoverBothReaders(): void - { - $legacy = $this->database('legacy'); - $dual = $this->database('dual'); - $key = $legacy->getQueryCacheKey('users'); - $before = \bin2hex(\random_bytes(8)); - $after = \bin2hex(\random_bytes(8)); - $this->assertSame($before, $legacy->withCache($key, fn () => $before)); - $this->assertSame($before, $dual->withCache($key, fn () => $before)); - $this->assertTrue($dual->purgeCachedQueries('users')); - $this->assertSame($after, $legacy->withCache($key, fn () => $after)); - $this->assertSame($after, $dual->withCache($key, fn () => $after)); - $purges = $dual->getCache()->recordPurges(fn () => $dual->getCache()->purge('MiXeD')); - $this->assertSame(['mixed' => ['' => true]], $purges, 'Recorded keys must stay logical so replay does not add the namespace twice'); - } - - public function testRollbackRetiresFillsInBothKeyspaces(): void - { - $legacy = $this->database('legacy'); - $dual = $this->database('dual'); - $legacy->getDocument('users', 'user'); - $dual->getDocument('users', 'user'); - try { - $dual->withTransaction(function () use ($dual): void { - $dual->updateDocument('users', 'user', new Document(['name' => 'rolled-back'])); - throw new \RuntimeException('rollback'); - }); - } catch (\RuntimeException $error) { - $this->assertSame('rollback', $error->getMessage()); - } - $this->assertSame('original', $legacy->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('original', $dual->getDocument('users', 'user')->getAttribute('name')); - $this->assertSame('original', $this->old('get')); - } - - public function testFailedLegacyCompletionStillFinishesV2AndReportsFailure(): void - { - $adapter = new FailingLegacyRedis($this->redis); - $bridge = new Bridge(new Cache($adapter), 'dual'); - $mutation = new Mutation($bridge); - $key = $this->namespace; - $token = \time().'.completion'; - $mutation->begin($key, $token, 3600); - $this->assertNull($mutation->read($key, 3600)->value); - $adapter->failCompletion = true; - try { - $mutation->finish($key, $token, 3600); - $this->fail('A partial completion must be reported'); - } catch (Unavailable $error) { - $this->assertSame('Legacy completion unavailable', $error->getMessage()); - } - $this->assertNotNull($mutation->read($key, 3600)->value, 'V2 completion must run even when legacy completion fails'); - $legacy = $this->cache->load($key.'#epoch', PHP_INT_MAX); - $this->assertIsString($legacy); - $this->assertStringStartsWith('blocked:', $legacy); - } - - public function testOpenBreakerAbortsRequiredCoordinationBeforeSQL(): void - { - $breaker = new Breaker(timeout: 3600); - $cache = new Cache(new CircuitBreaker(new RedisCache($this->redis), $breaker)); - $database = $this->database('dual', $cache); - $breaker->trip(); - try { - $database->updateDocument('users', 'user', new Document(['name' => 'must-not-commit'])); - $this->fail('An open breaker must not permit an unfenced SQL write'); - } catch (\Throwable $error) { - $this->assertInstanceOf(Unavailable::class, $error); - $this->assertSame('original', $this->old('get')); - } - } - - private function database(string $mode, ?Cache $cache = null): Database - { - $bridge = new Bridge($cache ?? $this->cache, $mode); - $database = new Database(new SQLite(new PDO('sqlite:'.$this->path)), $bridge); - $database->setDatabase('bridge')->setNamespace($this->namespace)->setQueryCache(new QueryCache($bridge)); - - return $database; - } - - /** @param array $arguments - * @return array{resource, array} - */ - private function startOld(string $action, array $arguments = []): array - { - $process = \proc_open([PHP_BINARY, __DIR__.'/../../../compat/runtime.php', \json_encode($arguments + ['action' => $action, 'path' => $this->path, 'namespace' => $this->namespace], JSON_THROW_ON_ERROR)], [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes); - $this->assertIsResource($process); - foreach ($pipes as $pipe) { - \stream_set_timeout($pipe, 15); - } - - return [$process, $pipes]; - } - - /** @param array $arguments */ - private function old(string $action, array $arguments = []): mixed - { - [$process, $pipes] = $this->startOld($action, $arguments); - \fclose($pipes[0]); - $output = \stream_get_contents($pipes[1]); - $error = \stream_get_contents($pipes[2]); - \fclose($pipes[1]); - \fclose($pipes[2]); - $this->assertSame(0, \proc_close($process), (string) $error.' '.(string) $output); - - return \json_decode((string) $output, true, flags: JSON_THROW_ON_ERROR); - } -} diff --git a/tests/unit/Cache/FailingLegacyRedis.php b/tests/unit/Cache/FailingLegacyRedis.php deleted file mode 100644 index c288ce56f..000000000 --- a/tests/unit/Cache/FailingLegacyRedis.php +++ /dev/null @@ -1,21 +0,0 @@ -failCompletion && \str_ends_with($key, '#owners')) { - throw new Unavailable('Legacy completion unavailable'); - } - - return parent::purgeLegacy($key, $hash); - } -} diff --git a/tests/unit/Cache/QueryPurgeTest.php b/tests/unit/Cache/QueryPurgeTest.php index 99f373aba..e5d3a7cd0 100644 --- a/tests/unit/Cache/QueryPurgeTest.php +++ b/tests/unit/Cache/QueryPurgeTest.php @@ -18,6 +18,29 @@ final class QueryPurgeTest extends TestCase { + public function testColdLocalQueryPurgeInvalidatesAWarmRemoteRegion(): void + { + [$local] = $this->createDatabases('posts_'.\uniqid()); + $remoteCache = new Cache(new MemoryCache()); + $remote = new Database(new DatabaseMemory(), $remoteCache); + $key = $local->getQueryCacheKey('posts'); + $reads = 0; + $read = static function () use (&$reads): int { + return ++$reads; + }; + $this->assertSame(1, $remote->withCache($key, $read)); + $local->getCache()->purge($key.'#epoch'); + + $purges = $local->getCache()->recordPurges(fn (): bool => $local->purgeCachedQueries('posts')); + foreach ($purges as $cacheKey => $fields) { + foreach ($fields as $field => $_) { + $remoteCache->purge($cacheKey, $field); + } + } + + $this->assertSame(2, $remote->withCache($key, $read)); + } + public function testPurgeCachedQueriesDropsTheResultsFindCached(): void { [$reader, $bypass] = $this->createDatabases('posts_'.\uniqid());