From 3f7fae5f2a51f187b999bd7a2cb189d0f21d9435 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 17:14:54 +0000 Subject: [PATCH 01/28] Prepare optional Laravel service HTTP hosting example --- examples/laravel-http/.gitignore | 10 + examples/laravel-http/README.md | 120 + .../app/Http/Controllers/Controller.php | 8 + .../Http/Controllers/WorkflowController.php | 63 + .../app/Http/Middleware/ExampleContext.php | 52 + examples/laravel-http/app/Models/User.php | 49 + .../app/Providers/AppServiceProvider.php | 18 + .../app/Workflows/EchoActivity.php | 17 + .../app/Workflows/HttpWorkflow.php | 40 + examples/laravel-http/artisan | 12 + examples/laravel-http/bootstrap/app.php | 21 + .../laravel-http/bootstrap/cache/.gitignore | 2 + examples/laravel-http/bootstrap/providers.php | 3 + examples/laravel-http/compose.fpm.yaml | 8 + examples/laravel-http/compose.yaml | 87 + examples/laravel-http/composer.json | 30 + examples/laravel-http/composer.lock | 7561 +++++++++++++++++ examples/laravel-http/config/app.php | 126 + examples/laravel-http/config/auth.php | 117 + examples/laravel-http/config/cache.php | 117 + .../laravel-http/config/durable-workflow.php | 16 + examples/laravel-http/config/example.php | 7 + examples/laravel-http/config/filesystems.php | 80 + examples/laravel-http/config/logging.php | 132 + examples/laravel-http/config/mail.php | 118 + examples/laravel-http/config/octane.php | 254 + examples/laravel-http/config/queue.php | 129 + examples/laravel-http/config/services.php | 38 + examples/laravel-http/config/session.php | 217 + examples/laravel-http/public/index.php | 11 + .../laravel-http/qualification/context.mjs | 39 + examples/laravel-http/routes/api.php | 20 + examples/laravel-http/routes/console.php | 1 + examples/laravel-http/runtime/Dockerfile | 17 + examples/laravel-http/runtime/fpm.conf | 16 + examples/laravel-http/runtime/nginx-fpm.conf | 24 + .../laravel-http/runtime/nginx-octane.conf | 25 + examples/laravel-http/runtime/opcache.ini | 11 + examples/laravel-http/runtime/rr.yaml | 1 + 39 files changed, 9617 insertions(+) create mode 100644 examples/laravel-http/.gitignore create mode 100644 examples/laravel-http/README.md create mode 100644 examples/laravel-http/app/Http/Controllers/Controller.php create mode 100644 examples/laravel-http/app/Http/Controllers/WorkflowController.php create mode 100644 examples/laravel-http/app/Http/Middleware/ExampleContext.php create mode 100644 examples/laravel-http/app/Models/User.php create mode 100644 examples/laravel-http/app/Providers/AppServiceProvider.php create mode 100644 examples/laravel-http/app/Workflows/EchoActivity.php create mode 100644 examples/laravel-http/app/Workflows/HttpWorkflow.php create mode 100644 examples/laravel-http/artisan create mode 100644 examples/laravel-http/bootstrap/app.php create mode 100644 examples/laravel-http/bootstrap/cache/.gitignore create mode 100644 examples/laravel-http/bootstrap/providers.php create mode 100644 examples/laravel-http/compose.fpm.yaml create mode 100644 examples/laravel-http/compose.yaml create mode 100644 examples/laravel-http/composer.json create mode 100644 examples/laravel-http/composer.lock create mode 100644 examples/laravel-http/config/app.php create mode 100644 examples/laravel-http/config/auth.php create mode 100644 examples/laravel-http/config/cache.php create mode 100644 examples/laravel-http/config/durable-workflow.php create mode 100644 examples/laravel-http/config/example.php create mode 100644 examples/laravel-http/config/filesystems.php create mode 100644 examples/laravel-http/config/logging.php create mode 100644 examples/laravel-http/config/mail.php create mode 100644 examples/laravel-http/config/octane.php create mode 100644 examples/laravel-http/config/queue.php create mode 100644 examples/laravel-http/config/services.php create mode 100644 examples/laravel-http/config/session.php create mode 100644 examples/laravel-http/public/index.php create mode 100644 examples/laravel-http/qualification/context.mjs create mode 100644 examples/laravel-http/routes/api.php create mode 100644 examples/laravel-http/routes/console.php create mode 100644 examples/laravel-http/runtime/Dockerfile create mode 100644 examples/laravel-http/runtime/fpm.conf create mode 100644 examples/laravel-http/runtime/nginx-fpm.conf create mode 100644 examples/laravel-http/runtime/nginx-octane.conf create mode 100644 examples/laravel-http/runtime/opcache.ini create mode 100644 examples/laravel-http/runtime/rr.yaml diff --git a/examples/laravel-http/.gitignore b/examples/laravel-http/.gitignore new file mode 100644 index 00000000..84a7acff --- /dev/null +++ b/examples/laravel-http/.gitignore @@ -0,0 +1,10 @@ +/vendor/ +/rr +/.env +/data/ +/storage/ +/bootstrap/cache/* +!/bootstrap/cache/.gitignore +/receipts/ +!/composer.lock +.rr.yaml diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md new file mode 100644 index 00000000..cd6fe6c7 --- /dev/null +++ b/examples/laravel-http/README.md @@ -0,0 +1,120 @@ +# Laravel service HTTP example + +This candidate application compares nginx/PHP-FPM with optional Laravel Octane +and RoadRunner. Qualification is in progress on [PHP SDK #112](https://github.com/durable-workflow/sdk-php/issues/112). +No performance or production hosting recommendation is established yet. + +The application uses the normally published SDK. Durable workflow and activity +polling runs in the separate `durable-workflow:worker` CLI container. HTTP +workers only submit and inspect runtime operations. Server or Cloud owns durable +history, timers, task leases and recovery. + +## Installation + +The exact PHP SDK, Laravel and Octane versions live in `composer.json` and the +resolved dependencies in `composer.lock`. The image uses PHP 8.3.35. Preparation +downloads RoadRunner 2025.1.15 through its standard installer, then runs +`octane:install` and `config:cache`. Octane remains an application dependency. + +Use a disposable self-hosted Server namespace with separate application-client +and worker role credentials. Put the following Compose values in a private file +outside this application directory. Do not put runtime credentials in Laravel +configuration or commit them. + +```text +APP_KEY= +DURABLE_WORKFLOW_RUNTIME_URL= +DURABLE_WORKFLOW_NAMESPACE= +DURABLE_WORKFLOW_CLIENT_TOKEN= +DURABLE_WORKFLOW_WORKER_TOKEN= +EXAMPLE_STATE_DIRECTORY= +``` + +Create the state directory as your local UID, then run: + +```bash +docker compose --env-file /private/example.env up --build -d +``` + +The HTTP container receives only the client token. The CLI container receives +only the worker token. The preparation container receives neither. Change the +runtime address, namespace or credential by replacing the relevant process. +Never rewrite a shared client using request headers or an authenticated user's +token. Multiple runtime authorities need independently configured clients or +separate application processes. + +For the FPM baseline: + +```bash +docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml up --build -d +``` + +Both modes use two HTTP workers, one durable CLI worker and nginx with the same +OPcache and cached configuration. The application allocation totals two CPUs +and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, CLI 1 CPU/320 MiB, nginx +0.1 CPU/64 MiB. Provision and report the separate Server/database allocation. +These limits are qualification settings, not a general capacity promise. + +## Exercise the application + +The two `example-alpha` and `example-beta` bearer values are synthetic application +identities. Replace this middleware with your application's authorization before +exposing the example. It binds workflow IDs to the current user and refuses a +different `X-Namespace` before calling the SDK. + +```bash +curl -f http://localhost:8088/api/workflows \ + -H 'Authorization: Bearer example-alpha' \ + -H 'X-Request-Id: start-1' -H 'Content-Type: application/json' \ + -d '{"id":"first","kind":"short"}' +curl -f http://localhost:8088/api/workflows/first \ + -H 'Authorization: Bearer example-alpha' -H 'X-Request-Id: read-1' +``` + +Kinds `short`, `timer`, `signal` and `cancel` share one echo activity. A timer +waits one second. A signal workflow waits for `POST /api/workflows/{id}/signal`. +Query it with `POST /api/workflows/{id}/query`. Cancel a waiting original with +`POST /api/workflows/{id}/cancel`. Status reads return canonical status and the +typed result, including an integer above JavaScript's exact-number range, null +and Unicode. Decode that integer with an appropriate client. + +Every API request requires a fresh `X-Request-Id`. Alternating requests omit or +set `X-Optional-Context` and inspect response headers plus application logs. +Request context is removed in the middleware's `finally` block. Set +`EXAMPLE_EAGER=true` before starting a fresh application to resolve the SDK at +boot; the default resolves it lazily. Both processes keep a fixed authority. + +`qualification/context.mjs` checks 40 alternating requests and three auth, +namespace and invalid-header refusals through `/api/context`. That diagnostic +route resolves the normal SDK client without sending a runtime request. It +checks HTTP request isolation only; it cannot prove workflow execution or +runtime namespace authorization. + +History-cache defaults remain 100 entries/300 seconds. The existing SDK validates +capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and +`EXAMPLE_HISTORY_TTL` select this process's settings before startup. A cache hit +retains history and still replays instructions. HTTP reuse does not retain a +workflow Fiber or avoid execution replay. + +## Qualification before recommendation + +Freeze the normal dependency lock, RoadRunner binary hash, runtime image digest, +role configuration, offered schedule, original deadlines and resource limits +before admitting workflows. Qualify eager/lazy resolution, alternating users and +namespace refusals, headers/log context, signal/query/cancel, cache expiry and +eviction, HTTP reload/recycling, CLI SIGKILL/cold replacement and runtime +connection loss first. Inspect canonical histories for duplicate effects and +unchanged identities/deadlines. + +Then run three independent FPM/Octane pairs and recheck FPM. Keep CLI execution +settings identical. Report offered/completed/failed/unfinished operations, +submission/status latency separately from workflow latency, measured drain, +timer due-to-resume latency, per-completion CPU, aggregate steady/peak memory, +history bytes/requests and existing cache diagnostics. Reject OOM, swapping or +growing backlog as a successful measurement. Follow with a bounded mixed-request +soak and explicit recycle/reload observations. Gains and tradeoffs belong in the +owning issue, including a result with no consistent benefit. + +Stop the Compose project and delete only this example's state when finished. +This application does not provision or retire your runtime namespace or its +credentials. Retire those through your Server or Cloud's normal admin path. diff --git a/examples/laravel-http/app/Http/Controllers/Controller.php b/examples/laravel-http/app/Http/Controllers/Controller.php new file mode 100644 index 00000000..8677cd5c --- /dev/null +++ b/examples/laravel-http/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +validate(['id' => 'required|alpha_dash|max:60', 'kind' => 'required|in:short,timer,signal,cancel']); + $handle = $this->workflows->start( + HttpWorkflow::class, + [$request->attributes->get('example_user'), $values['kind']], + workflowId: $this->workflowId($request, $values['id']), + options: new WorkflowStartOptions(executionTimeoutSeconds: 900, runTimeoutSeconds: 600), + ); + + return response()->json(['workflow_id' => $handle->workflowId, 'run_id' => $handle->selectedRunId], 201); + } + + public function status(Request $request, string $id) + { + $execution = $this->workflows->handle(HttpWorkflow::class, $this->workflowId($request, $id))->describe(); + + return response()->json(['status' => $execution->status, 'output' => $execution->output]); + } + + public function signal(Request $request, string $id) + { + $this->workflows->handle(HttpWorkflow::class, $this->workflowId($request, $id))->signal('release'); + + return response()->noContent(); + } + + public function query(Request $request, string $id) + { + return response()->json(['phase' => $this->workflows->handle(HttpWorkflow::class, $this->workflowId($request, $id))->query('phase')]); + } + + public function cancel(Request $request, string $id) + { + $this->workflows->handle(HttpWorkflow::class, $this->workflowId($request, $id))->cancel('example cancellation'); + + return response()->noContent(); + } + + private function workflowId(Request $request, string $id): string + { + abort_unless(preg_match('/^[a-zA-Z0-9_-]{1,60}$/', $id) === 1, 422); + + return $request->attributes->get('example_user').'-'.$id; + } +} diff --git a/examples/laravel-http/app/Http/Middleware/ExampleContext.php b/examples/laravel-http/app/Http/Middleware/ExampleContext.php new file mode 100644 index 00000000..72f0cc68 --- /dev/null +++ b/examples/laravel-http/app/Http/Middleware/ExampleContext.php @@ -0,0 +1,52 @@ +bearerToken() ?? ''] ?? null; + abort_unless(is_string($user), 401); + $namespace = config('durable-workflow.namespace'); + abort_unless($request->header('X-Namespace', $namespace) === $namespace, 403); + $requestId = $request->header('X-Request-Id'); + abort_unless(is_string($requestId) && preg_match('/^[a-zA-Z0-9_-]{1,80}$/', $requestId) === 1, 422); + $request->attributes->set('example_user', $user); + $context = [ + 'request_id' => $requestId, + 'user' => $user, + 'namespace' => $namespace, + 'optional_context' => $request->header('X-Optional-Context'), + ]; + Log::withContext($context); + $started = hrtime(true); + $status = 500; + + try { + $response = $next($request); + $status = $response->getStatusCode(); + $response->headers->add([ + 'X-Request-Id' => $requestId, + 'X-Worker-Pid' => (string) getmypid(), + 'X-Example-User' => $user, + 'X-Example-Namespace' => $namespace, + 'X-Optional-Context' => (string) ($context['optional_context'] ?? ''), + ]); + + return $response; + } finally { + Log::info('example.request', [ + 'pid' => getmypid(), 'path' => $request->path(), 'status' => $status, + 'elapsed_ms' => (hrtime(true) - $started) / 1e6, + ]); + Log::withoutContext(array_keys($context)); + } + } +} diff --git a/examples/laravel-http/app/Models/User.php b/examples/laravel-http/app/Models/User.php new file mode 100644 index 00000000..68f3a669 --- /dev/null +++ b/examples/laravel-http/app/Models/User.php @@ -0,0 +1,49 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } +} diff --git a/examples/laravel-http/app/Providers/AppServiceProvider.php b/examples/laravel-http/app/Providers/AppServiceProvider.php new file mode 100644 index 00000000..1a56ece7 --- /dev/null +++ b/examples/laravel-http/app/Providers/AppServiceProvider.php @@ -0,0 +1,18 @@ +app->runningInConsole() && config('example.eager')) { + $this->app->make(Client::class); + $this->app->make(LaravelWorkflowClientInterface::class); + } + } +} diff --git a/examples/laravel-http/app/Workflows/EchoActivity.php b/examples/laravel-http/app/Workflows/EchoActivity.php new file mode 100644 index 00000000..ebb65013 --- /dev/null +++ b/examples/laravel-http/app/Workflows/EchoActivity.php @@ -0,0 +1,17 @@ +activity('example.echo', [$user]); + if ($kind === 'timer') { + $context->sleep(1); + } elseif ($kind === 'signal') { + $context->waitCondition(fn (): bool => $context->signals('release') !== [], key: 'release'); + } elseif ($kind === 'cancel') { + $context->sleep(300); + } + return ['user' => $user, 'echo' => $echo, 'number' => 9007199254740993, 'nullable' => null, 'text' => '雪']; + } + + #[Query('phase')] + public function phase(QueryContext $context): string + { + return $context->events('ActivityCompleted') === [] ? 'activity' : 'waiting'; + } + + #[Signal('release')] + public function release(): void + { + // The workflow reads committed signal history rather than shared handler memory. + } +} diff --git a/examples/laravel-http/artisan b/examples/laravel-http/artisan new file mode 100644 index 00000000..89c1d3e9 --- /dev/null +++ b/examples/laravel-http/artisan @@ -0,0 +1,12 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput)); diff --git a/examples/laravel-http/bootstrap/app.php b/examples/laravel-http/bootstrap/app.php new file mode 100644 index 00000000..af21aa2b --- /dev/null +++ b/examples/laravel-http/bootstrap/app.php @@ -0,0 +1,21 @@ +withRouting( + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->alias(['example' => \App\Http\Middleware\ExampleContext::class]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + })->create(); + +$application->useStoragePath(getenv('EXAMPLE_STORAGE_PATH') ?: dirname(__DIR__).'/storage'); + +return $application; diff --git a/examples/laravel-http/bootstrap/cache/.gitignore b/examples/laravel-http/bootstrap/cache/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/examples/laravel-http/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/laravel-http/bootstrap/providers.php b/examples/laravel-http/bootstrap/providers.php new file mode 100644 index 00000000..2bd0b5ca --- /dev/null +++ b/examples/laravel-http/bootstrap/providers.php @@ -0,0 +1,3 @@ +- + sh -ec 'mkdir -p /state/storage/framework/cache/data /state/storage/framework/sessions + /state/storage/framework/views /state/storage/logs; + composer install --no-dev --no-interaction --prefer-dist; + test -x rr || php vendor/bin/rr get-binary --filter=2025.1.15 --no-interaction; + php artisan octane:install --server=roadrunner --no-interaction; + php artisan config:clear; + php artisan config:cache' + cpus: 1 + mem_limit: 1g + memswap_limit: 1g + + http-app: + <<: *application + environment: + <<: *environment + DURABLE_WORKFLOW_CLIENT_TOKEN: ${DURABLE_WORKFLOW_CLIENT_TOKEN:?Set the application client-role credential} + init: true + stop_signal: SIGTERM + command: php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000 --workers=2 --max-requests=500 + depends_on: + prepare: + condition: service_completed_successfully + restart: unless-stopped + cpus: 0.9 + mem_limit: 640m + memswap_limit: 640m + + worker: + <<: *application + environment: + <<: *environment + DURABLE_WORKFLOW_WORKER_TOKEN: ${DURABLE_WORKFLOW_WORKER_TOKEN:?Set a separate worker-role credential} + init: true + command: php artisan durable-workflow:worker + depends_on: + prepare: + condition: service_completed_successfully + restart: unless-stopped + stop_grace_period: 65s + cpus: 1 + mem_limit: 320m + memswap_limit: 320m + + nginx: + image: nginx@sha256:0985e772fb9f729e6fa0980da05fca5d9c468e870eed43071545afa9d2e27d94 + user: "1000:1000" + entrypoint: ["nginx", "-g", "daemon off;"] + ports: + - "127.0.0.1:${EXAMPLE_HTTP_PORT:-8088}:8080" + volumes: + - ./runtime/nginx-octane.conf:/etc/nginx/nginx.conf:ro + depends_on: + - http-app + restart: unless-stopped + cpus: 0.1 + mem_limit: 64m + memswap_limit: 64m diff --git a/examples/laravel-http/composer.json b/examples/laravel-http/composer.json new file mode 100644 index 00000000..d6753e69 --- /dev/null +++ b/examples/laravel-http/composer.json @@ -0,0 +1,30 @@ +{ + "name": "durable-workflow/service-http-example", + "description": "Optional Laravel HTTP hosting example for the service SDK", + "type": "project", + "license": "MIT", + "require": { + "php": "^8.3", + "durable-workflow/sdk": "2.4.0", + "laravel/framework": "12.69.3", + "laravel/octane": "2.21.0", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "autoload": {"psr-4": {"App\\": "app/"}}, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ] + }, + "config": { + "platform": {"php": "8.3.35"}, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": {"php-http/discovery": true} + }, + "minimum-stability": "stable", + "prefer-stable": true +} diff --git a/examples/laravel-http/composer.lock b/examples/laravel-http/composer.lock new file mode 100644 index 00000000..45e0b513 --- /dev/null +++ b/examples/laravel-http/composer.lock @@ -0,0 +1,7561 @@ +{ + "_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": "a6900d54b2d93f597bf6402efe37a357", + "packages": [ + { + "name": "apache/avro", + "version": "1.12.2", + "source": { + "type": "git", + "url": "https://github.com/apache/avro.git", + "reference": "8fa2067f70e3012cb3fd9a8839cd97e8c7cc1772" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apache/avro/zipball/8fa2067f70e3012cb3fd9a8839cd97e8c7cc1772", + "reference": "8fa2067f70e3012cb3fd9a8839cd97e8c7cc1772", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-json": "*", + "ext-pcntl": "*", + "ext-xml": "*", + "php-mock/php-mock-phpunit": "^2.10", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Apache\\Avro\\": "lang/php/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Apache Avro Developers", + "email": "dev@avro.apache.org", + "homepage": "http://avro.apache.org" + } + ], + "description": "Apache Avro™ is a data serialization system.", + "homepage": "http://avro.apache.org", + "keywords": [ + "avro", + "data", + "serialization" + ], + "support": { + "email": "dev@avro.apache.org", + "issues": "https://issues.apache.org/jira/browse/AVRO", + "source": "https://github.com/apache/avro/tree/release-1.12.2" + }, + "time": "2026-08-07T12:56:37+00:00" + }, + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2026-09-06T14:11:38+00:00" + }, + { + "name": "composer/semver", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "528948210cc46f967a4b98e83b19e76f2b504993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/528948210cc46f967a4b98e83b19e76f2b504993", + "reference": "528948210cc46f967a4b98e83b19e76f2b504993", + "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.5.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-10-05T13:12:12+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2026-10-01T06:59:29+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "durable-workflow/sdk", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/durable-workflow/sdk-php.git", + "reference": "821716a92f179ec360e045c28979a86a5f587075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/821716a92f179ec360e045c28979a86a5f587075", + "reference": "821716a92f179ec360e045c28979a86a5f587075", + "shasum": "" + }, + "require": { + "apache/avro": "^1.12", + "composer-runtime-api": "^2.0", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.8|^8.0", + "php": "^8.1", + "psr/container": "^1.1|^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1|^2.0", + "psr/log": "^1.1|^2.0|^3.0" + }, + "require-dev": { + "phpdocumentor/phpdocumentor": "^3.7", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5|^11.0" + }, + "suggest": { + "ext-pcntl": "Required by framework console workers for graceful SIGINT and SIGTERM handling.", + "laravel/framework": "Enables the auto-discovered Laravel 9 through 13 service-mode bridge.", + "symfony/console": "Required with symfony/framework-bundle to run the Symfony 6.4, 7, and 8 worker command.", + "symfony/framework-bundle": "Enables the Symfony 6.4, 7, and 8 service-mode bundle." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "DurableWorkflow": "DurableWorkflow\\Bridge\\Laravel\\Facades\\DurableWorkflow" + }, + "providers": [ + "DurableWorkflow\\Bridge\\Laravel\\DurableWorkflowServiceProvider" + ] + }, + "durable-workflow": { + "frameworks": { + "laravel": "^9.0|^10.0|^11.0|^12.0|^13.0", + "symfony": "^6.4|^7.0|^8.0" + }, + "product-train": "2.4.0", + "payload-codecs": [ + "avro" + ], + "message-streams": true, + "version-markers": true, + "workflow-streams": { + "contract": "durable-workflow.v2.workflow-streams.contract@1", + "delivery": "at-least-once", + "operations": [ + "list", + "describe", + "subscribe", + "append", + "close", + "error" + ], + "authoring-command": "record_side_effect.workflow_stream", + "external-payload-references": "opaque-reference" + }, + "durable-selection": true, + "control-plane-version": "2", + "version-marker-command": "record_version_marker", + "version-marker-helpers": [ + "patched", + "deprecatePatch" + ], + "worker-protocol-version": "1.20", + "supported-server-versions": "2.5.0", + "version-marker-history-event": "VersionMarkerRecorded", + "message-streams-minimum-worker-protocol-version": "1.15", + "durable-selection-minimum-worker-protocol-version": "1.19" + } + }, + "autoload": { + "psr-4": { + "DurableWorkflow\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Durable Workflow Contributors" + } + ], + "description": "PHP client and worker SDK for Durable Workflow Cloud and self-hosted Server", + "homepage": "https://php.durable-workflow.com/", + "keywords": [ + "cloud", + "durable", + "laravel", + "orchestration", + "php", + "sdk", + "symfony", + "worker", + "workflow" + ], + "support": { + "docs": "https://php.durable-workflow.com/", + "issues": "https://github.com/durable-workflow/sdk-php/issues", + "source": "https://github.com/durable-workflow/sdk-php" + }, + "time": "2026-10-14T00:51:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+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": "graham-campbell/result-type", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:06:52+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.15.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:21:06+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:11:28+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.13.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "95e7828100de18b4e269fb1703be530082d5166d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.25" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.13.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:13:11+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.11", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d0058dccf4299d70c3d9da3378b8908b32780368", + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.11" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:15:32+00:00" + }, + { + "name": "laminas/laminas-diactoros", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "4ab9fe10869ac0cd46fa78e641b6d0f4389bc6b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/4ab9fe10869ac0cd46fa78e641b6d0f4389bc6b4", + "reference": "4ab9fe10869ac0cd46fa78e641b6d0f4389bc6b4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "boundwize/structarmed": "^0.17.7", + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://crowdfunding.linuxfoundation.org/initiatives/laminas-project", + "type": "custom" + } + ], + "time": "2026-10-08T08:19:47+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.69.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-09-29T12:53:25+00:00" + }, + { + "name": "laravel/octane", + "version": "v2.21.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/octane.git", + "reference": "ce755128c9c2be39adcf8b50ec757c97b3b30eb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/octane/zipball/ce755128c9c2be39adcf8b50ec757c97b3b30eb7", + "reference": "ce755128c9c2be39adcf8b50ec757c97b3b30eb7", + "shasum": "" + }, + "require": { + "laminas/laminas-diactoros": "^3.0", + "laravel/framework": "^10.10.1|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nesbot/carbon": "^2.66.0|^3.0", + "php": "^8.1.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0|^8.0" + }, + "conflict": { + "spiral/roadrunner": "<2023.1.0", + "spiral/roadrunner-cli": "<2.6.0", + "spiral/roadrunner-http": "<3.3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.6.1", + "inertiajs/inertia-laravel": "^1.3.2|^2.0", + "laravel/scout": "^10.2.1", + "laravel/socialite": "^5.6.1", + "livewire/livewire": "^2.12.3|^3.0", + "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", + "orchestra/testbench": "^8.21|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.1.7", + "phpunit/phpunit": "^10.4|^11.5|^12.0|^13.0", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "bin": [ + "bin/roadrunner-worker", + "bin/swoole-server" + ], + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Octane": "Laravel\\Octane\\Facades\\Octane" + }, + "providers": [ + "Laravel\\Octane\\OctaneServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Octane\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Supercharge your Laravel application's performance.", + "keywords": [ + "frankenphp", + "laravel", + "octane", + "roadrunner", + "swoole" + ], + "support": { + "issues": "https://github.com/laravel/octane/issues", + "source": "https://github.com/laravel/octane" + }, + "time": "2026-10-05T14:39:46+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.26", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", + "reference": "2d1e9bf6bab8d3f383c254db7f2b4ea8816d55e0", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "source": "https://github.com/laravel/prompts/tree/v0.3.26" + }, + "time": "2026-10-09T14:45:13+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-09-22T14:32:34+00:00" + }, + { + "name": "league/commonmark", + "version": "2.10.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.11-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-09-21T13:07:34+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.36.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" + }, + "time": "2026-09-02T08:00:27+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.35.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" + }, + "time": "2026-08-12T13:29:21+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.17.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2026-07-09T11:49:27+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-09-29T19:21:12+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.14.2", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/94cfc34887f07ab5c0efa07914c8f527544e3224", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^3.0@dev", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-10-02T07:26:52+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.6", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "c54350438cd6914616f790a49cb424605f421562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.6" + }, + "time": "2026-08-16T21:58:41+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.5" + }, + "time": "2026-07-17T23:02:45+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2026-08-24T00:54:40+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+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/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+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": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+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": "roadrunner-php/roadrunner-api-dto", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/roadrunner-api-dto.git", + "reference": "81d8247894cd3f75a4e0fba2b619f9d7efa03d27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/roadrunner-api-dto/zipball/81d8247894cd3f75a4e0fba2b619f9d7efa03d27", + "reference": "81d8247894cd3f75a4e0fba2b619f9d7efa03d27", + "shasum": "" + }, + "require": { + "google/protobuf": "^4.31.1||^5.34.0", + "php": "^8.1" + }, + "conflict": { + "temporal/sdk": "<2.9.0" + }, + "suggest": { + "google/common-protos": "Required for Temporal API" + }, + "type": "library", + "autoload": { + "psr-4": { + "Temporal\\": "generated/Temporal", + "RoadRunner\\": "generated/RoadRunner", + "GPBMetadata\\": "generated/GPBMetadata" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pavel Butchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "email": "alexey.gagarin@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner PHP API", + "homepage": "https://roadrunner.dev", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev", + "forum": "https://forum.roadrunner.dev", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/roadrunner-api-dto/tree/v1.18.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2026-10-09T15:32:54+00:00" + }, + { + "name": "spiral/core", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/core.git", + "reference": "ca5b8483610aed0c82d4f06bc7907e191d88aca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/core/zipball/ca5b8483610aed0c82d4f06bc7907e191d88aca0", + "reference": "ca5b8483610aed0c82d4f06bc7907e191d88aca0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "spiral/security": "^3.17.2" + }, + "provide": { + "psr/container-implementation": "^1.1|^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.12", + "phpunit/phpunit": "^10.5.41", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Core\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "IoC container, IoC scopes, factory, memory, configuration interfaces", + "homepage": "https://spiral.dev", + "support": { + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/core" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:07:26+00:00" + }, + { + "name": "spiral/goridge", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/goridge.git", + "reference": "a14454ce6c9cfcec76bbd283292c25fb33fc5a43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/goridge/zipball/a14454ce6c9cfcec76bbd283292c25fb33fc5a43", + "reference": "a14454ce6c9cfcec76bbd283292c25fb33fc5a43", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-sockets": "*", + "php": ">=8.1", + "spiral/roadrunner": "^2023 || ^2024.1 || ^2025.1" + }, + "require-dev": { + "google/protobuf": "^3.22 || ^4.0", + "infection/infection": "^0.29.0", + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.5.45", + "rybakit/msgpack": "^0.7", + "spiral/code-style": "*", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "ext-msgpack": "MessagePack codec support", + "ext-protobuf": "Protobuf codec support", + "google/protobuf": "(^3.0) Protobuf codec support", + "rybakit/msgpack": "(^0.7) MessagePack codec support" + }, + "type": "goridge", + "autoload": { + "psr-4": { + "Spiral\\Goridge\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "High-performance PHP-to-Golang RPC bridge", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/goridge/tree/4.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2026-05-11T11:29:46+00:00" + }, + { + "name": "spiral/hmvc", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/hmvc.git", + "reference": "11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/hmvc/zipball/11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5", + "reference": "11111aac577f3d7ccc9bbe8b79dc6441fd21b9c5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1.0", + "spiral/core": "^3.17.2", + "spiral/interceptors": "^3.17.2" + }, + "require-dev": { + "phpunit/phpunit": "^10.5.41", + "spiral/testing": "^2.12", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Core\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "HMVC Core and Controllers", + "homepage": "https://spiral.dev", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://spiral.dev/docs", + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/hmvc" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:00:27+00:00" + }, + { + "name": "spiral/interceptors", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/interceptors.git", + "reference": "ea0e10ecb2c6058541a2b097a660fd2b532a5ffa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/interceptors/zipball/ea0e10ecb2c6058541a2b097a660fd2b532a5ffa", + "reference": "ea0e10ecb2c6058541a2b097a660fd2b532a5ffa", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1.0", + "spiral/core": "^3.17.2" + }, + "require-dev": { + "phpunit/phpunit": "^10.5.41", + "spiral/testing": "^2.12", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Interceptors\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "Spiral Interceptors", + "homepage": "https://spiral.dev", + "keywords": [ + "aop", + "interceptors", + "spiral" + ], + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://spiral.dev/docs", + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/interceptors" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:00:37+00:00" + }, + { + "name": "spiral/logger", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/logger.git", + "reference": "671d91a3427019a14670b614474cc57156d5cd69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/logger/zipball/671d91a3427019a14670b614474cc57156d5cd69", + "reference": "671d91a3427019a14670b614474cc57156d5cd69", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "1 - 3", + "spiral/core": "^3.17.2" + }, + "require-dev": { + "mockery/mockery": "^1.6.12", + "phpunit/phpunit": "^10.5.41", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Logger\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "LogFactory and global log listeners", + "homepage": "https://spiral.dev", + "support": { + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/logger" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:05:38+00:00" + }, + { + "name": "spiral/roadrunner", + "version": "v2025.1.15", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-server/roadrunner.git", + "reference": "321b817fab1056e404533ca1ddd200e77d1525fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-server/roadrunner/zipball/321b817fab1056e404533ca1ddd200e77d1525fd", + "reference": "321b817fab1056e404533ca1ddd200e77d1525fd", + "shasum": "" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov / Wolfy-J", + "email": "wolfy.jd@gmail.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: High-performance PHP application server and process manager written in Go and powered with plugins", + "homepage": "https://roadrunner.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-server/roadrunner/tree/v2025.1.15" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2026-06-17T14:49:38+00:00" + }, + { + "name": "spiral/roadrunner-cli", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/cli.git", + "reference": "54909d3fff22d34d1a6e894785f3df9dca0e2e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/cli/zipball/54909d3fff22d34d1a6e894785f3df9dca0e2e8c", + "reference": "54909d3fff22d34d1a6e894785f3df9dca0e2e8c", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "ext-json": "*", + "php": ">=8.1", + "spiral/roadrunner-worker": "^2 || ^3", + "spiral/tokenizer": "^2.13 || ^3.15", + "symfony/console": "^5.3 || ^6.0 || ^7.0 || ^8.0", + "symfony/http-client": "^4.4.51 || ^5.4.49 || ^6.4.17 || ^7.2 || ^8.0", + "symfony/yaml": "^5.4.49 || ^6.4.17 || ^7.2 || ^8.0" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "spiral/code-style": "^2.2.2", + "spiral/dumper": "^3.3", + "vimeo/psalm": "^6.0" + }, + "bin": [ + "bin/rr" + ], + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/spiral/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: Command Line Interface", + "homepage": "https://roadrunner.dev", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/cli/tree/v2.7.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2025-12-14T19:18:46+00:00" + }, + { + "name": "spiral/roadrunner-http", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/http.git", + "reference": "a44a5f7d54d4ee8a14fe99cd22dcd128db270c88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/http/zipball/a44a5f7d54d4ee8a14fe99cd22dcd128db270c88", + "reference": "a44a5f7d54d4ee8a14fe99cd22dcd128db270c88", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "psr/http-factory": "^1.0.1", + "psr/http-message": "^1.0.1 || ^2.0", + "roadrunner-php/roadrunner-api-dto": "^1.6", + "spiral/roadrunner": "^2023.3 || ^2024.1 || ^2025.1", + "spiral/roadrunner-worker": "^3.5", + "symfony/polyfill-php83": "^1.29" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.0", + "nyholm/psr7": "^1.3", + "phpunit/phpunit": "^10.5", + "spiral/code-style": "^2.3", + "spiral/dumper": "^3.3", + "symfony/process": "^6.2 || ^7.0", + "vimeo/psalm": "^6.13" + }, + "suggest": { + "ext-protobuf": "Provides Protocol Buffers support. Without it, performance will be lower.", + "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\Http\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: HTTP and PSR-7 worker", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev", + "forum": "https://forum.roadrunner.dev/", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/http/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2025-08-31T12:42:23+00:00" + }, + { + "name": "spiral/roadrunner-worker", + "version": "v3.6.2", + "source": { + "type": "git", + "url": "https://github.com/roadrunner-php/worker.git", + "reference": "8d9905b1e6677f34ff8623893f35b5e2fa828e37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/roadrunner-php/worker/zipball/8d9905b1e6677f34ff8623893f35b5e2fa828e37", + "reference": "8d9905b1e6677f34ff8623893f35b5e2fa828e37", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "ext-json": "*", + "ext-sockets": "*", + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0", + "spiral/goridge": "^4.1.0", + "spiral/roadrunner": "^2023.1 || ^2024.1 || ^2025.1" + }, + "require-dev": { + "buggregator/trap": "^1.13", + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.5.45", + "spiral/code-style": "^2.2", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "spiral/roadrunner-cli": "Provides RoadRunner installation and management CLI tools" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spiral\\RoadRunner\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Valery Piashchynski", + "homepage": "https://github.com/rustatian" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Pavel Buchnev (butschster)", + "email": "pavel.buchnev@spiralscout.com" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + }, + { + "name": "RoadRunner Community", + "homepage": "https://github.com/roadrunner-server/roadrunner/graphs/contributors" + } + ], + "description": "RoadRunner: PHP worker", + "homepage": "https://spiral.dev/", + "support": { + "chat": "https://discord.gg/V6EK4he", + "docs": "https://docs.roadrunner.dev", + "issues": "https://github.com/roadrunner-server/roadrunner/issues", + "source": "https://github.com/roadrunner-php/worker/tree/v3.6.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/roadrunner-server", + "type": "github" + } + ], + "time": "2025-05-05T12:34:50+00:00" + }, + { + "name": "spiral/security", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/security.git", + "reference": "aefd664efb3e618d0a57c967546f1ad5d9784580" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/security/zipball/aefd664efb3e618d0a57c967546f1ad5d9784580", + "reference": "aefd664efb3e618d0a57c967546f1ad5d9784580", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "spiral/core": "^3.17.2", + "spiral/hmvc": "^3.17.2" + }, + "require-dev": { + "mockery/mockery": "^1.6.12", + "phpunit/phpunit": "^10.5.41", + "spiral/console": "^3.17.2", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Security\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "Spiral, RBAC security layer", + "homepage": "https://spiral.dev", + "support": { + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/security" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:07:41+00:00" + }, + { + "name": "spiral/tokenizer", + "version": "3.17.2", + "source": { + "type": "git", + "url": "https://github.com/spiral/tokenizer.git", + "reference": "fcb7a36672d7aef900dc36377ff997ba6f8cde0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spiral/tokenizer/zipball/fcb7a36672d7aef900dc36377ff997ba6f8cde0b", + "reference": "fcb7a36672d7aef900dc36377ff997ba6f8cde0b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=8.1", + "spiral/core": "^3.17.2", + "spiral/logger": "^3.17.2", + "symfony/finder": "^6.4.30 || ^7.4 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6.12", + "phpunit/phpunit": "^10.5.41", + "spiral/attributes": "^2.8|^3.0", + "spiral/boot": "^3.17.2", + "spiral/files": "^3.17.2", + "vimeo/psalm": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.17.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spiral\\Tokenizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Titov (wolfy-j)", + "email": "wolfy-j@spiralscout.com" + }, + { + "name": "Pavel Butchnev (butschster)", + "homepage": "https://github.com/butschster" + }, + { + "name": "Aleksei Gagarin (roxblnfk)", + "homepage": "https://github.com/roxblnfk" + }, + { + "name": "Maksim Smakouz (msmakouz)", + "email": "maksim.smakouz@spiralscout.com" + } + ], + "description": "Static Analysis: Class and Invocation locators", + "homepage": "https://spiral.dev", + "support": { + "issues": "https://github.com/spiral/framework/issues", + "source": "https://github.com/spiral/tokenizer" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spiral", + "type": "github" + } + ], + "time": "2026-08-05T12:05:29+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "07437f612dca89c1894c1db04491002b1e798f03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/07437f612dca89c1894c1db04491002b1e798f03", + "reference": "07437f612dca89c1894c1db04491002b1e798f03", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "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": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.21" + }, + "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-12T14:31:36+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/21f1bc2296f7ff6043241a8dcbc09559b3732820", + "reference": "21f1bc2296f7ff6043241a8dcbc09559b3732820", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.21" + }, + "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-12T18:45:38+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.18" + }, + "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-23T10:03:40+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/error-handler", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.20" + }, + "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-22T18:56:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" + }, + "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-21T17:40:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^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\\EventDispatcher\\": "" + } + }, + "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 dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-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/finder", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.20" + }, + "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-26T06:31:10+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "121bf3a973bfd852db85ec32d0d64e9fbed4449b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/121bf3a973bfd852db85ec32d0d64e9fbed4449b", + "reference": "121bf3a973bfd852db85ec32d0d64e9fbed4449b", + "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.21" + }, + "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-13T15:31:38+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/http-foundation", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "58c89144c7a36dd89c3d01917e9fa3a72d8aaf31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/58c89144c7a36dd89c3d01917e9fa3a72d8aaf31", + "reference": "58c89144c7a36dd89c3d01917e9fa3a72d8aaf31", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.21" + }, + "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-08T16:56:12+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "4779fce7dedacb8801b2b61c703ef99159502331" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4779fce7dedacb8801b2b61c703ef99159502331", + "reference": "4779fce7dedacb8801b2b61c703ef99159502331", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.21" + }, + "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-14T06:44:08+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.19" + }, + "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-15T06:01:15+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.19" + }, + "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-04T10:45:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.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-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "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 intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/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:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/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-23T14:40:04+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "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 for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/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-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "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.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-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "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\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/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/polyfill-php84", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", + "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\\Php84\\": "" + }, + "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.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/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-23T22:01:09+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "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\\Php85\\": "" + }, + "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.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/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:46:47+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/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-24T05:20:52+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "e5247f4a5e0797d232ad8e8e6803aeb341843c83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/e5247f4a5e0797d232ad8e8e6803aeb341843c83", + "reference": "e5247f4a5e0797d232ad8e8e6803aeb341843c83", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.21" + }, + "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-08T16:56:12+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "b05964aa9d788bc8ffcc31c59d5c4573e3d4d1fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/b05964aa9d788bc8ffcc31c59d5c4573e3d4d1fc", + "reference": "b05964aa9d788bc8ffcc31c59d5c4573e3d4d1fc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.4.21" + }, + "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-07T08:20:48+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "7a344cf871532d081875bfdd18f3dff8dca36c5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/7a344cf871532d081875bfdd18f3dff8dca36c5b", + "reference": "7a344cf871532d081875bfdd18f3dff8dca36c5b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.21" + }, + "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-11T18:40:59+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": "symfony/string", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/9bc35d074ad16a58c21cd52b18c5cacc4f4c3981", + "reference": "9bc35d074ad16a58c21cd52b18c5cacc4f4c3981", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "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 an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.21" + }, + "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-03T15:15:30+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.17" + }, + "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-21T17:40:08+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "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\\Translation\\": "" + }, + "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 translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-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/uid", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "3e706a245b5343ea93be9cafe84525bfb54e60fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/3e706a245b5343ea93be9cafe84525bfb54e60fc", + "reference": "3e706a245b5343ea93be9cafe84525bfb54e60fc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.21" + }, + "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-12T14:31:36+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/55d06b02f49eca9bff61828548988e6f8763e8a6", + "reference": "55d06b02f49eca9bff61828548988e6f8763e8a6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "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 mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.21" + }, + "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-13T05:48:33+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.21", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "044907929c7999814378be22da34722c7a86bad4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/044907929c7999814378be22da34722c7a86bad4", + "reference": "044907929c7999814378be22da34722c7a86bad4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.21" + }, + "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-12T12:34:30+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpdotenv/phpdotenv.git", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpdotenv/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2026-08-24T18:07:49+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3.35" + }, + "plugin-api-version": "2.9.0" +} diff --git a/examples/laravel-http/config/app.php b/examples/laravel-http/config/app.php new file mode 100644 index 00000000..423eed59 --- /dev/null +++ b/examples/laravel-http/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/examples/laravel-http/config/auth.php b/examples/laravel-http/config/auth.php new file mode 100644 index 00000000..d7568ff1 --- /dev/null +++ b/examples/laravel-http/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/examples/laravel-http/config/cache.php b/examples/laravel-http/config/cache.php new file mode 100644 index 00000000..b32aead2 --- /dev/null +++ b/examples/laravel-http/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/examples/laravel-http/config/durable-workflow.php b/examples/laravel-http/config/durable-workflow.php new file mode 100644 index 00000000..3a79af58 --- /dev/null +++ b/examples/laravel-http/config/durable-workflow.php @@ -0,0 +1,16 @@ + env('DURABLE_WORKFLOW_RUNTIME_URL', 'http://localhost:8080'), + 'namespace' => env('DURABLE_WORKFLOW_NAMESPACE', 'service-http-example'), + 'task_queue' => env('DURABLE_WORKFLOW_TASK_QUEUE', 'service-http-example'), + 'handlers' => [App\Workflows\HttpWorkflow::class, App\Workflows\EchoActivity::class], + 'poll_timeout_seconds' => 5, + 'worker' => [ + 'sticky_cache' => [ + 'capacity' => filter_var(env('EXAMPLE_HISTORY_CAPACITY', 100), FILTER_VALIDATE_INT), + 'ttl_seconds' => filter_var(env('EXAMPLE_HISTORY_TTL', 300), FILTER_VALIDATE_INT), + ], + ], + // Runtime credentials are read from the process environment by the SDK. +]; diff --git a/examples/laravel-http/config/example.php b/examples/laravel-http/config/example.php new file mode 100644 index 00000000..75ded8a7 --- /dev/null +++ b/examples/laravel-http/config/example.php @@ -0,0 +1,7 @@ + (bool) env('EXAMPLE_EAGER', false), + // These are disposable application identities, not runtime credentials. + 'users' => ['example-alpha' => 'alpha', 'example-beta' => 'beta'], +]; diff --git a/examples/laravel-http/config/filesystems.php b/examples/laravel-http/config/filesystems.php new file mode 100644 index 00000000..37d8fca4 --- /dev/null +++ b/examples/laravel-http/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/examples/laravel-http/config/logging.php b/examples/laravel-http/config/logging.php new file mode 100644 index 00000000..b09cb25d --- /dev/null +++ b/examples/laravel-http/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/examples/laravel-http/config/mail.php b/examples/laravel-http/config/mail.php new file mode 100644 index 00000000..e32e88da --- /dev/null +++ b/examples/laravel-http/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/examples/laravel-http/config/octane.php b/examples/laravel-http/config/octane.php new file mode 100644 index 00000000..b5b609ed --- /dev/null +++ b/examples/laravel-http/config/octane.php @@ -0,0 +1,254 @@ + env('OCTANE_SERVER', 'roadrunner'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. + | + */ + + 'https' => env('OCTANE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + ...Octane::prepareApplicationForNextRequest(), + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // FlushUploadedFiles::class, + ], + + TaskReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushOnce::class, + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + // CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + CloseMonologHandlers::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + ...Octane::defaultServicesToWarm(), + ], + + 'flush' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ + + 'tables' => [ + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => 1000, + 'bytes' => 10000, + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config/**/*.php', + 'database/**/*.php', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => 50, + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => 30, + + /* + |-------------------------------------------------------------------------- + | Octane Server State File + |-------------------------------------------------------------------------- + | + | This value determines where Octane stores the state file used to track + | the running server's master process ID and admin endpoint, which is + | read by various Octane commands. You may tweak this if necessary. + | + */ + + 'state_file' => env('OCTANE_STATE_FILE', storage_path('logs/octane-server-state.json')), + + /* + |-------------------------------------------------------------------------- + | RoadRunner Options + |-------------------------------------------------------------------------- + | + | The following options are only used when the RoadRunner server is the + | server powering your application. You may customize the path to the + | worker binary here, which is useful for zero-downtime deployments + | where the application is served from a symlinked "current" path. + | + */ + + 'roadrunner' => [ + 'command' => env('OCTANE_ROADRUNNER_COMMAND', 'vendor/bin/roadrunner-worker'), + 'http_middleware' => env('OCTANE_ROADRUNNER_HTTP_MIDDLEWARE', 'static'), + ], + +]; diff --git a/examples/laravel-http/config/queue.php b/examples/laravel-http/config/queue.php new file mode 100644 index 00000000..79c2c0a2 --- /dev/null +++ b/examples/laravel-http/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/examples/laravel-http/config/services.php b/examples/laravel-http/config/services.php new file mode 100644 index 00000000..6a90eb83 --- /dev/null +++ b/examples/laravel-http/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/examples/laravel-http/config/session.php b/examples/laravel-http/config/session.php new file mode 100644 index 00000000..5b541b75 --- /dev/null +++ b/examples/laravel-http/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/examples/laravel-http/public/index.php b/examples/laravel-http/public/index.php new file mode 100644 index 00000000..fc2dfc9e --- /dev/null +++ b/examples/laravel-http/public/index.php @@ -0,0 +1,11 @@ +handleRequest(Request::capture()); diff --git a/examples/laravel-http/qualification/context.mjs b/examples/laravel-http/qualification/context.mjs new file mode 100644 index 00000000..cd2ca924 --- /dev/null +++ b/examples/laravel-http/qualification/context.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; + +const origin = process.env.EXAMPLE_ORIGIN ?? 'http://nginx:8080'; +const namespace = process.env.DURABLE_WORKFLOW_NAMESPACE ?? 'service-http-example'; +const pids = new Set(); + +for (let index = 0; index < 40; index++) { + const user = index % 2 === 0 ? 'alpha' : 'beta'; + const headers = { + Authorization: `Bearer example-${user}`, + Accept: 'application/json', + 'X-Request-Id': `context-${index}`, + 'X-Namespace': namespace, + }; + if (index % 3 === 0) headers['X-Optional-Context'] = `optional-${index}`; + const response = await fetch(`${origin}/api/context`, {headers, signal: AbortSignal.timeout(10_000)}); + assert.equal(response.status, 200); + assert.deepEqual(await response.json(), {user, namespace}); + assert.equal(response.headers.get('X-Request-Id'), `context-${index}`); + assert.equal(response.headers.get('X-Example-User'), user); + assert.equal(response.headers.get('X-Example-Namespace'), namespace); + assert.equal(response.headers.get('X-Optional-Context'), headers['X-Optional-Context'] ?? ''); + assert.match(response.headers.get('X-Worker-Pid'), /^\d+$/); + pids.add(response.headers.get('X-Worker-Pid')); +} + +for (const [headers, expectedStatus] of [ + [{}, 401], + [{Authorization: 'Bearer example-alpha', 'X-Request-Id': 'foreign', 'X-Namespace': `${namespace}-foreign`}, 403], + [{Authorization: 'Bearer example-beta', 'X-Request-Id': 'invalid!'}, 422], +]) { + const response = await fetch(`${origin}/api/context`, { + headers: {...headers, Accept: 'application/json'}, signal: AbortSignal.timeout(10_000), + }); + assert.equal(response.status, expectedStatus); + await response.arrayBuffer(); +} + +console.log(JSON.stringify({requests: 40, refusals: 3, http_worker_pids: [...pids]})); diff --git a/examples/laravel-http/routes/api.php b/examples/laravel-http/routes/api.php new file mode 100644 index 00000000..d9bbe44c --- /dev/null +++ b/examples/laravel-http/routes/api.php @@ -0,0 +1,20 @@ +group(function (): void { + Route::get('context', function (Request $request, Client $client) { + return response()->json([ + 'user' => $request->attributes->get('example_user'), + 'namespace' => $client->namespace, + ]); + }); + Route::post('workflows', [WorkflowController::class, 'start']); + Route::get('workflows/{id}', [WorkflowController::class, 'status']); + Route::post('workflows/{id}/signal', [WorkflowController::class, 'signal']); + Route::post('workflows/{id}/query', [WorkflowController::class, 'query']); + Route::post('workflows/{id}/cancel', [WorkflowController::class, 'cancel']); +}); diff --git a/examples/laravel-http/routes/console.php b/examples/laravel-http/routes/console.php new file mode 100644 index 00000000..b3d9bbc7 --- /dev/null +++ b/examples/laravel-http/routes/console.php @@ -0,0 +1 @@ + Date: Wed, 14 Oct 2026 17:19:43 +0000 Subject: [PATCH 02/28] Verify eager HTTP resolution and request log isolation --- .../app/Providers/AppServiceProvider.php | 4 ++- examples/laravel-http/compose.yaml | 2 ++ .../laravel-http/qualification/context.mjs | 27 ++++++++++++++++--- examples/laravel-http/routes/api.php | 1 + 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/examples/laravel-http/app/Providers/AppServiceProvider.php b/examples/laravel-http/app/Providers/AppServiceProvider.php index 1a56ece7..a79c2def 100644 --- a/examples/laravel-http/app/Providers/AppServiceProvider.php +++ b/examples/laravel-http/app/Providers/AppServiceProvider.php @@ -10,9 +10,11 @@ class AppServiceProvider extends ServiceProvider { public function boot(): void { - if (! $this->app->runningInConsole() && config('example.eager')) { + // Octane HTTP workers bootstrap through CLI. Preparation has no credentials. + if (getenv('EXAMPLE_PROCESS_ROLE') === 'http' && config('example.eager')) { $this->app->make(Client::class); $this->app->make(LaravelWorkflowClientInterface::class); } + $this->app->instance('example.sdk_boot_resolved', $this->app->resolved(Client::class)); } } diff --git a/examples/laravel-http/compose.yaml b/examples/laravel-http/compose.yaml index f0e55ba6..b43cad55 100644 --- a/examples/laravel-http/compose.yaml +++ b/examples/laravel-http/compose.yaml @@ -44,6 +44,7 @@ services: environment: <<: *environment DURABLE_WORKFLOW_CLIENT_TOKEN: ${DURABLE_WORKFLOW_CLIENT_TOKEN:?Set the application client-role credential} + EXAMPLE_PROCESS_ROLE: http init: true stop_signal: SIGTERM command: php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000 --workers=2 --max-requests=500 @@ -60,6 +61,7 @@ services: environment: <<: *environment DURABLE_WORKFLOW_WORKER_TOKEN: ${DURABLE_WORKFLOW_WORKER_TOKEN:?Set a separate worker-role credential} + EXAMPLE_PROCESS_ROLE: worker init: true command: php artisan durable-workflow:worker depends_on: diff --git a/examples/laravel-http/qualification/context.mjs b/examples/laravel-http/qualification/context.mjs index cd2ca924..932523c3 100644 --- a/examples/laravel-http/qualification/context.mjs +++ b/examples/laravel-http/qualification/context.mjs @@ -1,22 +1,25 @@ import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; const origin = process.env.EXAMPLE_ORIGIN ?? 'http://nginx:8080'; const namespace = process.env.DURABLE_WORKFLOW_NAMESPACE ?? 'service-http-example'; const pids = new Set(); +const expectedEager = process.env.EXAMPLE_EXPECT_EAGER === 'true'; +const prefix = process.env.EXAMPLE_REQUEST_PREFIX ?? 'context'; for (let index = 0; index < 40; index++) { const user = index % 2 === 0 ? 'alpha' : 'beta'; const headers = { Authorization: `Bearer example-${user}`, Accept: 'application/json', - 'X-Request-Id': `context-${index}`, + 'X-Request-Id': `${prefix}-${index}`, 'X-Namespace': namespace, }; if (index % 3 === 0) headers['X-Optional-Context'] = `optional-${index}`; const response = await fetch(`${origin}/api/context`, {headers, signal: AbortSignal.timeout(10_000)}); assert.equal(response.status, 200); - assert.deepEqual(await response.json(), {user, namespace}); - assert.equal(response.headers.get('X-Request-Id'), `context-${index}`); + assert.deepEqual(await response.json(), {user, namespace, client_resolved_at_boot: expectedEager}); + assert.equal(response.headers.get('X-Request-Id'), `${prefix}-${index}`); assert.equal(response.headers.get('X-Example-User'), user); assert.equal(response.headers.get('X-Example-Namespace'), namespace); assert.equal(response.headers.get('X-Optional-Context'), headers['X-Optional-Context'] ?? ''); @@ -36,4 +39,20 @@ for (const [headers, expectedStatus] of [ await response.arrayBuffer(); } -console.log(JSON.stringify({requests: 40, refusals: 3, http_worker_pids: [...pids]})); +let logRecords = 0; +if (process.env.EXAMPLE_LOG_PATH) { + const contents = await readFile(process.env.EXAMPLE_LOG_PATH, 'utf8'); + const records = contents.split('\n').filter(line => line.includes('example.request {')) + .map(line => JSON.parse(line.slice(line.indexOf('{')))); + for (let index = 0; index < 40; index++) { + const matches = records.filter(record => record.request_id === `${prefix}-${index}`); + assert.equal(matches.length, 1); + assert.equal(matches[0].user, index % 2 === 0 ? 'alpha' : 'beta'); + assert.equal(matches[0].namespace, namespace); + assert.equal(matches[0].optional_context, index % 3 === 0 ? `optional-${index}` : null); + assert.equal(matches[0].status, 200); + logRecords++; + } +} + +console.log(JSON.stringify({requests: 40, refusals: 3, log_records: logRecords, eager_at_boot: expectedEager, http_worker_pids: [...pids]})); diff --git a/examples/laravel-http/routes/api.php b/examples/laravel-http/routes/api.php index d9bbe44c..3e908c7b 100644 --- a/examples/laravel-http/routes/api.php +++ b/examples/laravel-http/routes/api.php @@ -10,6 +10,7 @@ return response()->json([ 'user' => $request->attributes->get('example_user'), 'namespace' => $client->namespace, + 'client_resolved_at_boot' => app('example.sdk_boot_resolved'), ]); }); Route::post('workflows', [WorkflowController::class, 'start']); From 827417caa628b78ca3fdd21e79d525460d19133d Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 17:35:30 +0000 Subject: [PATCH 03/28] Add published-runtime lifecycle controls to HTTP example --- examples/laravel-http/bootstrap/app.php | 11 ++++ .../laravel-http/qualification/lifecycle.mjs | 64 +++++++++++++++++++ .../qualification/verify-results.php | 23 +++++++ 3 files changed, 98 insertions(+) create mode 100644 examples/laravel-http/qualification/lifecycle.mjs create mode 100644 examples/laravel-http/qualification/verify-results.php diff --git a/examples/laravel-http/bootstrap/app.php b/examples/laravel-http/bootstrap/app.php index af21aa2b..4b51fdc4 100644 --- a/examples/laravel-http/bootstrap/app.php +++ b/examples/laravel-http/bootstrap/app.php @@ -3,6 +3,8 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Illuminate\Http\Request; +use DurableWorkflow\Exception\ServerException; $application = Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -14,6 +16,15 @@ $middleware->alias(['example' => \App\Http\Middleware\ExampleContext::class]); }) ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->render(function (ServerException $exception, Request $request) { + if ($request->is('api/*')) { + $status = $exception->status >= 400 && $exception->status <= 599 ? $exception->status : 502; + + return response()->json([ + 'error' => 'runtime_operation_refused', 'reason' => $exception->reason, + ], $status); + } + }); })->create(); $application->useStoragePath(getenv('EXAMPLE_STORAGE_PATH') ?: dirname(__DIR__).'/storage'); diff --git a/examples/laravel-http/qualification/lifecycle.mjs b/examples/laravel-http/qualification/lifecycle.mjs new file mode 100644 index 00000000..25c1c893 --- /dev/null +++ b/examples/laravel-http/qualification/lifecycle.mjs @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; + +const origin = process.env.EXAMPLE_ORIGIN ?? 'http://nginx:8080'; +const prefix = process.env.EXAMPLE_RUN_PREFIX; +assert.match(prefix ?? '', /^[a-zA-Z0-9_-]{1,40}$/, 'Select a fresh run prefix before admission.'); +let requestNumber = 0; +const originals = []; + +async function request(user, path, method = 'GET', body, expectedStatus = 200) { + const response = await fetch(`${origin}/api/${path}`, { + method, signal: AbortSignal.timeout(30_000), + headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', + 'Content-Type': 'application/json', 'X-Request-Id': `${prefix}-request-${requestNumber++}`}, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const raw = await response.text(); + assert.equal(response.status, expectedStatus, raw); + return {raw, value: raw === '' ? null : JSON.parse(raw)}; +} + +async function waitStatus(user, id, expected) { + const deadline = performance.now() + 35_000; + do { + const result = await request(user, `workflows/${id}`); + if (result.value.status?.toLowerCase() === expected) return result; + await new Promise(resolve => setTimeout(resolve, 250)); + } while (performance.now() < deadline); + throw new Error(`Original ${user}/${id} did not reach ${expected} within its 35-second observer.`); +} + +for (const [user, kind, suffix] of [ + ['alpha', 'short', 'short-a'], ['beta', 'short', 'short-b'], + ['alpha', 'timer', 'timer'], ['beta', 'signal', 'signal'], ['alpha', 'cancel', 'cancel'], +]) { + const id = `${prefix}-${suffix}`; + const {value} = await request(user, 'workflows', 'POST', {id, kind}, 201); + assert.equal(value.workflow_id, `${user}-${id}`); + assert.equal(typeof value.run_id, 'string'); + originals.push({user, kind, id, workflow_id: value.workflow_id, run_id: value.run_id}); +} + +for (const original of originals.filter(value => ['short', 'timer'].includes(value.kind))) { + const result = await waitStatus(original.user, original.id, 'completed'); + assert.equal(result.value.output.user, original.user); + assert.equal(result.value.output.echo, `hello ${original.user}`); + assert.equal(result.value.output.nullable, null); + assert.equal(result.value.output.text, '雪'); + assert.match(result.raw, /"number":9007199254740993(?:,|})/); +} + +const signal = originals.find(value => value.kind === 'signal'); +await waitStatus(signal.user, signal.id, 'waiting'); +assert.equal((await request(signal.user, `workflows/${signal.id}/query`, 'POST')).value.phase, 'waiting'); +await request(signal.user, `workflows/${signal.id}/signal`, 'POST', undefined, 204); +await waitStatus(signal.user, signal.id, 'completed'); + +const cancel = originals.find(value => value.kind === 'cancel'); +await waitStatus(cancel.user, cancel.id, 'waiting'); +assert.equal((await request(cancel.user, `workflows/${cancel.id}/query`, 'POST')).value.phase, 'waiting'); +await request(cancel.user, `workflows/${cancel.id}/cancel`, 'POST', undefined, 204); +await waitStatus(cancel.user, cancel.id, 'cancelled'); +await request('beta', `workflows/${prefix}-short-a`, 'GET', undefined, 404); + +console.log(JSON.stringify({originals, completed: 4, cancelled: 1, queries: 2, signal: 1, foreign_user_refusal: 404})); diff --git a/examples/laravel-http/qualification/verify-results.php b/examples/laravel-http/qualification/verify-results.php new file mode 100644 index 00000000..bb4b9082 --- /dev/null +++ b/examples/laravel-http/qualification/verify-results.php @@ -0,0 +1,23 @@ +workflowHandle($user.'-'.$prefix.'-'.$suffix)->result(15); + $expected = ['user' => $user, 'echo' => 'hello '.$user, 'number' => 9007199254740993, 'nullable' => null, 'text' => '雪']; + if ($result !== $expected) { + throw new RuntimeException('The original typed workflow output changed.'); + } +} +echo "Four original typed results pass, including exact int64/null/Unicode.\n"; From 221dceedefb25148d4d1f17d1d87bfc93b53c27e Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 17:36:10 +0000 Subject: [PATCH 04/28] Preserve lifecycle observer deadline across status reads --- examples/laravel-http/qualification/lifecycle.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/laravel-http/qualification/lifecycle.mjs b/examples/laravel-http/qualification/lifecycle.mjs index 25c1c893..539db948 100644 --- a/examples/laravel-http/qualification/lifecycle.mjs +++ b/examples/laravel-http/qualification/lifecycle.mjs @@ -6,9 +6,9 @@ assert.match(prefix ?? '', /^[a-zA-Z0-9_-]{1,40}$/, 'Select a fresh run prefix b let requestNumber = 0; const originals = []; -async function request(user, path, method = 'GET', body, expectedStatus = 200) { +async function request(user, path, method = 'GET', body, expectedStatus = 200, timeout = 30_000) { const response = await fetch(`${origin}/api/${path}`, { - method, signal: AbortSignal.timeout(30_000), + method, signal: AbortSignal.timeout(Math.max(1, Math.floor(timeout))), headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', 'Content-Type': 'application/json', 'X-Request-Id': `${prefix}-request-${requestNumber++}`}, body: body === undefined ? undefined : JSON.stringify(body), @@ -21,7 +21,8 @@ async function request(user, path, method = 'GET', body, expectedStatus = 200) { async function waitStatus(user, id, expected) { const deadline = performance.now() + 35_000; do { - const result = await request(user, `workflows/${id}`); + const result = await request(user, `workflows/${id}`, 'GET', undefined, 200, Math.min(30_000, deadline - performance.now())); + assert.ok(performance.now() <= deadline, 'The original observation deadline expired.'); if (result.value.status?.toLowerCase() === expected) return result; await new Promise(resolve => setTimeout(resolve, 250)); } while (performance.now() < deadline); From a121ff485cbd1d36044c2eda6b57c81a58d0e07a Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 17:54:31 +0000 Subject: [PATCH 05/28] Send SIGTERM to the independent service worker --- examples/laravel-http/compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/laravel-http/compose.yaml b/examples/laravel-http/compose.yaml index b43cad55..b084ae2b 100644 --- a/examples/laravel-http/compose.yaml +++ b/examples/laravel-http/compose.yaml @@ -63,6 +63,7 @@ services: DURABLE_WORKFLOW_WORKER_TOKEN: ${DURABLE_WORKFLOW_WORKER_TOKEN:?Set a separate worker-role credential} EXAMPLE_PROCESS_ROLE: worker init: true + stop_signal: SIGTERM command: php artisan durable-workflow:worker depends_on: prepare: From 6f4c6703b08b1c0f5c461221d968a5feefc18f60 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 18:05:56 +0000 Subject: [PATCH 06/28] Document lifecycle qualification and worker stop signal --- examples/laravel-http/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index cd6fe6c7..a0864c92 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -90,6 +90,20 @@ route resolves the normal SDK client without sending a runtime request. It checks HTTP request isolation only; it cannot prove workflow execution or runtime namespace authorization. +With the real runtime and CLI worker ready, choose a fresh original prefix and +run `qualification/lifecycle.mjs` with `EXAMPLE_ORIGIN` and `EXAMPLE_RUN_PREFIX`. +It admits five originals once, observes four completions and one cancellation, +and checks signal/query and a foreign-user refusal. Status observation budgets +are 35 seconds; individual HTTP calls are capped at the remaining budget. +`qualification/verify-results.php` reads those same original results through the +installed PHP SDK and checks the exact int64/null/Unicode types. Run the drivers +in a separate tool container so their resources are excluded from the fixed +application allocation. + +The durable worker explicitly uses Docker's SIGTERM stop signal. The FPM image's +default SIGQUIT belongs to the HTTP process; use SIGTERM for the CLI worker so +its normal SDK shutdown can deregister. + History-cache defaults remain 100 entries/300 seconds. The existing SDK validates capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and `EXAMPLE_HISTORY_TTL` select this process's settings before startup. A cache hit From 5b50e9e108c50c277afd84f759b99cee000539f4 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 18:16:34 +0000 Subject: [PATCH 07/28] Prepare original timer checkpoint for CLI recovery qualification --- examples/laravel-http/README.md | 12 ++++ .../Http/Controllers/WorkflowController.php | 2 +- .../app/Workflows/HttpWorkflow.php | 2 + .../qualification/recovery-checkpoint.php | 56 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 examples/laravel-http/qualification/recovery-checkpoint.php diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index a0864c92..8da462e8 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -104,6 +104,18 @@ The durable worker explicitly uses Docker's SIGTERM stop signal. The FPM image's default SIGQUIT belongs to the HTTP process; use SIGTERM for the CLI worker so its normal SDK shutdown can deregister. +The separate `recovery-timer` kind waits ten seconds after its echo activity. +It provides a checkpoint before testing CLI replacement. Use the returned +original workflow/run IDs with `qualification/recovery-checkpoint.php before`, +writing a temporary file under the private state directory. The check requires +one completed activity and one unfired persisted timer. An interruption control +must preserve the same original IDs and accepted 600/900-second deadlines, use +the declared observation budget, and never seed a replacement workflow or +manually repair history. After the replacement completes, `after` requires the +whole original prefix, unchanged deadline fields and exact typed result. Delete +the temporary checkpoint when the control ends. This helper's presence is not +proof that SIGKILL or connection-loss recovery has been qualified. + History-cache defaults remain 100 entries/300 seconds. The existing SDK validates capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and `EXAMPLE_HISTORY_TTL` select this process's settings before startup. A cache hit diff --git a/examples/laravel-http/app/Http/Controllers/WorkflowController.php b/examples/laravel-http/app/Http/Controllers/WorkflowController.php index e79a7d20..b0e53259 100644 --- a/examples/laravel-http/app/Http/Controllers/WorkflowController.php +++ b/examples/laravel-http/app/Http/Controllers/WorkflowController.php @@ -17,7 +17,7 @@ public function __construct(private readonly LaravelWorkflowClientInterface $wor public function start(Request $request) { - $values = $request->validate(['id' => 'required|alpha_dash|max:60', 'kind' => 'required|in:short,timer,signal,cancel']); + $values = $request->validate(['id' => 'required|alpha_dash|max:60', 'kind' => 'required|in:short,timer,recovery-timer,signal,cancel']); $handle = $this->workflows->start( HttpWorkflow::class, [$request->attributes->get('example_user'), $values['kind']], diff --git a/examples/laravel-http/app/Workflows/HttpWorkflow.php b/examples/laravel-http/app/Workflows/HttpWorkflow.php index ac8fa98f..5820af53 100644 --- a/examples/laravel-http/app/Workflows/HttpWorkflow.php +++ b/examples/laravel-http/app/Workflows/HttpWorkflow.php @@ -18,6 +18,8 @@ public function run(WorkflowContext $context, string $user, string $kind): array $echo = $context->activity('example.echo', [$user]); if ($kind === 'timer') { $context->sleep(1); + } elseif ($kind === 'recovery-timer') { + $context->sleep(10); } elseif ($kind === 'signal') { $context->waitCondition(fn (): bool => $context->signals('release') !== [], key: 'release'); } elseif ($kind === 'cancel') { diff --git a/examples/laravel-http/qualification/recovery-checkpoint.php b/examples/laravel-http/qualification/recovery-checkpoint.php new file mode 100644 index 00000000..1f1e2b18 --- /dev/null +++ b/examples/laravel-http/qualification/recovery-checkpoint.php @@ -0,0 +1,56 @@ +describeWorkflow($workflowId, $runId); +$page = $client->workflowHistory($workflowId, $runId, pageSize: 100); +if (($page['next_page_token'] ?? null) !== null) { + throw new RuntimeException('The bounded recovery recipe unexpectedly has another history page.'); +} +$history = $page['events'] ?? null; +if (!is_array($history) || $history === [] || !array_is_list($history)) { + throw new RuntimeException('Canonical original history is unavailable.'); +} +$counts = array_count_values(array_column($history, 'event_type')); +if (($counts['ActivityCompleted'] ?? 0) !== 1 || ($counts['ActivityScheduled'] ?? 0) !== 1 || ($counts['TimerScheduled'] ?? 0) !== 1) { + throw new RuntimeException('The original activity or durable timer is missing or duplicated.'); +} +$deadlines = []; +foreach (['run_deadline_at', 'execution_deadline_at'] as $name) { + $value = $execution->raw[$name] ?? null; + if (!is_string($value) || $value === '') throw new RuntimeException('The original deadline is unavailable.'); + $deadlines[$name] = $value; +} +if ($phase === 'before') { + if (strtolower((string) $execution->status) !== 'waiting' || isset($counts['TimerFired']) || isset($counts['WorkflowCompleted'])) { + throw new RuntimeException('The original is not waiting at its unfired durable timer.'); + } + $checkpoint = ['workflow_id' => $workflowId, 'run_id' => $runId, 'history' => $history, 'deadlines' => $deadlines]; + if (file_put_contents($checkpointPath, json_encode($checkpoint, JSON_THROW_ON_ERROR)) === false) { + throw new RuntimeException('Cannot retain the temporary original checkpoint.'); + } +} else { + $before = json_decode(file_get_contents($checkpointPath), true, 512, JSON_THROW_ON_ERROR); + if ($before['workflow_id'] !== $workflowId || $before['run_id'] !== $runId + || $before['deadlines'] !== $deadlines || array_slice($history, 0, count($before['history'])) !== $before['history']) { + throw new RuntimeException('Recovery changed the original identity, history prefix or deadlines.'); + } + if (strtolower((string) $execution->status) !== 'completed' || ($counts['TimerFired'] ?? 0) !== 1 || ($counts['WorkflowCompleted'] ?? 0) !== 1) { + throw new RuntimeException('The same original did not complete once after its timer.'); + } + $result = $client->workflowHandle($workflowId, $runId)->resultOfSelectedRun(15); + $expected = ['user' => 'alpha', 'echo' => 'hello alpha', 'number' => 9007199254740993, 'nullable' => null, 'text' => '雪']; + if ($result !== $expected) throw new RuntimeException('The original typed result changed.'); +} +echo json_encode(['phase' => $phase, 'workflow_id' => $workflowId, 'run_id' => $runId, 'events' => $counts], JSON_THROW_ON_ERROR), PHP_EOL; From 7113527ba0a775c70e82a7eef4541c9e57c1559f Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 18:35:05 +0000 Subject: [PATCH 08/28] docs: record original-run worker replacement qualification --- examples/laravel-http/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 8da462e8..2775a645 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -113,8 +113,11 @@ must preserve the same original IDs and accepted 600/900-second deadlines, use the declared observation budget, and never seed a replacement workflow or manually repair history. After the replacement completes, `after` requires the whole original prefix, unchanged deadline fields and exact typed result. Delete -the temporary checkpoint when the control ends. This helper's presence is not -proof that SIGKILL or connection-loss recovery has been qualified. +the temporary checkpoint when the control ends. + +One actual [SIGKILL and cold replacement control](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6175531934) +passes with the same original run, complete history prefix and unchanged +deadlines. It does not qualify cache expiry, eviction or runtime connection loss. History-cache defaults remain 100 entries/300 seconds. The existing SDK validates capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and From 8303c954daea3a29d093249b7080306caa79d689 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 20:44:12 +0000 Subject: [PATCH 09/28] Pin the HTTP example to qualified PHP SDK 2.4.1 diagnostics --- examples/laravel-http/composer.json | 2 +- examples/laravel-http/composer.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/laravel-http/composer.json b/examples/laravel-http/composer.json index d6753e69..d7e8e398 100644 --- a/examples/laravel-http/composer.json +++ b/examples/laravel-http/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": "^8.3", - "durable-workflow/sdk": "2.4.0", + "durable-workflow/sdk": "2.4.1", "laravel/framework": "12.69.3", "laravel/octane": "2.21.0", "spiral/roadrunner-cli": "^2.6.0", diff --git a/examples/laravel-http/composer.lock b/examples/laravel-http/composer.lock index 45e0b513..bf194365 100644 --- a/examples/laravel-http/composer.lock +++ b/examples/laravel-http/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": "a6900d54b2d93f597bf6402efe37a357", + "content-hash": "b5da26409c37aa4e6425bcf712ead87b", "packages": [ { "name": "apache/avro", @@ -585,16 +585,16 @@ }, { "name": "durable-workflow/sdk", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/durable-workflow/sdk-php.git", - "reference": "821716a92f179ec360e045c28979a86a5f587075" + "reference": "520a53f2c4c7fe19358556375302e8e47e09e519" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/821716a92f179ec360e045c28979a86a5f587075", - "reference": "821716a92f179ec360e045c28979a86a5f587075", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/520a53f2c4c7fe19358556375302e8e47e09e519", + "reference": "520a53f2c4c7fe19358556375302e8e47e09e519", "shasum": "" }, "require": { @@ -635,7 +635,7 @@ "laravel": "^9.0|^10.0|^11.0|^12.0|^13.0", "symfony": "^6.4|^7.0|^8.0" }, - "product-train": "2.4.0", + "product-train": "2.4.1", "payload-codecs": [ "avro" ], @@ -701,7 +701,7 @@ "issues": "https://github.com/durable-workflow/sdk-php/issues", "source": "https://github.com/durable-workflow/sdk-php" }, - "time": "2026-10-14T00:51:58+00:00" + "time": "2026-10-14T19:53:59+00:00" }, { "name": "egulias/email-validator", From 50259e69a2aedd005d1bafc00e913e2d6cbc70fd Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 21:01:26 +0000 Subject: [PATCH 10/28] Add published cache and connection recovery checks to the HTTP example --- examples/laravel-http/README.md | 37 ++++++ examples/laravel-http/qualification/cache.php | 114 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 examples/laravel-http/qualification/cache.php diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 2775a645..8689ad97 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -125,6 +125,43 @@ capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and retains history and still replays instructions. HTTP reuse does not retain a workflow Fiber or avoid execution replay. +### Cache and connection checks + +Run `qualification/cache.php` in a separate tool container with only the client +credential and a private writable checkpoint path. Start a fresh CLI process +with the settings below before admitting each profile. The driver fixes a +90-second observation, 600-second run and 900-second execution budget, checks the +whole pre-release history prefix and requires exact typed output once. + +| Mode | Capacity / TTL | Action | +| --- | --- | --- | +| `expiry` | 100 / 1 second | One waiting original; idle three seconds, then release it. | +| `eviction` | 1 / 300 seconds | Two waiting originals; release the target and its neighbor. | +| `checkpoint`, then `resume` | 100 / 1 second | Preserve one waiting original across a bounded test-runtime outage. | + +```bash +php qualification/cache.php expiry fresh-expiry /private/expiry.json +php qualification/cache.php eviction fresh-eviction /private/eviction.json +php qualification/cache.php checkpoint fresh-outage /private/outage.json +# Interrupt only your isolated test runtime, restore it, and verify readiness. +# The original observation cutoff continues while it is unavailable. +php qualification/cache.php resume fresh-outage /private/outage.json +``` + +Do not run the three profiles together or reuse a checkpoint. The driver does +not change the CLI's cache settings, stop services, restart workers or provision +credentials. Use the normal supervisor for connection recovery and record actual +worker replacements. Require an observed SDK transport error before claiming +connection-error recovery; a network configuration change alone is insufficient. + +After normal CLI shutdown, read the actual `worker.stopped` diagnostic's +`sticky_cache` counters from the logger or framework event: `hit`, `miss`, +`eviction`, and `forced_cold_replay`. Missing counters fail the observation. +Require a real eviction for expiry/capacity tests and a forced cold replay for +the capacity test. A complete runtime history can yield zero forced cold replays +after TTL expiry. Audit original task attempts/repairs and deregistration through +the isolated runtime, then delete temporary checkpoints and test resources. + ## Qualification before recommendation Freeze the normal dependency lock, RoadRunner binary hash, runtime image digest, diff --git a/examples/laravel-http/qualification/cache.php b/examples/laravel-http/qualification/cache.php new file mode 100644 index 00000000..ff1432c7 --- /dev/null +++ b/examples/laravel-http/qualification/cache.php @@ -0,0 +1,114 @@ +describeSelectedRun(); + $page = $client->workflowHistory($handle->workflowId, $handle->selectedRunId, pageSize: 100); + $events = $page['events'] ?? null; + if (($page['next_page_token'] ?? null) !== null || !is_array($events) || $events === [] || !array_is_list($events)) { + throw new RuntimeException('The complete bounded canonical history is unavailable.'); + } + $deadlines = []; + foreach (['run_deadline_at', 'execution_deadline_at'] as $name) { + $deadlines[$name] = $execution->raw[$name] ?? null; + if (!is_string($deadlines[$name]) || $deadlines[$name] === '') throw new RuntimeException('Missing original deadline.'); + } + + return ['status' => strtolower((string) $execution->status), 'events' => $events, + 'counts' => array_count_values(array_column($events, 'event_type')), 'deadlines' => $deadlines]; +} + +function remaining(float $cutoff): float +{ + $seconds = $cutoff - microtime(true); + if ($seconds <= 0) throw new RuntimeException('The original 90-second observation expired.'); + + return $seconds; +} + +if ($mode === 'resume') { + $checkpoint = json_decode(file_get_contents($checkpointPath), true, 512, JSON_THROW_ON_ERROR); + if ($checkpoint['origin'] !== $origin || $checkpoint['namespace'] !== $namespace || $checkpoint['prefix'] !== $prefix + || $checkpoint['mode'] !== 'checkpoint') throw new RuntimeException('Checkpoint authority or identity differs.'); + if (!is_array($checkpoint['originals'] ?? null) || count($checkpoint['originals']) !== 1 + || !is_array($checkpoint['originals'][0]['before'] ?? null)) { + throw new RuntimeException('The original waiting checkpoint was not completed.'); + } + $cutoff = $checkpoint['cutoff']; + remaining($cutoff); +} else { + if (file_exists($checkpointPath)) throw new RuntimeException('Use a fresh checkpoint; do not overwrite an original.'); + $cutoff = microtime(true) + 90; + $checkpoint = ['origin' => $origin, 'namespace' => $namespace, 'prefix' => $prefix, 'mode' => $mode, + 'cutoff' => $cutoff, 'originals' => []]; + foreach ($mode === 'eviction' ? ['first', 'neighbor'] : ['original'] as $suffix) { + $handle = $client->startWorkflow('example.http', $prefix.'-'.$suffix, + getenv('DURABLE_WORKFLOW_TASK_QUEUE') ?: 'service-http-example', ['alpha', 'signal'], + executionTimeoutSeconds: 900, runTimeoutSeconds: 600); + $checkpoint['originals'][] = ['workflow_id' => $handle->workflowId, 'run_id' => $handle->selectedRunId, 'before' => null]; + if (file_put_contents($checkpointPath, json_encode($checkpoint, JSON_THROW_ON_ERROR)) === false) { + throw new RuntimeException('Cannot retain the admitted original identity.'); + } + do { + remaining($cutoff); + $before = snapshot($client, $handle); + if ($before['status'] === 'waiting' && ($before['counts']['ActivityCompleted'] ?? 0) === 1) break; + usleep(200000); + } while (true); + if (isset($before['counts']['WorkflowCompleted'])) throw new RuntimeException('Premature completion.'); + $checkpoint['originals'][array_key_last($checkpoint['originals'])]['before'] = $before; + if (file_put_contents($checkpointPath, json_encode($checkpoint, JSON_THROW_ON_ERROR)) === false) { + throw new RuntimeException('Cannot write the private checkpoint.'); + } + } + if ($mode === 'checkpoint') { + echo json_encode(['phase' => 'waiting', 'originals' => array_map( + static fn (array $original): array => array_intersect_key($original, array_flip(['workflow_id', 'run_id'])), + $checkpoint['originals'], + ), 'observation_cutoff' => $cutoff], JSON_THROW_ON_ERROR), PHP_EOL; + exit; + } + if ($mode === 'expiry') sleep(3); +} + +$results = []; +foreach ($checkpoint['originals'] as $original) { + remaining($cutoff); + $handle = $client->workflowHandle($original['workflow_id'], $original['run_id']); + $handle->signalSelectedRun('release'); + do { + remaining($cutoff); + $after = snapshot($client, $handle); + if ($after['status'] === 'completed') break; + usleep(200000); + } while (true); + $before = $original['before']; + if ($before['deadlines'] !== $after['deadlines'] || array_slice($after['events'], 0, count($before['events'])) !== $before['events']) { + throw new RuntimeException('Original history prefix or deadlines changed.'); + } + foreach (['ActivityScheduled', 'ActivityCompleted', 'WorkflowCompleted'] as $event) { + if (($after['counts'][$event] ?? 0) !== 1) throw new RuntimeException('Missing or duplicated '.$event); + } + if ($handle->resultOfSelectedRun(remaining($cutoff)) !== ['user' => 'alpha', 'echo' => 'hello alpha', + 'number' => 9007199254740993, 'nullable' => null, 'text' => '雪']) throw new RuntimeException('Typed output changed.'); + $results[] = ['workflow_id' => $handle->workflowId, 'run_id' => $handle->selectedRunId, + 'events' => $after['counts'], 'prefix_preserved' => true, 'deadlines' => $after['deadlines'], 'typed_output_exact' => true]; +} +echo json_encode(['mode' => $mode, 'originals' => $results], JSON_THROW_ON_ERROR), PHP_EOL; From 1857a89ecc688ccf2aa0e68c8971eb7345fcc131 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 21:22:01 +0000 Subject: [PATCH 11/28] Add bounded k6 service HTTP comparison workload --- examples/laravel-http/README.md | 25 ++++ examples/laravel-http/qualification/load.js | 140 ++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 examples/laravel-http/qualification/load.js diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 8689ad97..4d83166e 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -172,6 +172,31 @@ eviction, HTTP reload/recycling, CLI SIGKILL/cold replacement and runtime connection loss first. Inspect canonical histories for duplicate effects and unchanged identities/deadlines. +`qualification/load.js` uses the open-source k6 constant-arrival-rate executor: +six authenticated context reads and one new original workflow per second. Run +the tool separately from the fixed application allocation. Each original has +the existing 35-second observer; the outer run allows 36 seconds to drain after +the last offer. Every expected admission, terminal result and context read is +counted. Dropped offers, HTTP/check failures, incomplete originals, OOM or +restarts invalidate a successful comparison. + +Set `EXAMPLE_ORIGIN`, `DURABLE_WORKFLOW_NAMESPACE`, a fresh `EXAMPLE_RUN_PREFIX` +and `EXAMPLE_SUMMARY_PATH` in the tool container. Freeze its image digest and +limits as well as application/backend roles. `EXAMPLE_DURATION_SECONDS` selects +5–300 seconds; the planned warmup/window/soak are 30/120/300 seconds. The normal +window uses one-activity workflows. `EXAMPLE_MIXED=true` cycles two short, +one timer, one queried/signalled and one queried/cancelled original. Wire checks +preserve the exact int64 digits; normal installed PHP readers and native task +records independently verify types, attempts, histories and repairs afterward. + +Use fresh projects/databases for three pairs with order FPM/Octane, +Octane/FPM, FPM/Octane, then an FPM drift recheck and mixed soak. Separate warmup +from measured offers. Report context/start/status latency separately from the +poll-observed workflow latency; the latter includes the declared 250ms status +poll interval. Sample every app/backend/tool role through drain and verification. +Keep the small k6 summary and readable report; delete raw samples, histories, +databases, dependencies and temporary hosts after their immediate audit. + Then run three independent FPM/Octane pairs and recheck FPM. Keep CLI execution settings identical. Report offered/completed/failed/unfinished operations, submission/status latency separately from workflow latency, measured drain, diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js new file mode 100644 index 00000000..4e6214ab --- /dev/null +++ b/examples/laravel-http/qualification/load.js @@ -0,0 +1,140 @@ +import http from 'k6/http'; +import {check, sleep} from 'k6'; +import execution from 'k6/execution'; +import {Counter, Trend} from 'k6/metrics'; + +const origin = __ENV.EXAMPLE_ORIGIN || 'http://nginx:8080'; +const namespace = __ENV.DURABLE_WORKFLOW_NAMESPACE; +const prefix = __ENV.EXAMPLE_RUN_PREFIX; +const seconds = Number(__ENV.EXAMPLE_DURATION_SECONDS || 120); +const mixed = __ENV.EXAMPLE_MIXED === 'true'; +if (!namespace || !/^[a-zA-Z0-9_-]{1,30}$/.test(prefix || '') || !Number.isInteger(seconds) || seconds < 5 || seconds > 300) { + throw new Error('Select a fixed namespace, fresh prefix and a 5–300 second offered window.'); +} + +const offered = new Counter('workflow_offered'); +const admitted = new Counter('workflow_admitted'); +const completed = new Counter('workflow_completed'); +const cancelled = new Counter('workflow_cancelled'); +const terminal = new Counter('workflow_terminal'); +const contextCompleted = new Counter('context_completed'); +const errors = new Counter('run_errors'); +const observed = new Trend('workflow_observed_ms', true); +const timings = Object.fromEntries(['context', 'start', 'status', 'query', 'signal', 'cancel'] + .map(name => [name, new Trend(`${name}_http_ms`, true)])); +let requestNumber = 0; + +export const options = { + scenarios: { + context: {executor: 'constant-arrival-rate', rate: 6, timeUnit: '1s', duration: `${seconds}s`, + preAllocatedVUs: 2, maxVUs: 8, exec: 'context', gracefulStop: '36s'}, + workflows: {executor: 'constant-arrival-rate', rate: 1, timeUnit: '1s', duration: `${seconds}s`, + preAllocatedVUs: 4, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, + }, + thresholds: { + checks: [{threshold: 'rate==1', abortOnFail: true}], + run_errors: [{threshold: 'count==0', abortOnFail: true}], + http_req_failed: [{threshold: 'rate==0', abortOnFail: true}], + dropped_iterations: ['count==0'], + workflow_offered: [`count==${seconds}`], + workflow_admitted: [`count==${seconds}`], + workflow_terminal: [`count==${seconds}`], + context_completed: [`count==${seconds * 6}`], + }, + summaryTrendStats: ['count', 'avg', 'min', 'med', 'max', 'p(95)', 'p(99)'], +}; + +function requireCheck(condition, message) { + if (!check(condition, {[message]: value => value === true})) throw new Error(message); +} + +function header(response, name) { + const key = Object.keys(response.headers).find(key => key.toLowerCase() === name.toLowerCase()); + return key === undefined ? '' : response.headers[key]; +} + +function request(user, path, name, method = 'GET', body, expected = 200, remainingMs = 10000) { + if (remainingMs <= 0) throw new Error('Original 35-second observation expired.'); + const requestId = `${prefix}-${__VU}-${__ITER}-${requestNumber++}`; + const response = http.request(method, `${origin}/api/${path}`, body === undefined ? null : JSON.stringify(body), { + headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', + 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Namespace': namespace}, + timeout: `${Math.max(1, Math.floor(Math.min(10000, remainingMs)))}ms`, + tags: {name}, redirects: 0, + }); + timings[name].add(response.timings.duration); + requireCheck(response.status === expected, `${name} returned expected status`); + requireCheck(header(response, 'X-Request-Id') === requestId, 'request context is isolated'); + requireCheck(header(response, 'X-Example-User') === user, 'user context is isolated'); + requireCheck(header(response, 'X-Example-Namespace') === namespace, 'namespace context is fixed'); + return response; +} + +export function context() { + errors.add(0); + try { + const user = execution.scenario.iterationInTest % 2 === 0 ? 'alpha' : 'beta'; + const response = request(user, 'context', 'context'); + const value = response.json(); + requireCheck(value.user === user && value.namespace === namespace, 'context values match'); + requireCheck(value.client_resolved_at_boot === false, 'fixed lazy profile'); + requireCheck(header(response, 'X-Optional-Context') === '', 'optional context does not leak'); + contextCompleted.add(1); + } catch (error) { + errors.add(1); + console.error(JSON.stringify({phase: 'context_failed', message: String(error)})); + } +} + +function waitStatus(user, id, expected, cutoff) { + while (Date.now() < cutoff) { + const response = request(user, `workflows/${id}`, 'status', 'GET', undefined, 200, cutoff - Date.now()); + requireCheck(Date.now() <= cutoff, 'original observer has not expired'); + if ((response.json().status || '').toLowerCase() === expected) return response; + sleep(Math.min(0.25, Math.max(0, cutoff - Date.now()) / 1000)); + } + throw new Error('Original 35-second observation expired.'); +} + +export function workflow() { + errors.add(0); + offered.add(1); + const index = execution.scenario.iterationInTest; + const user = index % 2 === 0 ? 'alpha' : 'beta'; + const kind = mixed ? ['short', 'short', 'timer', 'signal', 'cancel'][index % 5] : 'short'; + const id = `${prefix}-${kind}-${index}`; + const started = Date.now(); + const cutoff = started + 35000; + try { + const acceptance = request(user, 'workflows', 'start', 'POST', {id, kind}, 201, cutoff - Date.now()).json(); + admitted.add(1); + console.log(JSON.stringify({phase: 'admitted', user, kind, id, workflow_id: acceptance.workflow_id, run_id: acceptance.run_id, started})); + requireCheck(acceptance.workflow_id === `${user}-${id}` && typeof acceptance.run_id === 'string', 'accepted identity matches'); + if (kind === 'signal' || kind === 'cancel') { + waitStatus(user, id, 'waiting', cutoff); + const phase = request(user, `workflows/${id}/query`, 'query', 'POST', undefined, 200, cutoff - Date.now()).json(); + requireCheck(phase.phase === 'waiting', 'query observes committed activity'); + request(user, `workflows/${id}/${kind === 'signal' ? 'signal' : 'cancel'}`, + kind === 'signal' ? 'signal' : 'cancel', 'POST', undefined, 204, cutoff - Date.now()); + } + const response = waitStatus(user, id, kind === 'cancel' ? 'cancelled' : 'completed', cutoff); + if (kind === 'cancel') { + cancelled.add(1); + } else { + const output = response.json().output; + requireCheck(output.user === user && output.echo === `hello ${user}` && output.nullable === null && output.text === '雪', 'typed output matches'); + requireCheck(/"number":9007199254740993(?:,|})/.test(response.body), 'int64 wire bytes are exact'); + completed.add(1); + } + observed.add(Date.now() - started, {kind}); + terminal.add(1); + console.log(JSON.stringify({phase: 'terminal', user, kind, id, workflow_id: acceptance.workflow_id, run_id: acceptance.run_id, elapsed_ms: Date.now() - started})); + } catch (error) { + errors.add(1); + console.error(JSON.stringify({phase: 'workflow_failed', user, kind, id, message: String(error)})); + } +} + +export function handleSummary(data) { + return {stdout: JSON.stringify(data), [__ENV.EXAMPLE_SUMMARY_PATH || '/proof/summary.json']: JSON.stringify(data)}; +} From aa21cd5c7c0bd2b0efe08094cbe1af59c6d70b42 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 21:36:01 +0000 Subject: [PATCH 12/28] Audit original load results and count terminal failures explicitly --- examples/laravel-http/README.md | 4 ++ .../laravel-http/qualification/audit-load.php | 69 +++++++++++++++++++ examples/laravel-http/qualification/load.js | 10 ++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 examples/laravel-http/qualification/audit-load.php diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 4d83166e..4586b11b 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -188,6 +188,10 @@ window uses one-activity workflows. `EXAMPLE_MIXED=true` cycles two short, one timer, one queried/signalled and one queried/cancelled original. Wire checks preserve the exact int64 digits; normal installed PHP readers and native task records independently verify types, attempts, histories and repairs afterward. +Run `php qualification/audit-load.php` with those same original environment +values and only the client credential. It reads each selected original through +the installed SDK, checks canonical activity/terminal counts and exact PHP +types, and reads actual timer due/fired timestamps without inventing a clock. Use fresh projects/databases for three pairs with order FPM/Octane, Octane/FPM, FPM/Octane, then an FPM drift recheck and mixed soak. Separate warmup diff --git a/examples/laravel-http/qualification/audit-load.php b/examples/laravel-http/qualification/audit-load.php new file mode 100644 index 00000000..67f3f0d9 --- /dev/null +++ b/examples/laravel-http/qualification/audit-load.php @@ -0,0 +1,69 @@ + 300) { + throw new InvalidArgumentException('Use the original load prefix, duration and mixed setting.'); +} +$client = new DurableWorkflow\Client(getenv('DURABLE_WORKFLOW_RUNTIME_URL'), + namespace: getenv('DURABLE_WORKFLOW_NAMESPACE'), controlToken: getenv('DURABLE_WORKFLOW_CLIENT_TOKEN')); +$completed = $cancelled = $eventsRead = 0; +$timers = []; +function timestamp(string $value): float +{ + if (preg_match('/(?:Z|[+-][0-9]{2}:[0-9]{2})$/', $value) !== 1) throw new RuntimeException('Persisted timestamp lacks an explicit zone.'); + + return (float) (new DateTimeImmutable($value))->format('U.u'); +} +for ($index = 0; $index < $seconds; ++$index) { + $user = $index % 2 === 0 ? 'alpha' : 'beta'; + $kind = $mixed ? ['short', 'short', 'timer', 'signal', 'cancel'][$index % 5] : 'short'; + $id = $user.'-'.$prefix.'-'.$kind.'-'.$index; + $execution = $client->describeWorkflow($id); + if (!is_string($execution->runId) || $execution->runId === '') throw new RuntimeException('Missing original '.$id); + $handle = $client->workflowHandle($id, $execution->runId); + $history = $client->workflowHistory($id, $execution->runId, pageSize: 100); + if (($history['next_page_token'] ?? null) !== null || !is_array($history['events'] ?? null)) { + throw new RuntimeException('Missing complete bounded original history: '.$id); + } + $events = $history['events']; + $counts = array_count_values(array_column($events, 'event_type')); + $eventsRead += count($events); + foreach (['ActivityScheduled', 'ActivityStarted', 'ActivityCompleted'] as $name) { + if (($counts[$name] ?? 0) !== 1) throw new RuntimeException('Missing/duplicate '.$name.': '.$id); + } + if (isset($counts['RepairRequested']) || isset($counts['ActivityFailed'])) throw new RuntimeException('Original contains repair/failure: '.$id); + foreach (['run_deadline_at', 'execution_deadline_at'] as $name) { + if (!is_string($execution->raw[$name] ?? null) || $execution->raw[$name] === '') throw new RuntimeException('Missing deadline: '.$id); + } + if ($kind === 'cancel') { + if (strtolower((string) $execution->status) !== 'cancelled' || ($counts['WorkflowCancelled'] ?? 0) !== 1 + || isset($counts['WorkflowCompleted']) || isset($counts['TimerFired'])) throw new RuntimeException('Cancellation differs: '.$id); + ++$cancelled; + continue; + } + if (strtolower((string) $execution->status) !== 'completed' || ($counts['WorkflowCompleted'] ?? 0) !== 1) { + throw new RuntimeException('Original is not completed exactly once: '.$id); + } + if ($handle->resultOfSelectedRun(5) !== ['user' => $user, 'echo' => 'hello '.$user, + 'number' => 9007199254740993, 'nullable' => null, 'text' => '雪']) throw new RuntimeException('Typed result differs: '.$id); + if ($kind === 'timer') { + if (($counts['TimerScheduled'] ?? 0) !== 1 || ($counts['TimerFired'] ?? 0) !== 1) throw new RuntimeException('Timer differs: '.$id); + $fired = array_values(array_filter($events, static fn (array $event): bool => $event['event_type'] === 'TimerFired'))[0]; + $fireAt = $fired['payload']['fire_at'] ?? null; + $firedAt = $fired['payload']['fired_at'] ?? null; + if (!is_string($fireAt) || !is_string($firedAt)) throw new RuntimeException('Timer clock evidence missing: '.$id); + $lateMs = (timestamp($firedAt) - timestamp($fireAt)) * 1000; + if ($lateMs < 0) throw new RuntimeException('Timer fired early: '.$id); + $timers[] = ['workflow_id' => $id, 'run_id' => $execution->runId, 'due_to_fire_ms' => $lateMs]; + } + ++$completed; +} +echo json_encode(['prefix' => $prefix, 'originals' => $seconds, 'completed' => $completed, + 'cancelled' => $cancelled, 'events_read' => $eventsRead, 'typed_outputs_exact' => true, 'timers' => $timers], JSON_THROW_ON_ERROR), PHP_EOL; diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index 4e6214ab..a05b31bd 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -16,6 +16,7 @@ const offered = new Counter('workflow_offered'); const admitted = new Counter('workflow_admitted'); const completed = new Counter('workflow_completed'); const cancelled = new Counter('workflow_cancelled'); +const failed = new Counter('workflow_failed'); const terminal = new Counter('workflow_terminal'); const contextCompleted = new Counter('context_completed'); const errors = new Counter('run_errors'); @@ -90,7 +91,11 @@ function waitStatus(user, id, expected, cutoff) { while (Date.now() < cutoff) { const response = request(user, `workflows/${id}`, 'status', 'GET', undefined, 200, cutoff - Date.now()); requireCheck(Date.now() <= cutoff, 'original observer has not expired'); - if ((response.json().status || '').toLowerCase() === expected) return response; + const status = (response.json().status || '').toLowerCase(); + if (status === expected) return response; + if (['completed', 'failed', 'cancelled', 'terminated', 'timed_out'].includes(status)) { + throw new Error(`Original reached unexpected terminal status ${status}.`); + } sleep(Math.min(0.25, Math.max(0, cutoff - Date.now()) / 1000)); } throw new Error('Original 35-second observation expired.'); @@ -130,11 +135,14 @@ export function workflow() { terminal.add(1); console.log(JSON.stringify({phase: 'terminal', user, kind, id, workflow_id: acceptance.workflow_id, run_id: acceptance.run_id, elapsed_ms: Date.now() - started})); } catch (error) { + failed.add(1); errors.add(1); console.error(JSON.stringify({phase: 'workflow_failed', user, kind, id, message: String(error)})); } } export function handleSummary(data) { + data.example = {prefix, namespace, duration_seconds: seconds, context_rate: 6, workflow_rate: 1, + mixed, original_observer_seconds: 35, outer_drain_seconds: 36}; return {stdout: JSON.stringify(data), [__ENV.EXAMPLE_SUMMARY_PATH || '/proof/summary.json']: JSON.stringify(data)}; } From 12ed289c7a07ece3993cba3bc01f93f3306aca1b Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 21:48:53 +0000 Subject: [PATCH 13/28] Record HTTP worker identities during the Laravel load profile --- examples/laravel-http/qualification/load.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index a05b31bd..8314be8f 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -24,6 +24,7 @@ const observed = new Trend('workflow_observed_ms', true); const timings = Object.fromEntries(['context', 'start', 'status', 'query', 'signal', 'cancel'] .map(name => [name, new Trend(`${name}_http_ms`, true)])); let requestNumber = 0; +const seenPids = new Set(); export const options = { scenarios: { @@ -68,6 +69,12 @@ function request(user, path, name, method = 'GET', body, expected = 200, remaini requireCheck(header(response, 'X-Request-Id') === requestId, 'request context is isolated'); requireCheck(header(response, 'X-Example-User') === user, 'user context is isolated'); requireCheck(header(response, 'X-Example-Namespace') === namespace, 'namespace context is fixed'); + const pid = header(response, 'X-Worker-Pid'); + requireCheck(/^\d+$/.test(pid), 'HTTP worker identity is present'); + if (!seenPids.has(pid)) { + seenPids.add(pid); + console.log(JSON.stringify({phase: 'http_worker_observed', pid})); + } return response; } From e907a97c989362c17013ad207b5798da981881a2 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Wed, 14 Oct 2026 22:25:01 +0000 Subject: [PATCH 14/28] Bound load offers before admission and preallocate the declared client pool --- examples/laravel-http/README.md | 24 ++++++++++---------- examples/laravel-http/qualification/load.js | 25 +++++++++++++++++---- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 4586b11b..ca25c595 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -174,7 +174,11 @@ unchanged identities/deadlines. `qualification/load.js` uses the open-source k6 constant-arrival-rate executor: six authenticated context reads and one new original workflow per second. Run -the tool separately from the fixed application allocation. Each original has +the tool separately from the fixed application allocation. Preallocate all eight +context and sixteen workflow virtual users; a missed offer still fails the run. +The request sequence is finite. A generator tick at the window boundary is +counted separately and cannot admit an extra workflow or context read. A late +planned offer fails before HTTP admission. Each original has the existing 35-second observer; the outer run allows 36 seconds to drain after the last offer. Every expected admission, terminal result and context read is counted. Dropped offers, HTTP/check failures, incomplete originals, OOM or @@ -195,20 +199,16 @@ types, and reads actual timer due/fired timestamps without inventing a clock. Use fresh projects/databases for three pairs with order FPM/Octane, Octane/FPM, FPM/Octane, then an FPM drift recheck and mixed soak. Separate warmup -from measured offers. Report context/start/status latency separately from the +from measured offers and keep CLI settings identical. Report context/start/status latency separately from the poll-observed workflow latency; the latter includes the declared 250ms status poll interval. Sample every app/backend/tool role through drain and verification. +Include offered/completed/failed/unfinished operations, measured drain, timer +due-to-resume latency, per-completion CPU, aggregate steady/peak memory, history +bytes/requests and cache diagnostics. Reject OOM, swapping or growing backlog as +a successful measurement. Exercise recycling/reload during the mixed soak. Keep the small k6 summary and readable report; delete raw samples, histories, -databases, dependencies and temporary hosts after their immediate audit. - -Then run three independent FPM/Octane pairs and recheck FPM. Keep CLI execution -settings identical. Report offered/completed/failed/unfinished operations, -submission/status latency separately from workflow latency, measured drain, -timer due-to-resume latency, per-completion CPU, aggregate steady/peak memory, -history bytes/requests and existing cache diagnostics. Reject OOM, swapping or -growing backlog as a successful measurement. Follow with a bounded mixed-request -soak and explicit recycle/reload observations. Gains and tradeoffs belong in the -owning issue, including a result with no consistent benefit. +databases, dependencies and temporary hosts after their immediate audit. Report +gains and tradeoffs in the owning issue, including no consistent benefit. Stop the Compose project and delete only this example's state when finished. This application does not provision or retire your runtime namespace or its diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index 8314be8f..f7c1b474 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -20,6 +20,7 @@ const failed = new Counter('workflow_failed'); const terminal = new Counter('workflow_terminal'); const contextCompleted = new Counter('context_completed'); const errors = new Counter('run_errors'); +const boundaryIterations = new Counter('generator_boundary_iterations'); const observed = new Trend('workflow_observed_ms', true); const timings = Object.fromEntries(['context', 'start', 'status', 'query', 'signal', 'cancel'] .map(name => [name, new Trend(`${name}_http_ms`, true)])); @@ -29,9 +30,9 @@ const seenPids = new Set(); export const options = { scenarios: { context: {executor: 'constant-arrival-rate', rate: 6, timeUnit: '1s', duration: `${seconds}s`, - preAllocatedVUs: 2, maxVUs: 8, exec: 'context', gracefulStop: '36s'}, + preAllocatedVUs: 8, maxVUs: 8, exec: 'context', gracefulStop: '36s'}, workflows: {executor: 'constant-arrival-rate', rate: 1, timeUnit: '1s', duration: `${seconds}s`, - preAllocatedVUs: 4, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, + preAllocatedVUs: 16, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, }, thresholds: { checks: [{threshold: 'rate==1', abortOnFail: true}], @@ -80,8 +81,15 @@ function request(user, path, name, method = 'GET', body, expected = 200, remaini export function context() { errors.add(0); + boundaryIterations.add(0); + const index = execution.scenario.iterationInTest; + if (index >= seconds * 6) { + boundaryIterations.add(1); + return; + } try { - const user = execution.scenario.iterationInTest % 2 === 0 ? 'alpha' : 'beta'; + requireCheck(Date.now() < execution.scenario.startTime + seconds * 1000, 'context offer is inside the selected window'); + const user = index % 2 === 0 ? 'alpha' : 'beta'; const response = request(user, 'context', 'context'); const value = response.json(); requireCheck(value.user === user && value.namespace === namespace, 'context values match'); @@ -110,14 +118,23 @@ function waitStatus(user, id, expected, cutoff) { export function workflow() { errors.add(0); - offered.add(1); + failed.add(0); + completed.add(0); + cancelled.add(0); + boundaryIterations.add(0); const index = execution.scenario.iterationInTest; + if (index >= seconds) { + boundaryIterations.add(1); + return; + } + offered.add(1); const user = index % 2 === 0 ? 'alpha' : 'beta'; const kind = mixed ? ['short', 'short', 'timer', 'signal', 'cancel'][index % 5] : 'short'; const id = `${prefix}-${kind}-${index}`; const started = Date.now(); const cutoff = started + 35000; try { + requireCheck(started < execution.scenario.startTime + seconds * 1000, 'workflow offer is inside the selected window'); const acceptance = request(user, 'workflows', 'start', 'POST', {id, kind}, 201, cutoff - Date.now()).json(); admitted.add(1); console.log(JSON.stringify({phase: 'admitted', user, kind, id, workflow_id: acceptance.workflow_id, run_id: acceptance.run_id, started})); From 1d90256da227c2c1315fd509d4565727ba220eac Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Thu, 15 Oct 2026 23:20:41 +0000 Subject: [PATCH 15/28] chore: clarify Laravel HTTP example configuration --- examples/laravel-http/app/Models/User.php | 1 - examples/laravel-http/config/logging.php | 8 +++++--- examples/laravel-http/runtime/Dockerfile | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/laravel-http/app/Models/User.php b/examples/laravel-http/app/Models/User.php index 68f3a669..b969eb14 100644 --- a/examples/laravel-http/app/Models/User.php +++ b/examples/laravel-http/app/Models/User.php @@ -2,7 +2,6 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; diff --git a/examples/laravel-http/config/logging.php b/examples/laravel-http/config/logging.php index b09cb25d..c959ecae 100644 --- a/examples/laravel-http/config/logging.php +++ b/examples/laravel-http/config/logging.php @@ -5,6 +5,8 @@ use Monolog\Handler\SyslogUdpHandler; use Monolog\Processor\PsrLogMessageProcessor; +$logFile = storage_path('logs/laravel.log'); + return [ /* @@ -60,14 +62,14 @@ 'single' => [ 'driver' => 'single', - 'path' => storage_path('logs/laravel.log'), + 'path' => $logFile, 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'daily' => [ 'driver' => 'daily', - 'path' => storage_path('logs/laravel.log'), + 'path' => $logFile, 'level' => env('LOG_LEVEL', 'debug'), 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, @@ -124,7 +126,7 @@ ], 'emergency' => [ - 'path' => storage_path('logs/laravel.log'), + 'path' => $logFile, ], ], diff --git a/examples/laravel-http/runtime/Dockerfile b/examples/laravel-http/runtime/Dockerfile index 4a4bbde0..314252f8 100644 --- a/examples/laravel-http/runtime/Dockerfile +++ b/examples/laravel-http/runtime/Dockerfile @@ -6,9 +6,9 @@ RUN docker-php-ext-install -j1 bcmath opcache pcntl \ && groupmod --gid 1000 www-data \ && usermod --uid 1000 --gid 1000 www-data \ && mkdir -p /app \ - && chown 1000:1000 /app -RUN docker-php-ext-install -j1 sockets -RUN apt-get update \ + && chown 1000:1000 /app \ + && docker-php-ext-install -j1 sockets \ + && apt-get update \ && apt-get install -y --no-install-recommends unzip \ && rm -rf /var/lib/apt/lists/* COPY --from=composer /usr/bin/composer /usr/bin/composer From 08a2a33949ed100bd3703a19f3a12cbdfb7ba4c0 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 15:40:51 +0000 Subject: [PATCH 16/28] Keep mixed query requests within the original observation budget --- examples/laravel-http/README.md | 4 ++++ examples/laravel-http/qualification/load.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index ca25c595..c5ae0dd0 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -184,6 +184,10 @@ the last offer. Every expected admission, terminal result and context read is counted. Dropped offers, HTTP/check failures, incomplete originals, OOM or restarts invalidate a successful comparison. +A query request may use up to 30 seconds, capped by the original workflow's +remaining observation budget. Other requests retain their ten-second transport +cap. No request resets or extends the original 35-second observer. + Set `EXAMPLE_ORIGIN`, `DURABLE_WORKFLOW_NAMESPACE`, a fresh `EXAMPLE_RUN_PREFIX` and `EXAMPLE_SUMMARY_PATH` in the tool container. Freeze its image digest and limits as well as application/backend roles. `EXAMPLE_DURATION_SECONDS` selects diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index f7c1b474..1efa2092 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -58,11 +58,12 @@ function header(response, name) { function request(user, path, name, method = 'GET', body, expected = 200, remainingMs = 10000) { if (remainingMs <= 0) throw new Error('Original 35-second observation expired.'); + const transportLimitMs = name === 'query' ? 30000 : 10000; const requestId = `${prefix}-${__VU}-${__ITER}-${requestNumber++}`; const response = http.request(method, `${origin}/api/${path}`, body === undefined ? null : JSON.stringify(body), { headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Namespace': namespace}, - timeout: `${Math.max(1, Math.floor(Math.min(10000, remainingMs)))}ms`, + timeout: `${Math.max(1, Math.floor(Math.min(transportLimitMs, remainingMs)))}ms`, tags: {name}, redirects: 0, }); timings[name].add(response.timings.duration); From 989acc65de90e9ce4617315485be711cc4d825f9 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 17:03:14 +0000 Subject: [PATCH 17/28] Expose transport timing for Laravel example diagnostics --- examples/laravel-http/README.md | 6 ++++++ examples/laravel-http/qualification/load.js | 4 ++++ examples/laravel-http/runtime/nginx-fpm.conf | 6 +++++- examples/laravel-http/runtime/nginx-octane.conf | 6 +++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index c5ae0dd0..3afe79e1 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -206,6 +206,12 @@ Octane/FPM, FPM/Octane, then an FPM drift recheck and mixed soak. Separate warmu from measured offers and keep CLI settings identical. Report context/start/status latency separately from the poll-observed workflow latency; the latter includes the declared 250ms status poll interval. Sample every app/backend/tool role through drain and verification. +Both proxies emit JSON request IDs and connection/header/response timings without +request bodies or authorization headers. A failed driver request records the +same ID, status, error code and k6 transport timings. Correlate these records with +component CPU, throttling, memory and queue state before attributing a timeout to +the application, backend or load generator. These diagnostics do not relax the +offered schedule or original deadlines. Include offered/completed/failed/unfinished operations, measured drain, timer due-to-resume latency, per-completion CPU, aggregate steady/peak memory, history bytes/requests and cache diagnostics. Reject OOM, swapping or growing backlog as diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index 1efa2092..19a0df74 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -67,6 +67,10 @@ function request(user, path, name, method = 'GET', body, expected = 200, remaini tags: {name}, redirects: 0, }); timings[name].add(response.timings.duration); + if (response.status !== expected) { + console.error(JSON.stringify({phase: 'request_failed', name, request_id: requestId, + status: response.status, error_code: response.error_code, timings: response.timings})); + } requireCheck(response.status === expected, `${name} returned expected status`); requireCheck(header(response, 'X-Request-Id') === requestId, 'request context is isolated'); requireCheck(header(response, 'X-Example-User') === user, 'user context is isolated'); diff --git a/examples/laravel-http/runtime/nginx-fpm.conf b/examples/laravel-http/runtime/nginx-fpm.conf index 3610f2b5..20ad5ac2 100644 --- a/examples/laravel-http/runtime/nginx-fpm.conf +++ b/examples/laravel-http/runtime/nginx-fpm.conf @@ -3,7 +3,11 @@ error_log /dev/stderr warn; worker_processes 1; events { worker_connections 256; } http { - access_log /dev/stdout; + log_format request_timing escape=json + '{"request_id":"$http_x_request_id","status":$status,"finished_at":$msec,' + '"request_seconds":$request_time,"upstream_connect_seconds":"$upstream_connect_time",' + '"upstream_header_seconds":"$upstream_header_time","upstream_response_seconds":"$upstream_response_time"}'; + access_log /dev/stdout request_timing; client_body_temp_path /tmp/client-body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; diff --git a/examples/laravel-http/runtime/nginx-octane.conf b/examples/laravel-http/runtime/nginx-octane.conf index f3761528..cc41abeb 100644 --- a/examples/laravel-http/runtime/nginx-octane.conf +++ b/examples/laravel-http/runtime/nginx-octane.conf @@ -3,7 +3,11 @@ error_log /dev/stderr warn; worker_processes 1; events { worker_connections 256; } http { - access_log /dev/stdout; + log_format request_timing escape=json + '{"request_id":"$http_x_request_id","status":$status,"finished_at":$msec,' + '"request_seconds":$request_time,"upstream_connect_seconds":"$upstream_connect_time",' + '"upstream_header_seconds":"$upstream_header_time","upstream_response_seconds":"$upstream_response_time"}'; + access_log /dev/stdout request_timing; client_body_temp_path /tmp/client-body; proxy_temp_path /tmp/proxy; fastcgi_temp_path /tmp/fastcgi; From 55cd083a5cdd07618901036290b9429bcfc2e259 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 18:49:01 +0000 Subject: [PATCH 18/28] Use an HTTP-only RoadRunner profile in Laravel example --- examples/laravel-http/README.md | 4 ++++ examples/laravel-http/compose.yaml | 2 +- examples/laravel-http/runtime/roadrunner.yaml | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 examples/laravel-http/runtime/roadrunner.yaml diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 3afe79e1..cff213b0 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -55,6 +55,10 @@ and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, CLI 1 CPU/320 MiB, nginx 0.1 CPU/64 MiB. Provision and report the separate Server/database allocation. These limits are qualification settings, not a general capacity promise. +The checked-in `runtime/roadrunner.yaml` enables the HTTP pool and local control +RPC. The generated starter configuration also starts a jobs pool, which this +HTTP-only application does not use. Durable polling stays in the CLI worker. + ## Exercise the application The two `example-alpha` and `example-beta` bearer values are synthetic application diff --git a/examples/laravel-http/compose.yaml b/examples/laravel-http/compose.yaml index b084ae2b..e6dfa03a 100644 --- a/examples/laravel-http/compose.yaml +++ b/examples/laravel-http/compose.yaml @@ -47,7 +47,7 @@ services: EXAMPLE_PROCESS_ROLE: http init: true stop_signal: SIGTERM - command: php artisan octane:start --server=roadrunner --host=0.0.0.0 --port=8000 --workers=2 --max-requests=500 + command: php artisan octane:start --server=roadrunner --rr-config=/app/runtime/roadrunner.yaml --host=0.0.0.0 --port=8000 --workers=2 --max-requests=500 depends_on: prepare: condition: service_completed_successfully diff --git a/examples/laravel-http/runtime/roadrunner.yaml b/examples/laravel-http/runtime/roadrunner.yaml new file mode 100644 index 00000000..462dcf39 --- /dev/null +++ b/examples/laravel-http/runtime/roadrunner.yaml @@ -0,0 +1,23 @@ +version: '3' + +rpc: + listen: 'tcp://127.0.0.1:6001' + +server: + command: 'php vendor/bin/roadrunner-worker' + relay: pipes + +http: + address: '0.0.0.0:8000' + middleware: + - static + static: + dir: public + forbid: + - .php + - .htaccess + pool: + num_workers: 2 + max_jobs: 500 + supervisor: + max_worker_memory: 100 From 67b311af82587885ae9dbabb6d95c866c74aa2c2 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 18:50:50 +0000 Subject: [PATCH 19/28] Bind the Octane control RPC to loopback explicitly --- examples/laravel-http/compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/laravel-http/compose.yaml b/examples/laravel-http/compose.yaml index e6dfa03a..40c1f356 100644 --- a/examples/laravel-http/compose.yaml +++ b/examples/laravel-http/compose.yaml @@ -47,7 +47,7 @@ services: EXAMPLE_PROCESS_ROLE: http init: true stop_signal: SIGTERM - command: php artisan octane:start --server=roadrunner --rr-config=/app/runtime/roadrunner.yaml --host=0.0.0.0 --port=8000 --workers=2 --max-requests=500 + command: php artisan octane:start --server=roadrunner --rr-config=/app/runtime/roadrunner.yaml --host=0.0.0.0 --rpc-host=127.0.0.1 --port=8000 --workers=2 --max-requests=500 depends_on: prepare: condition: service_completed_successfully From f027697c83e753f708386d3487746c13a9a8c1e5 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 19:09:42 +0000 Subject: [PATCH 20/28] Qualify the Laravel HTTP example against current published packages --- examples/laravel-http/README.md | 2 ++ examples/laravel-http/composer.json | 2 +- examples/laravel-http/composer.lock | 14 +++++++------- examples/laravel-http/qualification/inputs.json | 13 +++++++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 examples/laravel-http/qualification/inputs.json diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index cff213b0..02f7b0c2 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -58,6 +58,8 @@ These limits are qualification settings, not a general capacity promise. The checked-in `runtime/roadrunner.yaml` enables the HTTP pool and local control RPC. The generated starter configuration also starts a jobs pool, which this HTTP-only application does not use. Durable polling stays in the CLI worker. +`qualification/inputs.json` records the pinned runtime and tool images for the +comparison; application dependencies remain in the normal Composer lock. ## Exercise the application diff --git a/examples/laravel-http/composer.json b/examples/laravel-http/composer.json index d7e8e398..ac6de8e4 100644 --- a/examples/laravel-http/composer.json +++ b/examples/laravel-http/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": "^8.3", - "durable-workflow/sdk": "2.4.1", + "durable-workflow/sdk": "2.4.2", "laravel/framework": "12.69.3", "laravel/octane": "2.21.0", "spiral/roadrunner-cli": "^2.6.0", diff --git a/examples/laravel-http/composer.lock b/examples/laravel-http/composer.lock index bf194365..c8ade2d7 100644 --- a/examples/laravel-http/composer.lock +++ b/examples/laravel-http/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": "b5da26409c37aa4e6425bcf712ead87b", + "content-hash": "b7fc96c5a47dec4499520543a32a26bf", "packages": [ { "name": "apache/avro", @@ -585,16 +585,16 @@ }, { "name": "durable-workflow/sdk", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/durable-workflow/sdk-php.git", - "reference": "520a53f2c4c7fe19358556375302e8e47e09e519" + "reference": "25d796ad2a55d0d114b58017c3ba23390b09e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/520a53f2c4c7fe19358556375302e8e47e09e519", - "reference": "520a53f2c4c7fe19358556375302e8e47e09e519", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/25d796ad2a55d0d114b58017c3ba23390b09e120", + "reference": "25d796ad2a55d0d114b58017c3ba23390b09e120", "shasum": "" }, "require": { @@ -635,7 +635,7 @@ "laravel": "^9.0|^10.0|^11.0|^12.0|^13.0", "symfony": "^6.4|^7.0|^8.0" }, - "product-train": "2.4.1", + "product-train": "2.4.2", "payload-codecs": [ "avro" ], @@ -701,7 +701,7 @@ "issues": "https://github.com/durable-workflow/sdk-php/issues", "source": "https://github.com/durable-workflow/sdk-php" }, - "time": "2026-10-14T19:53:59+00:00" + "time": "2026-10-15T03:25:43+00:00" }, { "name": "egulias/email-validator", diff --git a/examples/laravel-http/qualification/inputs.json b/examples/laravel-http/qualification/inputs.json new file mode 100644 index 00000000..9468e9db --- /dev/null +++ b/examples/laravel-http/qualification/inputs.json @@ -0,0 +1,13 @@ +{ + "platform": "linux/amd64", + "server": { + "version": "2.8.6", + "commit": "f2775db76a21f21f3b64e751a4e3222e0badf4f5", + "image": "durableworkflow/server@sha256:b3c53408dfadb0e029d66775cc1764af350d0bc3e200bda1ada219cba8fecea2", + "workflow_version": "2.5.22" + }, + "tools": { + "k6": "grafana/k6@sha256:98305727d160c065e1da695619c40ea4becbc3e116a3842da2d6027970e428c2", + "node": "node@sha256:0e5f906573693feaa1e21057ebdcfdb5bd5021f050b2dc7c9deceb629c7da2a8" + } +} From e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 20:01:34 +0000 Subject: [PATCH 21/28] Size example HTTP pools for concurrent query waits and graceful reloads --- examples/laravel-http/compose.yaml | 2 +- examples/laravel-http/runtime/fpm.conf | 4 +++- examples/laravel-http/runtime/roadrunner.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/laravel-http/compose.yaml b/examples/laravel-http/compose.yaml index 40c1f356..ed4453f7 100644 --- a/examples/laravel-http/compose.yaml +++ b/examples/laravel-http/compose.yaml @@ -47,7 +47,7 @@ services: EXAMPLE_PROCESS_ROLE: http init: true stop_signal: SIGTERM - command: php artisan octane:start --server=roadrunner --rr-config=/app/runtime/roadrunner.yaml --host=0.0.0.0 --rpc-host=127.0.0.1 --port=8000 --workers=2 --max-requests=500 + command: php artisan octane:start --server=roadrunner --rr-config=/app/runtime/roadrunner.yaml --host=0.0.0.0 --rpc-host=127.0.0.1 --port=8000 --workers=4 --max-requests=500 depends_on: prepare: condition: service_completed_successfully diff --git a/examples/laravel-http/runtime/fpm.conf b/examples/laravel-http/runtime/fpm.conf index 7a816e0a..76a1e28d 100644 --- a/examples/laravel-http/runtime/fpm.conf +++ b/examples/laravel-http/runtime/fpm.conf @@ -2,13 +2,15 @@ daemonize = no error_log = /proc/self/fd/2 pid = /tmp/service-example-fpm.pid +; Allow bounded in-flight SDK queries to finish during a graceful reload. +process_control_timeout = 35s [www] user = www-data group = www-data listen = 0.0.0.0:9000 pm = static -pm.max_children = 2 +pm.max_children = 4 pm.max_requests = 500 catch_workers_output = yes decorate_workers_output = no diff --git a/examples/laravel-http/runtime/roadrunner.yaml b/examples/laravel-http/runtime/roadrunner.yaml index 462dcf39..f1c16b92 100644 --- a/examples/laravel-http/runtime/roadrunner.yaml +++ b/examples/laravel-http/runtime/roadrunner.yaml @@ -17,7 +17,7 @@ http: - .php - .htaccess pool: - num_workers: 2 + num_workers: 4 max_jobs: 500 supervisor: max_worker_memory: 100 From db5d53ade34d2569a98f90bb39e5cb66d93cfda2 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Fri, 16 Oct 2026 21:32:48 +0000 Subject: [PATCH 22/28] Document interim HTTP hosting results and remaining backend gate --- README.md | 1 + docs/portal/frameworks/laravel.md | 16 ++ examples/laravel-http/README.md | 40 +++- .../laravel-http/qualification/inputs.json | 10 + examples/laravel-http/qualification/report.md | 194 ++++++++++++++++++ 5 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 examples/laravel-http/qualification/report.md diff --git a/README.md b/README.md index 5c7d10c9..a6f96502 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ for portable payloads and accepts any PSR-18 HTTP client. | Plain PHP client or worker | [First workflow](https://php.durable-workflow.com/getting-started/first-workflow/) | | Laravel v1 or v2 embedded | [Choose a Laravel ownership model](https://php.durable-workflow.com/frameworks/laravel/) | | Laravel service mode | [Laravel service-mode bridge](https://php.durable-workflow.com/frameworks/laravel/#service-mode) | +| Laravel HTTP hosting | [FPM and optional Octane example](examples/laravel-http/) | | Symfony service mode | [Symfony bundle](https://php.durable-workflow.com/frameworks/symfony/) | | Runtime authentication | [Authentication guide](https://php.durable-workflow.com/operate/authentication/) | diff --git a/docs/portal/frameworks/laravel.md b/docs/portal/frameworks/laravel.md index 006025e9..14bbe8c1 100644 --- a/docs/portal/frameworks/laravel.md +++ b/docs/portal/frameworks/laravel.md @@ -192,6 +192,8 @@ Rollback is also per runtime: stop new service starts and send new IDs back to embedded v2, while service workers finish or explicitly terminate the runs that the service namespace already owns. + + ## Build the service-mode form as Laravel services Both handler classes are resolved from Laravel's container. Constructor @@ -457,6 +459,20 @@ neither a running Server nor source inspection. Use the same fake assertions for a concurrent application start; concurrency is an implementation detail of the registered handler, so the class-shaped start contract does not change. For handler-level coverage, build the registered Laravel worker in the test container and inspect `WorkerTestHarness::runWorkflow()`: every declared leaf must appear in the same command sequence with one group/path identity. Replay mixed-order or partial history through the same method to assert declaration-order results without starting a Server. +## HTTP hosting and durable workers + +Use the [FPM and optional Octane example](https://github.com/durable-workflow/sdk-php/tree/main/examples/laravel-http) +for a complete Laravel HTTP application with a normal Composer lock, explicit +resource limits, request-context controls and a separately supervised Artisan +worker. The recipe records its runtime/tool images and qualification outcomes. + +HTTP workers submit and inspect workflows. Run `durable-workflow:worker` in its +own process; Server or Cloud owns durable state, timers and task leases. Keep +each process's endpoint, namespace and credential authority fixed, and pass +client and worker credentials only to their respective roles outside cached +configuration. The CLI worker's history cache retains history and still replays +workflow instructions. + ## Qualification boundary Clean compatibility jobs install this SDK in every supported Laravel/PHP cell diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 02f7b0c2..e90b766c 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -1,8 +1,12 @@ # Laravel service HTTP example -This candidate application compares nginx/PHP-FPM with optional Laravel Octane -and RoadRunner. Qualification is in progress on [PHP SDK #112](https://github.com/durable-workflow/sdk-php/issues/112). -No performance or production hosting recommendation is established yet. +This application runs with nginx/PHP-FPM or optional Laravel Octane and +RoadRunner. The [qualification report](qualification/report.md) records the +tested configuration, resource costs and limits. + +Qualification is incomplete. The mixed-load FPM runs failed, including a +confirmed backend command deadlock tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). +This recipe has no production-capacity or performance recommendation yet. The application uses the normally published SDK. Durable workflow and activity polling runs in the separate `durable-workflow:worker` CLI container. HTTP @@ -11,6 +15,13 @@ history, timers, task leases and recovery. ## Installation +Check out the runnable application: + +```bash +git clone --depth 1 https://github.com/durable-workflow/sdk-php.git +cd sdk-php/examples/laravel-http +``` + The exact PHP SDK, Laravel and Octane versions live in `composer.json` and the resolved dependencies in `composer.lock`. The image uses PHP 8.3.35. Preparation downloads RoadRunner 2025.1.15 through its standard installer, then runs @@ -30,10 +41,11 @@ DURABLE_WORKFLOW_WORKER_TOKEN= EXAMPLE_STATE_DIRECTORY= ``` -Create the state directory as your local UID, then run: +The checkout and state directory must be writable by UID/GID `1000:1000`, used +by these containers. Start the FPM profile with: ```bash -docker compose --env-file /private/example.env up --build -d +docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml up --build -d ``` The HTTP container receives only the client token. The CLI container receives @@ -43,18 +55,23 @@ Never rewrite a shared client using request headers or an authenticated user's token. Multiple runtime authorities need independently configured clients or separate application processes. -For the FPM baseline: +For the optional Octane profile: ```bash -docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml up --build -d +docker compose --env-file /private/example.env up --build -d ``` -Both modes use two HTTP workers, one durable CLI worker and nginx with the same +Both modes use four HTTP workers, one durable CLI worker and nginx with the same OPcache and cached configuration. The application allocation totals two CPUs and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, CLI 1 CPU/320 MiB, nginx 0.1 CPU/64 MiB. Provision and report the separate Server/database allocation. These limits are qualification settings, not a general capacity promise. +Synchronous query requests occupy HTTP workers while awaiting their result. +Size the HTTP pool for that waiting time as well as request CPU. The FPM +configuration gives in-flight requests a bounded 35-second process-control +grace period during reload. The durable CLI worker remains independent. + The checked-in `runtime/roadrunner.yaml` enables the HTTP pool and local control RPC. The generated starter configuration also starts a jobs pool, which this HTTP-only application does not use. Durable polling stays in the CLI worker. @@ -121,9 +138,12 @@ manually repair history. After the replacement completes, `after` requires the whole original prefix, unchanged deadline fields and exact typed result. Delete the temporary checkpoint when the control ends. -One actual [SIGKILL and cold replacement control](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6175531934) +One actual [SIGKILL and cold replacement control](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222923826) passes with the same original run, complete history prefix and unchanged -deadlines. It does not qualify cache expiry, eviction or runtime connection loss. +deadlines. Separate [cache and connection controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223066356) +cover expiry, capacity eviction and supervised recovery after API connection +loss. The latter replaces the CLI process three times, so it does not establish +recovery within one process. History-cache defaults remain 100 entries/300 seconds. The existing SDK validates capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and diff --git a/examples/laravel-http/qualification/inputs.json b/examples/laravel-http/qualification/inputs.json index 9468e9db..7997a755 100644 --- a/examples/laravel-http/qualification/inputs.json +++ b/examples/laravel-http/qualification/inputs.json @@ -6,6 +6,16 @@ "image": "durableworkflow/server@sha256:b3c53408dfadb0e029d66775cc1764af350d0bc3e200bda1ada219cba8fecea2", "workflow_version": "2.5.22" }, + "services": { + "mysql": { + "version": "8.4.11", + "image": "mysql@sha256:24989179f58856db763f418010c1169fecc944e3ec1ea7e921572d83eb070f70" + }, + "redis": { + "version": "7.4.11", + "image": "redis@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499" + } + }, "tools": { "k6": "grafana/k6@sha256:98305727d160c065e1da695619c40ea4becbc3e116a3842da2d6027970e428c2", "node": "node@sha256:0e5f906573693feaa1e21057ebdcfdb5bd5021f050b2dc7c9deceb629c7da2a8" diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md new file mode 100644 index 00000000..74c9acf4 --- /dev/null +++ b/examples/laravel-http/qualification/report.md @@ -0,0 +1,194 @@ +# Laravel HTTP hosting qualification: interim report + +These observations were collected October 16, 2026. Each result retains its +original application profile. The current four-worker candidate is runtime +source `e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772`; earlier two-worker results +use `f027697c83e753f708386d3487746c13a9a8c1e5`. +The [input manifest](inputs.json), [Composer lock](../composer.lock) and +[RoadRunner configuration](../runtime/roadrunner.yaml) identify the published +packages, images and HTTP pool. The SDK is installed normally from Composer. +All 166 installed SDK source/resource/schema files match its released commit. + +**Qualification is incomplete.** The four-worker FPM mixed runs failed. A +confirmed backend command deadlock is tracked in +[Workflow #819](https://github.com/durable-workflow/workflow/issues/819). +Its compatible library fix is published, but the affected Server image and +hosting comparison still need qualification. +No final hosting-capacity or Octane performance recommendation is established. + +## Method and remaining qualification + +Both profiles use nginx, four HTTP workers and one independent Artisan durable +worker in the current recipe. The completed short comparisons below used two +HTTP workers. Fresh paired comparisons and both complete mixed soaks are still +required for the current recipe and corrected backend. + +Each cell starts with a fresh Server database, namespace, role +credentials, application storage and processes. OPcache, cached configuration, +the dependency lock, RoadRunner binary and generator stay fixed. RoadRunner +enables only its HTTP pool and loopback control RPC. It has no jobs pool. + +The measurement host ran Linux/amd64 with eight logical CPUs, 16 GiB RAM and no +swap. Its reported CPU was AMD EPYC Rome. Component limits are: + +| Component | CPU | Memory, no swap | +| --- | ---: | ---: | +| HTTP application | 0.9 | 640 MiB | +| Durable CLI worker | 1 | 320 MiB | +| Application nginx | 0.1 | 64 MiB | +| Separate Server API | 2 | 1 GiB | +| Separate MySQL | 1 | 2 GiB | +| Separate Server queue worker | 0.2 | 384 MiB | +| Separate Server maintenance | 0.2 | 128 MiB | +| Separate Redis | 0.1 | 128 MiB | +| Separate k6 generator | 0.3 | 512 MiB | + +The application allocation totals 2 CPUs and 1 GiB. The backend and tools are +additional resources, not hidden inside that limit. + +Each cell has a 30-second warmup. Measured windows offer six context reads and +one new one-activity workflow per second for 120 seconds, with eight context and +16 workflow virtual users. Each original keeps its 35-second observer, +600-second run deadline and 900-second execution deadline. The drain limit is +36 seconds. Independent installed PHP readers verify the exact int64, null and +Unicode values and histories. SQL checks original identities, first-attempt +tasks, repairs, pending work and microsecond deadline offsets. + +The comparison plan orders pairs FPM/Octane, Octane/FPM, FPM/Octane, followed by +a fresh FPM drift check. Each profile also requires a fresh 300-second mixed +soak. Its five-original +cycle contains two short workflows, one timer, one queried/signalled workflow +and one queried/cancelled workflow. Two real HTTP reloads are requested at +120 and 240 seconds. Their actual times and replacement worker PIDs are read +back. Automatic HTTP recycling remains enabled at 500 requests per worker. + +## How to read the measurements + +Context, submission and status timings are separate HTTP operations. Workflow +latency is observed by polling at 250 ms intervals. It includes backend work +and that polling delay. Application CPU per terminal workflow includes the +fixed context workload, variable status polls, nginx and the CLI worker. It is +not callback CPU or a measure of durable replay savings. + +The sampler remains active through drain and verification. The tables' CPU +deltas cover offered load and its actual drain; the independent post-run +verification is separate. CPU and throttling use cgroup counters. Process RSS may count shared +pages more than once. Charged cgroup memory is reported separately. Sampled +peaks can miss short spikes; cgroup lifetime peaks also include warmup. +Individual process reads can disappear during recycling and are marked +unavailable rather than counted as measured zeroes. + +Backend access counters measure produced response-body bytes. Workflow poll +envelopes contain inline history and protocol metadata, so those counts are +not pure history bytes. Produced bytes can exceed delivered bytes after an +aborted response. Cache diagnostics are read from the actual stopped worker +and include that cell's prerequisites, warmup and measured window. + +## Published-package controls + +These controls ran on `f027697c` with the earlier two-worker HTTP profile. +Their linked receipts retain that scope. The changed HTTP pool and backend +still require their affected controls before final acceptance. + +The [lifecycle and cold-replacement receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222923826) +covers alternating users, request headers and logs, auth/namespace refusals, +five lifecycle originals, exact typed reads, eager/lazy SDK resolution and +HTTP reloads. An actual CLI SIGKILL followed by a fresh worker completes the +same original with its six-event prefix preserved in the final eight events, +one activity, five first-attempt tasks, zero repairs and unchanged deadlines. + +The [cache and connection receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223066356) +records these independent controls: + +| Control | Result | Actual stopped cache counters: hit / miss / eviction / forced cold | +| --- | --- | --- | +| TTL 1 second, capacity 100 | One original completes with its full prefix | 0 / 3 / 2 / 0 | +| Capacity 1, TTL 300 seconds | Two originals complete with their full prefixes | 0 / 6 / 3 / 4 | +| Actual 10-second API outage | Same original completes within its original 90-second cutoff | 0 / 1 / 0 / 0, final replacement only | + +All four cache/outage originals preserve typed results and exact deadlines, +with 16 completed first-attempt tasks and zero repairs. The outage produces an +actual cURL error 56 and three automatic CLI supervisor restarts. It does not +prove recovery within one process. The intentional kill and outage leave one +killed registration each. Their original cleanup wrappers fail the overly +strict zero-registration predicate; that wrapper result remains unsuccessful. +The task resources are removed. Ordinary measured cells retain the stricter +zero-registration and zero-restart checks. + +Earlier load, controller and older-package observations remain separate +[failed or scoped reference outcomes](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222599030). +They are not substituted for the final published tuple's measurements. + +## Results + +Fresh repetitions and reload soaks are required after the corrected package is +published and qualified in its normal Server consumer. Earlier results keep +their original application/backend identities and outcomes. + +### Four-worker FPM mixed runs + +Both runs use runtime source `e81b7c80`, the published backend in the manifest +and the unchanged 300-second mixed workload. The second run increases only the +Server API allocation from two to four CPUs, with its memory still 1 GiB. + +| Observation | Two-CPU API | Four-CPU API | +| --- | ---: | ---: | +| Admitted original workflows | 233 | 105 | +| Observed completions | 174 | 76 | +| Observed cancellations | 44 | 18 | +| Context responses | 1,728 | 727 | +| Dropped offers | 140 | 61 | +| Observer errors | 1 | 1 | +| Repairs on canonical readback | 118 | 35 | +| Result | Failed | Failed | + +The [two-CPU API run](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223818842) +passes both real master reloads and all HTTP requests, but fails the offered +workload and zero-repair criteria. Later canonical readback has 185 completed, +44 cancelled and four waiting originals. The API records 514.73 CPU seconds +over 302.99 elapsed seconds, including 38.32 seconds throttled. This is one +observed resource limit, not a proven explanation for every missed criterion. + +The separate four-CPU API run stops at a real cancellation HTTP 500. MySQL's +actual error 1213 lock graph shows signal and cancellation transactions for +different runs acquiring task/failure index locks in a cycle. MySQL rolls back +cancellation completely. Its original remains waiting with the same six-event +prefix and no failure record. Later aggregate readback has 82 completed, +18 cancelled and five waiting originals. Workflow #819 owns the reproduction, +compatible package correction and affected published consumer. + +Increasing API CPU is not an established fix, and neither run qualifies the +four-worker recipe. Their resources and failed originals were retired after +the immediate diagnosis. The narrow failure description remains in the owning +issue. No Octane mixed run or fresh four-worker paired comparison is claimed. + +### Earlier two-worker profile + +The earlier source `f027697c` completed three short-window pairs and an FPM +drift check. The [paired receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223479757) +records these measured windows, each with 120 completed workflows and 720 +context reads per profile: + +| Pair order | FPM app CPU seconds | Octane app CPU seconds | FPM / Octane observed workflow p95 ms | +| --- | ---: | ---: | ---: | +| FPM, Octane | 18.089 | 15.774 | 3,312 / 2,515 | +| Octane, FPM | 18.528 | 14.809 | 3,146 / 1,554 | +| FPM, Octane | 14.843 | 14.791 | 1,583 / 1,555 | + +The third pair's application CPU differs by only 0.35%. The receipt records +mean charged application memory across these pairs of 88.82 MiB for FPM and +191.57 MiB for Octane. These limited observations do not establish a consistent +speedup or durable execution gain. FPM remains the ordinary starting profile. + +Its first FPM mixed soak [failed](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223653640): +100 admitted originals, 70 observed completions, 18 observed cancellations, +457 context responses, 282 dropped iterations and two HTTP 502s. Independent +nginx logs show connection resets at the first FPM master reload. Subsequent +canonical readback has 78 completed, 18 cancelled and four waiting originals, +with 16 repairs. No OOM or automatic process restart occurred. + +That failed outcome is unchanged. Its short-window results do not qualify the +four-worker recipe or establish mixed-traffic capacity. The prospective recipe +keeps the same CPU/memory allocation and workload, increases both HTTP pools +and gives FPM a bounded in-flight request grace period. New measurements are +required for the changed profile. From 8df5e88fd3c6c6a5ea29e07ccc0e240d8a4ae9c3 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sat, 17 Oct 2026 22:58:43 +0000 Subject: [PATCH 23/28] test: select corrected published Server for HTTP qualification --- examples/laravel-http/README.md | 3 +++ .../laravel-http/qualification/inputs.json | 8 +++---- examples/laravel-http/qualification/report.md | 22 ++++++++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index e90b766c..7a3af7de 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -7,6 +7,9 @@ tested configuration, resource costs and limits. Qualification is incomplete. The mixed-load FPM runs failed, including a confirmed backend command deadlock tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). This recipe has no production-capacity or performance recommendation yet. +The published runtime now selected by [the input manifest](qualification/inputs.json) +includes that library fix. Fresh affected HTTP and four-worker hosting +qualification remain pending; earlier mixed-load results are unchanged. The application uses the normally published SDK. Durable workflow and activity polling runs in the separate `durable-workflow:worker` CLI container. HTTP diff --git a/examples/laravel-http/qualification/inputs.json b/examples/laravel-http/qualification/inputs.json index 7997a755..2001daf1 100644 --- a/examples/laravel-http/qualification/inputs.json +++ b/examples/laravel-http/qualification/inputs.json @@ -1,10 +1,10 @@ { "platform": "linux/amd64", "server": { - "version": "2.8.6", - "commit": "f2775db76a21f21f3b64e751a4e3222e0badf4f5", - "image": "durableworkflow/server@sha256:b3c53408dfadb0e029d66775cc1764af350d0bc3e200bda1ada219cba8fecea2", - "workflow_version": "2.5.22" + "version": "2.8.7", + "commit": "964f9e3d5b713376757294fd38c2cb3b4cba1400", + "image": "ghcr.io/durable-workflow/server@sha256:27c08e15c86e897703c2bab87dcf61847946b379876b15d0a3a491bc91cf30cd", + "workflow_version": "2.5.32" }, "services": { "mysql": { diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index 74c9acf4..98ce482a 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -4,18 +4,30 @@ These observations were collected October 16, 2026. Each result retains its original application profile. The current four-worker candidate is runtime source `e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772`; earlier two-worker results use `f027697c83e753f708386d3487746c13a9a8c1e5`. -The [input manifest](inputs.json), [Composer lock](../composer.lock) and -[RoadRunner configuration](../runtime/roadrunner.yaml) identify the published -packages, images and HTTP pool. The SDK is installed normally from Composer. +The [Composer lock](../composer.lock) and +[RoadRunner configuration](../runtime/roadrunner.yaml) identify the application +packages and HTTP pool. The October 16 results use published Server 2.8.6, +immutable image index `b3c53408`, with Workflow 2.5.22. The [input manifest](inputs.json) +now selects the next backend cohort; it does not retuple those observations. +The SDK is installed normally from Composer. All 166 installed SDK source/resource/schema files match its released commit. **Qualification is incomplete.** The four-worker FPM mixed runs failed. A confirmed backend command deadlock is tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). -Its compatible library fix is published, but the affected Server image and -hosting comparison still need qualification. +Its compatible library fix is now included in the newly published Server +selected below. Fresh affected HTTP and hosting acceptance remain pending. No final hosting-capacity or Octane performance recommendation is established. +## Next published backend + +The next cohort selects Server 2.8.7 / `964f9e3d`, immutable image index +`27c08e15`, with normally installed Workflow 2.5.32 / `78075b6f`. +The application, SDK lock, HTTP pools, workload, original deadlines and backend +allocations remain unchanged from the last four-CPU API profile. That Server +release has its own source, image and ordinary upgrade acceptance. It does not +qualify these HTTP measurements. All earlier failed originals remain failed. + ## Method and remaining qualification Both profiles use nginx, four HTTP workers and one independent Artisan durable From f6ac71a3568f8ae31f4f2e77915ed830434bb982 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sat, 17 Oct 2026 23:39:10 +0000 Subject: [PATCH 24/28] fix(qualification): drain original HTTP observers before final failure --- .github/workflows/ci.yml | 16 ++++ .../qualification/check-load-drain.sh | 56 ++++++++++++ .../qualification/load-drain-fixture.mjs | 64 +++++++++++++ examples/laravel-http/qualification/load.js | 14 ++- examples/laravel-http/qualification/report.md | 91 ++++++++++++++++++- 5 files changed, 229 insertions(+), 12 deletions(-) create mode 100644 examples/laravel-http/qualification/check-load-drain.sh create mode 100644 examples/laravel-http/qualification/load-drain-fixture.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d78b80eb..f88b7dc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -369,6 +369,22 @@ jobs: WORKFLOW_REQUIREMENT: ${{ matrix.workflow }} run: scripts/ci/laravel-service-transition-smoke.sh + load-driver-drain: + name: Laravel HTTP load-driver drain + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + LOAD_DRAIN_RUN: dw-load-drain-${{ github.run_id }}-${{ github.run_attempt }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false + - name: Preserve original observers after an independent HTTP failure + run: bash examples/laravel-http/qualification/check-load-drain.sh + - name: Remove only this test's HTTP fixture, generator and network + if: always() + run: bash examples/laravel-http/qualification/check-load-drain.sh cleanup + analyse: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/examples/laravel-http/qualification/check-load-drain.sh b/examples/laravel-http/qualification/check-load-drain.sh new file mode 100644 index 00000000..b15cce92 --- /dev/null +++ b/examples/laravel-http/qualification/check-load-drain.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +run=${LOAD_DRAIN_RUN:?Set a unique load-driver test name} +[[ "$run" =~ ^[a-z0-9-]{1,60}$ ]] +node=$(jq -er '.tools.node' inputs.json) +k6=$(jq -er '.tools.k6' inputs.json) +cleanup() { + docker rm --force "$run-k6" "$run-http" >/dev/null 2>&1 || true + docker network rm "$run" >/dev/null 2>&1 || true +} +if [[ "${1:-}" == cleanup ]]; then cleanup; exit; fi +report=$(mktemp -d) +trap 'cleanup; rm -r -- "$report"' EXIT +docker network create "$run" >/dev/null +for mode in clean failure; do + fail=false + expected_exit=0 + if [[ "$mode" == failure ]]; then fail=true; expected_exit=99; fi + docker run --detach --name "$run-http" --network "$run" --network-alias mock \ + --user 1000:1000 --cpus .2 --memory 128m --memory-swap 128m \ + --mount "type=bind,source=$PWD,target=/fixture,readonly" \ + -e "FAIL_LAST_CONTEXT=$fail" "$node" node /fixture/load-drain-fixture.mjs >/dev/null + ready=false + for attempt in $(seq 1 20); do + if docker exec "$run-http" node -e 'fetch("http://127.0.0.1:8080/test-state").then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))'; then + ready=true; break + fi + sleep .25 + done + [[ "$ready" == true ]] + set +e + docker run --name "$run-k6" --network "$run" --user 1000:1000 \ + --cpus .3 --memory 256m --memory-swap 256m \ + --mount "type=bind,source=$PWD,target=/fixture,readonly" \ + -e K6_NO_USAGE_REPORT=true -e EXAMPLE_ORIGIN=http://mock:8080 \ + -e DURABLE_WORKFLOW_NAMESPACE=drain-test -e "EXAMPLE_RUN_PREFIX=drain-$mode" \ + -e EXAMPLE_DURATION_SECONDS=5 -e EXAMPLE_MIXED=false -e EXAMPLE_SUMMARY_PATH=/tmp/drain-summary.json \ + --entrypoint k6 "$k6" run --quiet /fixture/load.js > "$report/$mode.log" 2>&1 + result=$? + set -e + docker cp "$run-k6:/tmp/drain-summary.json" "$report/$mode.json" + docker exec "$run-http" node -e 'fetch("http://127.0.0.1:8080/test-state").then(r=>r.text()).then(t=>console.log(t))' > "$report/$mode-state.json" + if [[ "$result" != "$expected_exit" ]]; then tail -n 12 "$report/$mode.log"; exit 1; fi + jq -c '{admitted:.metrics.workflow_admitted.values.count,terminal:.metrics.workflow_terminal.values.count,errors:.metrics.run_errors.values.count}' "$report/$mode.json" + cat "$report/$mode-state.json" + jq -e --arg mode "$mode" '.metrics.workflow_admitted.values.count==5 and + .metrics.workflow_terminal.values.count==5 and .metrics.context_completed.values.count==(if $mode=="clean" then 30 else 29 end) and + .metrics.dropped_iterations.values.count==0 and .metrics.run_errors.values.count==(if $mode=="clean" then 0 else 1 end) and + .example.original_observer_seconds==35 and .example.outer_drain_seconds==36' "$report/$mode.json" >/dev/null + jq -e '.admitted==5 and .observed==5 and .contexts==30 and .lateAdmissions==0' "$report/$mode-state.json" >/dev/null + printf '%s: all five originals observed through the declared drain; k6 exit %s\n' "$mode" "$result" + docker rm "$run-k6" >/dev/null + docker stop --time 2 "$run-http" >/dev/null + docker rm "$run-http" >/dev/null +done diff --git a/examples/laravel-http/qualification/load-drain-fixture.mjs b/examples/laravel-http/qualification/load-drain-fixture.mjs new file mode 100644 index 00000000..f596fea1 --- /dev/null +++ b/examples/laravel-http/qualification/load-drain-fixture.mjs @@ -0,0 +1,64 @@ +// Synthetic HTTP only. This checks the load driver's drain, not workflow execution. +import http from 'node:http'; + +const failLastContext = process.env.FAIL_LAST_CONTEXT === 'true'; +const originals = new Map(); +const observed = new Set(); +let contexts = 0; +let lateAdmissions = 0; +let firstAdmission; + +http.createServer(async (request, response) => { + if (request.url === '/test-state') { + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify({contexts, admitted: originals.size, observed: observed.size, lateAdmissions})); + return; + } + + const user = request.headers.authorization?.replace('Bearer example-', ''); + response.setHeader('X-Request-Id', request.headers['x-request-id'] || ''); + response.setHeader('X-Example-User', user || ''); + response.setHeader('X-Example-Namespace', 'drain-test'); + response.setHeader('X-Worker-Pid', '1'); + response.setHeader('Content-Type', 'application/json'); + if (!['alpha', 'beta'].includes(user)) { + response.writeHead(401).end('{}'); + return; + } + + if (request.method === 'GET' && request.url === '/api/context') { + contexts++; + if (failLastContext && contexts === 30) { + response.writeHead(503).end('{"error":"constructed_last_context_failure"}'); + return; + } + response.end(JSON.stringify({user, namespace: 'drain-test', client_resolved_at_boot: false})); + return; + } + if (request.method === 'POST' && request.url === '/api/workflows') { + let body = ''; + for await (const chunk of request) body += chunk; + const {id, kind} = JSON.parse(body); + if (kind !== 'short' || originals.has(id)) { + response.writeHead(409).end('{}'); + return; + } + const now = Date.now(); + firstAdmission ??= now; + if (now - firstAdmission >= 5000) lateAdmissions++; + originals.set(id, now); + response.writeHead(201).end(JSON.stringify({workflow_id: `${user}-${id}`, run_id: id})); + return; + } + const id = request.url?.replace('/api/workflows/', ''); + if (request.method === 'GET' && originals.has(id)) { + if (Date.now() - originals.get(id) < 4000) { + response.end('{"status":"waiting"}'); + return; + } + observed.add(id); + response.end(`{"status":"completed","output":{"user":"${user}","echo":"hello ${user}","number":9007199254740993,"nullable":null,"text":"雪"}}`); + return; + } + response.writeHead(404).end('{}'); +}).listen(8080, '0.0.0.0'); diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index 19a0df74..efdc789e 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -35,9 +35,9 @@ export const options = { preAllocatedVUs: 16, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, }, thresholds: { - checks: [{threshold: 'rate==1', abortOnFail: true}], - run_errors: [{threshold: 'count==0', abortOnFail: true}], - http_req_failed: [{threshold: 'rate==0', abortOnFail: true}], + checks: ['rate==1'], + run_errors: ['count==0'], + http_req_failed: ['rate==0'], dropped_iterations: ['count==0'], workflow_offered: [`count==${seconds}`], workflow_admitted: [`count==${seconds}`], @@ -88,12 +88,11 @@ export function context() { errors.add(0); boundaryIterations.add(0); const index = execution.scenario.iterationInTest; - if (index >= seconds * 6) { + if (index >= seconds * 6 || Date.now() >= execution.scenario.startTime + seconds * 1000) { boundaryIterations.add(1); return; } try { - requireCheck(Date.now() < execution.scenario.startTime + seconds * 1000, 'context offer is inside the selected window'); const user = index % 2 === 0 ? 'alpha' : 'beta'; const response = request(user, 'context', 'context'); const value = response.json(); @@ -128,7 +127,8 @@ export function workflow() { cancelled.add(0); boundaryIterations.add(0); const index = execution.scenario.iterationInTest; - if (index >= seconds) { + const started = Date.now(); + if (index >= seconds || started >= execution.scenario.startTime + seconds * 1000) { boundaryIterations.add(1); return; } @@ -136,10 +136,8 @@ export function workflow() { const user = index % 2 === 0 ? 'alpha' : 'beta'; const kind = mixed ? ['short', 'short', 'timer', 'signal', 'cancel'][index % 5] : 'short'; const id = `${prefix}-${kind}-${index}`; - const started = Date.now(); const cutoff = started + 35000; try { - requireCheck(started < execution.scenario.startTime + seconds * 1000, 'workflow offer is inside the selected window'); const acceptance = request(user, 'workflows', 'start', 'POST', {id, kind}, 201, cutoff - Date.now()).json(); admitted.add(1); console.log(JSON.stringify({phase: 'admitted', user, kind, id, workflow_id: acceptance.workflow_id, run_id: acceptance.run_id, started})); diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index 98ce482a..4569bd21 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -1,6 +1,6 @@ # Laravel HTTP hosting qualification: interim report -These observations were collected October 16, 2026. Each result retains its +These observations were collected October 16–17, 2026. Each result retains its original application profile. The current four-worker candidate is runtime source `e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772`; earlier two-worker results use `f027697c83e753f708386d3487746c13a9a8c1e5`. @@ -19,15 +19,45 @@ Its compatible library fix is now included in the newly published Server selected below. Fresh affected HTTP and hosting acceptance remain pending. No final hosting-capacity or Octane performance recommendation is established. -## Next published backend +## Current published backend -The next cohort selects Server 2.8.7 / `964f9e3d`, immutable image index +The October 17 cohort selects Server 2.8.7 / `964f9e3d`, immutable image index `27c08e15`, with normally installed Workflow 2.5.32 / `78075b6f`. The application, SDK lock, HTTP pools, workload, original deadlines and backend allocations remain unchanged from the last four-CPU API profile. That Server release has its own source, image and ordinary upgrade acceptance. It does not qualify these HTTP measurements. All earlier failed originals remain failed. +The [fresh functional controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243029697) +verify all 166 normally installed SDK files and the exact application source. +FPM passes alternating request identities, namespace refusals, five lifecycle +originals, four cold typed reads, eager/lazy resolution and real master reloads. +The initial 30-second warm run completes 30 workflows and 180 context requests +with zero errors, drops, repairs or retried tasks. + +| Independent control | Original outcomes | Stopped cache: hit / miss / eviction / forced cold | +| --- | --- | --- | +| TTL 1 second, capacity 100 | One completes with exact history prefix, values and deadlines | 0 / 3 / 1 / 1 | +| Capacity 1, TTL 300 seconds | Two complete with exact prefixes, values and deadlines | 0 / 6 / 3 / 4 | +| Ten-second API outage | Same original completes within its original 90-second cutoff | 0 / 1 / 0 / 0, final replacement only | + +These four originals have sixteen completed first-attempt tasks and zero +repairs, retries or pending tasks. Actual process settings and loaded Laravel +configuration are checked before cache admission. The outage causes three +supervised CLI replacements and leaves one stale registration. It does not +establish recovery within one process. A separate deliberate CLI SIGKILL also +passes same-original cold recovery with the eight-event final history and +unchanged deadlines. Normal stacks deregister fully and stop without OOM or +automatic restarts; intentional crash/outage results retain their stated limits. + +The first intended expiry control accidentally used the default TTL 300 because +the experiment helper's exported defaults overrode Compose's requested settings. +Its workflow completed, but its expiry assertion failed. A second setup was +retired before business admission. After correcting only that helper, the fresh +cache controls above verify the effective settings before starting new originals. +Application and vendor bytes are unchanged. The earlier attempts remain +unqualified for their intended cache settings. + ## Method and remaining qualification Both profiles use nginx, four HTTP workers and one independent Artisan durable @@ -48,7 +78,7 @@ swap. Its reported CPU was AMD EPYC Rome. Component limits are: | HTTP application | 0.9 | 640 MiB | | Durable CLI worker | 1 | 320 MiB | | Application nginx | 0.1 | 64 MiB | -| Separate Server API | 2 | 1 GiB | +| Separate Server API | 2 in the earlier pairs, 4 in the October 17 cohort | 1 GiB | | Separate MySQL | 1 | 2 GiB | | Separate Server queue worker | 0.2 | 384 MiB | | Separate Server maintenance | 0.2 | 128 MiB | @@ -137,6 +167,59 @@ Fresh repetitions and reload soaks are required after the corrected package is published and qualified in its normal Server consumer. Earlier results keep their original application/backend identities and outcomes. +### Published 2.8.7 four-worker FPM run + +The October 17 mixed run uses the published backend above, unchanged application +runtime `e81b7c80`, four HTTP workers and the four-CPU API allocation. It **fails**: + +| Observation | Result | +| --- | ---: | +| Admitted originals | 265 of 300 required | +| Observed completed / cancelled | 201 / 51 | +| Completed context requests | 1,743 of 1,800 required | +| Dropped offers | 92 | +| Canonical repairs | 82 | +| Canonical completed / cancelled / waiting | 210 / 51 / 4 | + +Both real master reloads replace all four HTTP workers. All 51 observed +cancellation requests and 51 signal requests return their expected 204, and +102 query replies pass their committed-activity checks. The earlier cancellation +deadlock is not observed in this run. That scoped result does not qualify the +load target or establish a cause for the remaining backlog and repairs. + +The original driver also has a separate failure: two iterations start outside +the offered window, fail its boundary assertions and trigger immediate threshold +abort at about 302 seconds. Thirteen observers are interrupted before the declared +36-second drain finishes. The independent audit then correctly refuses an +unfinished signal original. Those transport interruptions are not attributed to +a runtime crash. Dropped offers and repairs independently keep this run failed. + +A later read finds 82 distinct completed workflow tasks with one repair and one +attempt each. Sampled repaired tasks waited four to six seconds before their +first lease. This does not establish why they waited. All 265 activities complete +once in the recorded canonical audit; original run/execution deadline offsets +remain exact. The API records 521.84 CPU seconds, without CPU throttling; the +application records 84.08 CPU seconds. There is no OOM, swap or sampled host CPU +steal. Every normal role exits 0 without automatic restarts, all registrations +are removed, and the stack is retired. + +The prospective driver correction refuses late admissions without inventing +replacement offers and evaluates failures after the original observers drain. +The 35-second per-original observer, 36-second drain, rates, VU limits and all +success thresholds remain unchanged. This original is not rerun or relabeled. +Fresh load acceptance remains required after the correction and diagnosis. + +The synthetic [drain regression](check-load-drain.sh) exercises the actual k6 +driver against an HTTP fixture, not a workflow runtime. Five originals each +remain pending for four seconds during a five-second offered window. A separate +case injects one HTTP failure at the last context request. The old driver aborts +with only two terminal observations; the corrected driver observes all five and +still returns k6's failure exit 99. Its clean case observes all five and exits 0. +CI runs both cases without credentials or a real backend. Run locally with Docker +and jq using `LOAD_DRAIN_RUN=unique-drain-test bash qualification/check-load-drain.sh` +from the example directory. This check does not qualify load, durable execution +or a higher capacity. + ### Four-worker FPM mixed runs Both runs use runtime source `e81b7c80`, the published backend in the manifest From 79b0a6f4179c1b1ccbe965bce52a9a373b5b3852 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sat, 17 Oct 2026 23:55:57 +0000 Subject: [PATCH 25/28] Prepare bounded CLI worker pool for HTTP qualification --- examples/laravel-http/README.md | 11 ++++- .../laravel-http/compose.worker-pool.yaml | 7 +++ examples/laravel-http/qualification/report.md | 47 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 examples/laravel-http/compose.worker-pool.yaml diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 7a3af7de..57047451 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -8,8 +8,8 @@ Qualification is incomplete. The mixed-load FPM runs failed, including a confirmed backend command deadlock tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). This recipe has no production-capacity or performance recommendation yet. The published runtime now selected by [the input manifest](qualification/inputs.json) -includes that library fix. Fresh affected HTTP and four-worker hosting -qualification remain pending; earlier mixed-load results are unchanged. +includes that library fix. Fresh affected cancellation and signal controls pass. +Full hosting qualification remains pending; earlier mixed-load results are unchanged. The application uses the normally published SDK. Durable workflow and activity polling runs in the separate `durable-workflow:worker` CLI container. HTTP @@ -70,6 +70,13 @@ and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, CLI 1 CPU/320 MiB, nginx 0.1 CPU/64 MiB. Provision and report the separate Server/database allocation. These limits are qualification settings, not a general capacity promise. +The prospective two-worker configuration adds `-f compose.worker-pool.yaml` to +either Compose command. It runs two independent CLI workers at 0.5 CPU/160 MiB +each, keeping their combined allocation at 1 CPU/320 MiB. Each process has its +own registration and history cache. A small normal-SDK diagnostic supports +testing this configuration next; its Laravel lifecycle, mixed-load and paired +hosting acceptance remain pending. See the report before choosing it. + Synchronous query requests occupy HTTP workers while awaiting their result. Size the HTTP pool for that waiting time as well as request CPU. The FPM configuration gives in-flight requests a bounded 35-second process-control diff --git a/examples/laravel-http/compose.worker-pool.yaml b/examples/laravel-http/compose.worker-pool.yaml new file mode 100644 index 00000000..17e26be3 --- /dev/null +++ b/examples/laravel-http/compose.worker-pool.yaml @@ -0,0 +1,7 @@ +services: + worker: + deploy: + replicas: 2 + cpus: 0.5 + mem_limit: 160m + memswap_limit: 160m diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index 4569bd21..ad3973aa 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -16,7 +16,8 @@ All 166 installed SDK source/resource/schema files match its released commit. confirmed backend command deadlock is tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). Its compatible library fix is now included in the newly published Server -selected below. Fresh affected HTTP and hosting acceptance remain pending. +selected below. Fresh affected cancellation and signal controls pass; full +hosting acceptance remains pending. No final hosting-capacity or Octane performance recommendation is established. ## Current published backend @@ -65,6 +66,12 @@ worker in the current recipe. The completed short comparisons below used two HTTP workers. Fresh paired comparisons and both complete mixed soaks are still required for the current recipe and corrected backend. +The prospective [worker-pool override](../compose.worker-pool.yaml) runs two +independent Artisan workers at 0.5 CPU/160 MiB each. HTTP and nginx allocations +are unchanged, keeping the application total at 2 CPUs/1 GiB. Its actual Laravel +lifecycle, shutdown, cache, recovery and load checks must run on that new +configuration. Earlier one-worker results do not qualify it. + Each cell starts with a fresh Server database, namespace, role credentials, application storage and processes. OPcache, cached configuration, the dependency lock, RoadRunner binary and generator stay fixed. RoadRunner @@ -220,6 +227,44 @@ and jq using `LOAD_DRAIN_RUN=unique-drain-test bash qualification/check-load-dra from the example directory. This check does not qualify load, durable execution or a higher capacity. +### Published-SDK claim diagnostic + +A separate local diagnostic uses normal SDK 2.4.2 / `25d796ad` with all 166 +source/resource/schema files verified, the published 2.8.7 backend and the same +pure SDK workflow/activity definitions as this application. It exercises eight +fresh short, timer, signal/query and cancellation originals per configuration. +It does not run Laravel HTTP, offered load or a performance comparison. The +shared four-CPU host has existing swap and is not reserved for measurements. + +| Functional observation | One CLI process | Two CLI processes | +| --- | ---: | ---: | +| Combined SDK limit | 0.5 CPU / 320 MiB | 0.5 CPU / 320 MiB | +| Completed / cancelled originals | 7 / 1 | 7 / 1 | +| Completed / cancelled tasks | 30 / 1 | 30 / 1 | +| Completed activities | 8 | 8 | +| Repairs / retried tasks / final registrations | 0 / 0 / 0 | 0 / 0 / 0 | +| Maximum observed ready-to-first-lease interval | 5.402 seconds | 2.009 seconds | +| Longest of three actual queries | 5.451 seconds | 2.046 seconds | + +Both batches retain their original 35-second observers, exact typed results and +600/900-second deadlines. Each process stops normally without OOM or automatic +restart. Guzzle's ordinary request/statistics hooks record only allowlisted +poll identities, paths, timing and response metadata. Vendor bytes are unchanged. + +In the one-process original, the worker spends 13.478 seconds in actual HTTP +requests during the approximately 13.6-second active interval. Its workflow +poll replies during queued work take at most 0.470 seconds. The initial held +query poll returns `task_queue_changed`; subsequent sweeps lease workflow tasks. +The 5.451-second query observes `workflow_task_pending` until its own workflow's +pending resume completes, then receives the expected answer. These observations +support trying independent worker concurrency. They do not identify the cause +of the earlier mixed-run repairs, prove a missing wakeup, or establish a gain +or capacity. The two-process batch is fresh evidence, not a rescue of the first. + +The next hosting qualification uses the prospective pool override, the corrected +driver and fresh application processes. Original rates, budgets and success +thresholds stay fixed. Full hosting acceptance remains open. + ### Four-worker FPM mixed runs Both runs use runtime source `e81b7c80`, the published backend in the manifest From 530c98bfe55998bdd6c86e652e530031a482be29 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sun, 18 Oct 2026 00:29:57 +0000 Subject: [PATCH 26/28] Fix hosting probe readiness and reload reserve --- .../qualification/check-load-drain.sh | 20 ++++++--- .../laravel-http/qualification/lifecycle.mjs | 36 ++++++++++------ .../qualification/load-drain-fixture.mjs | 36 +++++++++++++--- examples/laravel-http/qualification/load.js | 16 +++++-- examples/laravel-http/qualification/report.md | 43 ++++++++++++++++--- 5 files changed, 118 insertions(+), 33 deletions(-) diff --git a/examples/laravel-http/qualification/check-load-drain.sh b/examples/laravel-http/qualification/check-load-drain.sh index b15cce92..247b2ca5 100644 --- a/examples/laravel-http/qualification/check-load-drain.sh +++ b/examples/laravel-http/qualification/check-load-drain.sh @@ -10,17 +10,24 @@ cleanup() { docker network rm "$run" >/dev/null 2>&1 || true } if [[ "${1:-}" == cleanup ]]; then cleanup; exit; fi +selected=${1:-all} +[[ "$selected" == all || "$selected" == mixed || "$selected" == reload-latency ]] report=$(mktemp -d) trap 'cleanup; rm -r -- "$report"' EXIT docker network create "$run" >/dev/null -for mode in clean failure; do +for mode in clean failure mixed reload-latency; do + if [[ "$selected" != all && "$selected" != "$mode" ]]; then continue; fi fail=false + mixed=false + context_delay=0 expected_exit=0 if [[ "$mode" == failure ]]; then fail=true; expected_exit=99; fi + if [[ "$mode" == mixed ]]; then mixed=true; fi + if [[ "$mode" == reload-latency ]]; then context_delay=1800; fi docker run --detach --name "$run-http" --network "$run" --network-alias mock \ --user 1000:1000 --cpus .2 --memory 128m --memory-swap 128m \ --mount "type=bind,source=$PWD,target=/fixture,readonly" \ - -e "FAIL_LAST_CONTEXT=$fail" "$node" node /fixture/load-drain-fixture.mjs >/dev/null + -e "FAIL_LAST_CONTEXT=$fail" -e "CONTEXT_DELAY_MS=$context_delay" "$node" node /fixture/load-drain-fixture.mjs >/dev/null ready=false for attempt in $(seq 1 20); do if docker exec "$run-http" node -e 'fetch("http://127.0.0.1:8080/test-state").then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))'; then @@ -35,7 +42,7 @@ for mode in clean failure; do --mount "type=bind,source=$PWD,target=/fixture,readonly" \ -e K6_NO_USAGE_REPORT=true -e EXAMPLE_ORIGIN=http://mock:8080 \ -e DURABLE_WORKFLOW_NAMESPACE=drain-test -e "EXAMPLE_RUN_PREFIX=drain-$mode" \ - -e EXAMPLE_DURATION_SECONDS=5 -e EXAMPLE_MIXED=false -e EXAMPLE_SUMMARY_PATH=/tmp/drain-summary.json \ + -e EXAMPLE_DURATION_SECONDS=5 -e "EXAMPLE_MIXED=$mixed" -e EXAMPLE_SUMMARY_PATH=/tmp/drain-summary.json \ --entrypoint k6 "$k6" run --quiet /fixture/load.js > "$report/$mode.log" 2>&1 result=$? set -e @@ -45,10 +52,11 @@ for mode in clean failure; do jq -c '{admitted:.metrics.workflow_admitted.values.count,terminal:.metrics.workflow_terminal.values.count,errors:.metrics.run_errors.values.count}' "$report/$mode.json" cat "$report/$mode-state.json" jq -e --arg mode "$mode" '.metrics.workflow_admitted.values.count==5 and - .metrics.workflow_terminal.values.count==5 and .metrics.context_completed.values.count==(if $mode=="clean" then 30 else 29 end) and - .metrics.dropped_iterations.values.count==0 and .metrics.run_errors.values.count==(if $mode=="clean" then 0 else 1 end) and + .metrics.workflow_terminal.values.count==5 and .metrics.context_completed.values.count==(if $mode=="failure" then 29 else 30 end) and + .metrics.dropped_iterations.values.count==0 and .metrics.run_errors.values.count==(if $mode=="failure" then 1 else 0 end) and .example.original_observer_seconds==35 and .example.outer_drain_seconds==36' "$report/$mode.json" >/dev/null - jq -e '.admitted==5 and .observed==5 and .contexts==30 and .lateAdmissions==0' "$report/$mode-state.json" >/dev/null + jq -e --arg mode "$mode" '.admitted==5 and .observed==5 and .contexts==30 and .lateAdmissions==0 and .earlyActions==0 and + (if $mode=="mixed" then .activityQueries==4 else .activityQueries==0 end)' "$report/$mode-state.json" >/dev/null printf '%s: all five originals observed through the declared drain; k6 exit %s\n' "$mode" "$result" docker rm "$run-k6" >/dev/null docker stop --time 2 "$run-http" >/dev/null diff --git a/examples/laravel-http/qualification/lifecycle.mjs b/examples/laravel-http/qualification/lifecycle.mjs index 539db948..8b2885d5 100644 --- a/examples/laravel-http/qualification/lifecycle.mjs +++ b/examples/laravel-http/qualification/lifecycle.mjs @@ -7,6 +7,7 @@ let requestNumber = 0; const originals = []; async function request(user, path, method = 'GET', body, expectedStatus = 200, timeout = 30_000) { + assert.ok(timeout > 0, 'The original observation expired before this request.'); const response = await fetch(`${origin}/api/${path}`, { method, signal: AbortSignal.timeout(Math.max(1, Math.floor(timeout))), headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', @@ -18,8 +19,7 @@ async function request(user, path, method = 'GET', body, expectedStatus = 200, t return {raw, value: raw === '' ? null : JSON.parse(raw)}; } -async function waitStatus(user, id, expected) { - const deadline = performance.now() + 35_000; +async function waitStatus(user, id, expected, deadline) { do { const result = await request(user, `workflows/${id}`, 'GET', undefined, 200, Math.min(30_000, deadline - performance.now())); assert.ok(performance.now() <= deadline, 'The original observation deadline expired.'); @@ -29,19 +29,31 @@ async function waitStatus(user, id, expected) { throw new Error(`Original ${user}/${id} did not reach ${expected} within its 35-second observer.`); } +async function waitForActivity(user, id, deadline) { + do { + const {value} = await request(user, `workflows/${id}/query`, 'POST', undefined, 200, deadline - performance.now()); + assert.ok(performance.now() <= deadline, 'The original activity observation expired.'); + assert.ok(['activity', 'waiting'].includes(value.phase), 'Unknown activity phase.'); + if (value.phase === 'waiting') return; + await new Promise(resolve => setTimeout(resolve, 250)); + } while (performance.now() < deadline); + throw new Error(`Original ${user}/${id} activity did not complete within its 35-second observer.`); +} + for (const [user, kind, suffix] of [ ['alpha', 'short', 'short-a'], ['beta', 'short', 'short-b'], ['alpha', 'timer', 'timer'], ['beta', 'signal', 'signal'], ['alpha', 'cancel', 'cancel'], ]) { const id = `${prefix}-${suffix}`; + const deadline = performance.now() + 35_000; const {value} = await request(user, 'workflows', 'POST', {id, kind}, 201); assert.equal(value.workflow_id, `${user}-${id}`); assert.equal(typeof value.run_id, 'string'); - originals.push({user, kind, id, workflow_id: value.workflow_id, run_id: value.run_id}); + originals.push({user, kind, id, workflow_id: value.workflow_id, run_id: value.run_id, deadline}); } for (const original of originals.filter(value => ['short', 'timer'].includes(value.kind))) { - const result = await waitStatus(original.user, original.id, 'completed'); + const result = await waitStatus(original.user, original.id, 'completed', original.deadline); assert.equal(result.value.output.user, original.user); assert.equal(result.value.output.echo, `hello ${original.user}`); assert.equal(result.value.output.nullable, null); @@ -50,16 +62,16 @@ for (const original of originals.filter(value => ['short', 'timer'].includes(val } const signal = originals.find(value => value.kind === 'signal'); -await waitStatus(signal.user, signal.id, 'waiting'); -assert.equal((await request(signal.user, `workflows/${signal.id}/query`, 'POST')).value.phase, 'waiting'); -await request(signal.user, `workflows/${signal.id}/signal`, 'POST', undefined, 204); -await waitStatus(signal.user, signal.id, 'completed'); +await waitStatus(signal.user, signal.id, 'waiting', signal.deadline); +await waitForActivity(signal.user, signal.id, signal.deadline); +await request(signal.user, `workflows/${signal.id}/signal`, 'POST', undefined, 204, signal.deadline - performance.now()); +await waitStatus(signal.user, signal.id, 'completed', signal.deadline); const cancel = originals.find(value => value.kind === 'cancel'); -await waitStatus(cancel.user, cancel.id, 'waiting'); -assert.equal((await request(cancel.user, `workflows/${cancel.id}/query`, 'POST')).value.phase, 'waiting'); -await request(cancel.user, `workflows/${cancel.id}/cancel`, 'POST', undefined, 204); -await waitStatus(cancel.user, cancel.id, 'cancelled'); +await waitStatus(cancel.user, cancel.id, 'waiting', cancel.deadline); +await waitForActivity(cancel.user, cancel.id, cancel.deadline); +await request(cancel.user, `workflows/${cancel.id}/cancel`, 'POST', undefined, 204, cancel.deadline - performance.now()); +await waitStatus(cancel.user, cancel.id, 'cancelled', cancel.deadline); await request('beta', `workflows/${prefix}-short-a`, 'GET', undefined, 404); console.log(JSON.stringify({originals, completed: 4, cancelled: 1, queries: 2, signal: 1, foreign_user_refusal: 404})); diff --git a/examples/laravel-http/qualification/load-drain-fixture.mjs b/examples/laravel-http/qualification/load-drain-fixture.mjs index f596fea1..c3ca8c77 100644 --- a/examples/laravel-http/qualification/load-drain-fixture.mjs +++ b/examples/laravel-http/qualification/load-drain-fixture.mjs @@ -2,8 +2,11 @@ import http from 'node:http'; const failLastContext = process.env.FAIL_LAST_CONTEXT === 'true'; +const contextDelay = Number(process.env.CONTEXT_DELAY_MS || 0); const originals = new Map(); const observed = new Set(); +let earlyActions = 0; +let activityQueries = 0; let contexts = 0; let lateAdmissions = 0; let firstAdmission; @@ -11,7 +14,7 @@ let firstAdmission; http.createServer(async (request, response) => { if (request.url === '/test-state') { response.setHeader('Content-Type', 'application/json'); - response.end(JSON.stringify({contexts, admitted: originals.size, observed: observed.size, lateAdmissions})); + response.end(JSON.stringify({contexts, admitted: originals.size, observed: observed.size, lateAdmissions, earlyActions, activityQueries})); return; } @@ -28,6 +31,7 @@ http.createServer(async (request, response) => { if (request.method === 'GET' && request.url === '/api/context') { contexts++; + if (contextDelay > 0) await new Promise(resolve => setTimeout(resolve, contextDelay)); if (failLastContext && contexts === 30) { response.writeHead(503).end('{"error":"constructed_last_context_failure"}'); return; @@ -39,24 +43,46 @@ http.createServer(async (request, response) => { let body = ''; for await (const chunk of request) body += chunk; const {id, kind} = JSON.parse(body); - if (kind !== 'short' || originals.has(id)) { + if (!['short', 'timer', 'signal', 'cancel'].includes(kind) || originals.has(id)) { response.writeHead(409).end('{}'); return; } const now = Date.now(); firstAdmission ??= now; if (now - firstAdmission >= 5000) lateAdmissions++; - originals.set(id, now); + originals.set(id, {started: now, kind, queries: 0, terminal: false}); response.writeHead(201).end(JSON.stringify({workflow_id: `${user}-${id}`, run_id: id})); return; } - const id = request.url?.replace('/api/workflows/', ''); + const parts = request.url?.replace('/api/workflows/', '').split('/') || []; + const id = parts[0]; + const original = originals.get(id); + if (request.method === 'POST' && original && parts[1] === 'query') { + original.queries++; + activityQueries++; + response.end(JSON.stringify({phase: original.queries === 1 ? 'activity' : 'waiting'})); + return; + } + if (request.method === 'POST' && original && ['signal', 'cancel'].includes(parts[1])) { + if (original.queries < 2 || parts[1] !== original.kind) { + earlyActions++; + response.writeHead(409).end('{}'); + return; + } + original.terminal = true; + response.writeHead(204).end(); + return; + } if (request.method === 'GET' && originals.has(id)) { - if (Date.now() - originals.get(id) < 4000) { + if (['signal', 'cancel'].includes(original.kind) ? !original.terminal : Date.now() - original.started < 4000) { response.end('{"status":"waiting"}'); return; } observed.add(id); + if (original.kind === 'cancel') { + response.end('{"status":"cancelled"}'); + return; + } response.end(`{"status":"completed","output":{"user":"${user}","echo":"hello ${user}","number":9007199254740993,"nullable":null,"text":"雪"}}`); return; } diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index efdc789e..c9536133 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -30,7 +30,7 @@ const seenPids = new Set(); export const options = { scenarios: { context: {executor: 'constant-arrival-rate', rate: 6, timeUnit: '1s', duration: `${seconds}s`, - preAllocatedVUs: 8, maxVUs: 8, exec: 'context', gracefulStop: '36s'}, + preAllocatedVUs: 16, maxVUs: 16, exec: 'context', gracefulStop: '36s'}, workflows: {executor: 'constant-arrival-rate', rate: 1, timeUnit: '1s', duration: `${seconds}s`, preAllocatedVUs: 16, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, }, @@ -120,6 +120,17 @@ function waitStatus(user, id, expected, cutoff) { throw new Error('Original 35-second observation expired.'); } +function waitForActivity(user, id, cutoff) { + while (Date.now() < cutoff) { + const phase = request(user, `workflows/${id}/query`, 'query', 'POST', undefined, 200, cutoff - Date.now()).json().phase; + requireCheck(Date.now() <= cutoff, 'original observer has not expired'); + requireCheck(['activity', 'waiting'].includes(phase), 'query returns a known activity phase'); + if (phase === 'waiting') return; + sleep(Math.min(0.25, Math.max(0, cutoff - Date.now()) / 1000)); + } + throw new Error('Activity was not committed within the original 35-second observation.'); +} + export function workflow() { errors.add(0); failed.add(0); @@ -144,8 +155,7 @@ export function workflow() { requireCheck(acceptance.workflow_id === `${user}-${id}` && typeof acceptance.run_id === 'string', 'accepted identity matches'); if (kind === 'signal' || kind === 'cancel') { waitStatus(user, id, 'waiting', cutoff); - const phase = request(user, `workflows/${id}/query`, 'query', 'POST', undefined, 200, cutoff - Date.now()).json(); - requireCheck(phase.phase === 'waiting', 'query observes committed activity'); + waitForActivity(user, id, cutoff); request(user, `workflows/${id}/${kind === 'signal' ? 'signal' : 'cancel'}`, kind === 'signal' ? 'signal' : 'cancel', 'POST', undefined, 204, cutoff - Date.now()); } diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index ad3973aa..2160492c 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -1,6 +1,6 @@ # Laravel HTTP hosting qualification: interim report -These observations were collected October 16–17, 2026. Each result retains its +These observations were collected October 16–18, 2026. Each result retains its original application profile. The current four-worker candidate is runtime source `e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772`; earlier two-worker results use `f027697c83e753f708386d3487746c13a9a8c1e5`. @@ -69,11 +69,40 @@ required for the current recipe and corrected backend. The prospective [worker-pool override](../compose.worker-pool.yaml) runs two independent Artisan workers at 0.5 CPU/160 MiB each. HTTP and nginx allocations are unchanged, keeping the application total at 2 CPUs/1 GiB. Its actual Laravel -lifecycle, shutdown, cache, recovery and load checks must run on that new -configuration. Earlier one-worker results do not qualify it. - -Each cell starts with a fresh Server database, namespace, role -credentials, application storage and processes. OPcache, cached configuration, +lifecycle, cold typed reads, request isolation, eager/lazy resolution and real +reload controls pass on published Server 2.8.7. A 30-second warmup completes +30 workflows and 180 context requests with zero errors, drops, repairs or +retries. Both CLI processes stop cleanly and deregister. Cache, crash, outage +and complete load acceptance remain pending for this configuration. + +The first two-process mixed soak at `79b0a6f` remains **failed**. It admits all +300 workflows and completes every activity once, but observes only 239 completed +and 59 cancelled workflows. Two query assertions abort their observers, leaving +two runs waiting at the post-run read. Three context requests are dropped. +All 4,046 HTTP responses have their expected status, with zero repairs, retried +tasks, OOMs or swapping. Both scheduled reloads replace all four FPM workers. +The eleven stack roles subsequently exit zero, without automatic restarts or +remaining registrations. + +The driver incorrectly treats run status `waiting` as proof of a committed +activity. That status can also mean waiting for the activity itself. The revised +driver waits for this example's explicit query phase within the original +35-second cutoff, then sends the signal or cancellation. Its context pool also +increases from eight to sixteen virtual users after the generator reports +exhausting its reserve during reload. The offered rates, application limits, +deadlines, drain and zero-error/drop requirements stay unchanged. These are new +qualification inputs. They do not turn the previous run into a pass. + +Constructed HTTP controls reproduce both old-driver failures. The revised +driver observes all five originals in the mixed phase transition, with no +signal or cancellation before readiness. A separate 1.8-second context-delay +control completes all thirty context requests without dropped offers. The +clean drain control passes, and the injected-error control still fails with +exit 99 after observing every original. These are driver controls, not a +successful application soak. + +Each cell starts with a fresh Server database, namespace and role-credential +records, application storage and processes. OPcache, cached configuration, the dependency lock, RoadRunner binary and generator stay fixed. RoadRunner enables only its HTTP pool and loopback control RPC. It has no jobs pool. @@ -96,7 +125,7 @@ The application allocation totals 2 CPUs and 1 GiB. The backend and tools are additional resources, not hidden inside that limit. Each cell has a 30-second warmup. Measured windows offer six context reads and -one new one-activity workflow per second for 120 seconds, with eight context and +one new one-activity workflow per second for 120 seconds, with sixteen context and 16 workflow virtual users. Each original keeps its 35-second observer, 600-second run deadline and 900-second execution deadline. The drain limit is 36 seconds. Independent installed PHP readers verify the exact int64, null and From af741d1a0905279b65851a9d80e63c1cf96d096a Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sun, 18 Oct 2026 00:59:01 +0000 Subject: [PATCH 27/28] Reserve context users through the declared request limit --- examples/laravel-http/README.md | 6 ++++-- .../laravel-http/qualification/check-load-drain.sh | 2 +- examples/laravel-http/qualification/load.js | 8 ++++++-- examples/laravel-http/qualification/report.md | 13 ++++++++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index 57047451..df8017e3 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -210,8 +210,10 @@ unchanged identities/deadlines. `qualification/load.js` uses the open-source k6 constant-arrival-rate executor: six authenticated context reads and one new original workflow per second. Run -the tool separately from the fixed application allocation. Preallocate all eight -context and sixteen workflow virtual users; a missed offer still fails the run. +the tool separately from the fixed application allocation. Preallocate 64 context +and sixteen workflow virtual users; a missed offer still fails the run. The +context reserve covers six requests per second through their existing ten-second +transport limit. It does not increase the offered rate or extend a request. The request sequence is finite. A generator tick at the window boundary is counted separately and cannot admit an extra workflow or context read. A late planned offer fails before HTTP admission. Each original has diff --git a/examples/laravel-http/qualification/check-load-drain.sh b/examples/laravel-http/qualification/check-load-drain.sh index 247b2ca5..2ae88514 100644 --- a/examples/laravel-http/qualification/check-load-drain.sh +++ b/examples/laravel-http/qualification/check-load-drain.sh @@ -23,7 +23,7 @@ for mode in clean failure mixed reload-latency; do expected_exit=0 if [[ "$mode" == failure ]]; then fail=true; expected_exit=99; fi if [[ "$mode" == mixed ]]; then mixed=true; fi - if [[ "$mode" == reload-latency ]]; then context_delay=1800; fi + if [[ "$mode" == reload-latency ]]; then context_delay=9000; fi docker run --detach --name "$run-http" --network "$run" --network-alias mock \ --user 1000:1000 --cpus .2 --memory 128m --memory-swap 128m \ --mount "type=bind,source=$PWD,target=/fixture,readonly" \ diff --git a/examples/laravel-http/qualification/load.js b/examples/laravel-http/qualification/load.js index c9536133..c667a1b1 100644 --- a/examples/laravel-http/qualification/load.js +++ b/examples/laravel-http/qualification/load.js @@ -8,6 +8,9 @@ const namespace = __ENV.DURABLE_WORKFLOW_NAMESPACE; const prefix = __ENV.EXAMPLE_RUN_PREFIX; const seconds = Number(__ENV.EXAMPLE_DURATION_SECONDS || 120); const mixed = __ENV.EXAMPLE_MIXED === 'true'; +const requestTimeoutMs = 10000; +// Deliver the fixed context rate while every still-on-time request is in flight. +const contextUsers = 6 * (requestTimeoutMs / 1000) + 4; if (!namespace || !/^[a-zA-Z0-9_-]{1,30}$/.test(prefix || '') || !Number.isInteger(seconds) || seconds < 5 || seconds > 300) { throw new Error('Select a fixed namespace, fresh prefix and a 5–300 second offered window.'); } @@ -30,7 +33,7 @@ const seenPids = new Set(); export const options = { scenarios: { context: {executor: 'constant-arrival-rate', rate: 6, timeUnit: '1s', duration: `${seconds}s`, - preAllocatedVUs: 16, maxVUs: 16, exec: 'context', gracefulStop: '36s'}, + preAllocatedVUs: contextUsers, maxVUs: contextUsers, exec: 'context', gracefulStop: '36s'}, workflows: {executor: 'constant-arrival-rate', rate: 1, timeUnit: '1s', duration: `${seconds}s`, preAllocatedVUs: 16, maxVUs: 16, exec: 'workflow', gracefulStop: '36s'}, }, @@ -58,7 +61,7 @@ function header(response, name) { function request(user, path, name, method = 'GET', body, expected = 200, remainingMs = 10000) { if (remainingMs <= 0) throw new Error('Original 35-second observation expired.'); - const transportLimitMs = name === 'query' ? 30000 : 10000; + const transportLimitMs = name === 'query' ? 30000 : requestTimeoutMs; const requestId = `${prefix}-${__VU}-${__ITER}-${requestNumber++}`; const response = http.request(method, `${origin}/api/${path}`, body === undefined ? null : JSON.stringify(body), { headers: {Authorization: `Bearer example-${user}`, Accept: 'application/json', @@ -180,6 +183,7 @@ export function workflow() { export function handleSummary(data) { data.example = {prefix, namespace, duration_seconds: seconds, context_rate: 6, workflow_rate: 1, + context_virtual_users: contextUsers, context_request_timeout_ms: requestTimeoutMs, mixed, original_observer_seconds: 35, outer_drain_seconds: 36}; return {stdout: JSON.stringify(data), [__ENV.EXAMPLE_SUMMARY_PATH || '/proof/summary.json']: JSON.stringify(data)}; } diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index 2160492c..27f29677 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -101,6 +101,17 @@ clean drain control passes, and the injected-error control still fails with exit 99 after observing every original. These are driver controls, not a successful application soak. +The sixteen-context-user driver subsequently passes the fresh FPM mixed cohort +with 300 terminal originals, 1,800 contexts and zero errors, drops or repairs. +Its separate Octane cohort completes all 300 originals and every response check, +but drops twelve context offers at reload, so that run stays failed. The generator +again reports exhausting its reserve; actual context latency reaches 4.002 seconds, +within the existing ten-second HTTP limit. The next driver reserves 64 context +users, covering six requests per second through that unchanged limit. Its +constructed latency control now waits nine seconds. Both profiles need fresh +load qualification with this common input. No request or workflow deadline, +offered rate, application allocation or success threshold is increased. + Each cell starts with a fresh Server database, namespace and role-credential records, application storage and processes. OPcache, cached configuration, the dependency lock, RoadRunner binary and generator stay fixed. RoadRunner @@ -125,7 +136,7 @@ The application allocation totals 2 CPUs and 1 GiB. The backend and tools are additional resources, not hidden inside that limit. Each cell has a 30-second warmup. Measured windows offer six context reads and -one new one-activity workflow per second for 120 seconds, with sixteen context and +one new one-activity workflow per second for 120 seconds, with 64 context and 16 workflow virtual users. Each original keeps its 35-second observer, 600-second run deadline and 900-second execution deadline. The drain limit is 36 seconds. Independent installed PHP readers verify the exact int64, null and From 516d13a22a35adedd71c67095cf7f33176e9ef44 Mon Sep 17 00:00:00 2001 From: Durable Workflow Date: Sun, 18 Oct 2026 01:52:40 +0000 Subject: [PATCH 28/28] Document qualified Laravel FPM and Octane profiles --- examples/laravel-http/README.md | 47 +- examples/laravel-http/qualification/report.md | 654 ++++++++---------- 2 files changed, 318 insertions(+), 383 deletions(-) diff --git a/examples/laravel-http/README.md b/examples/laravel-http/README.md index df8017e3..d06d6131 100644 --- a/examples/laravel-http/README.md +++ b/examples/laravel-http/README.md @@ -4,12 +4,13 @@ This application runs with nginx/PHP-FPM or optional Laravel Octane and RoadRunner. The [qualification report](qualification/report.md) records the tested configuration, resource costs and limits. -Qualification is incomplete. The mixed-load FPM runs failed, including a -confirmed backend command deadlock tracked in [Workflow #819](https://github.com/durable-workflow/workflow/issues/819). -This recipe has no production-capacity or performance recommendation yet. -The published runtime now selected by [the input manifest](qualification/inputs.json) -includes that library fix. Fresh affected cancellation and signal controls pass. -Full hosting qualification remains pending; earlier mixed-load results are unchanged. +Start with FPM for this recipe. Both documented two-CLI-worker profiles pass +functional/recovery controls, three short comparison pairs, a baseline recheck +and mixed-load reload soaks on the pinned tuple. Octane improves small context +reads but shows no consistent application CPU or workflow-latency gain, while +using about 185 MiB more charged memory. Production capacity remains +workload-specific. The [report](qualification/report.md) gives the measured +costs, limits and earlier failed runs. The application uses the normally published SDK. Durable workflow and activity polling runs in the separate `durable-workflow:worker` CLI container. HTTP @@ -48,7 +49,7 @@ The checkout and state directory must be writable by UID/GID `1000:1000`, used by these containers. Start the FPM profile with: ```bash -docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml up --build -d +docker compose --env-file /private/example.env -f compose.yaml -f compose.fpm.yaml -f compose.worker-pool.yaml up --build -d ``` The HTTP container receives only the client token. The CLI container receives @@ -61,21 +62,20 @@ separate application processes. For the optional Octane profile: ```bash -docker compose --env-file /private/example.env up --build -d +docker compose --env-file /private/example.env -f compose.yaml -f compose.worker-pool.yaml up --build -d ``` -Both modes use four HTTP workers, one durable CLI worker and nginx with the same +Both commands use four HTTP workers, two durable CLI workers and nginx with the same OPcache and cached configuration. The application allocation totals two CPUs -and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, CLI 1 CPU/320 MiB, nginx +and 1 GiB with no swap: HTTP 0.9 CPU/640 MiB, each CLI 0.5 CPU/160 MiB, nginx 0.1 CPU/64 MiB. Provision and report the separate Server/database allocation. These limits are qualification settings, not a general capacity promise. -The prospective two-worker configuration adds `-f compose.worker-pool.yaml` to -either Compose command. It runs two independent CLI workers at 0.5 CPU/160 MiB -each, keeping their combined allocation at 1 CPU/320 MiB. Each process has its -own registration and history cache. A small normal-SDK diagnostic supports -testing this configuration next; its Laravel lifecycle, mixed-load and paired -hosting acceptance remain pending. See the report before choosing it. +The `compose.worker-pool.yaml` override selects the two-worker configuration +used in the current results. Each process has its own registration and history +cache. Omitting that override uses the base recipe's one CLI worker at +1 CPU/320 MiB, preserving the same combined application allocation. Its earlier +load outcomes are recorded separately. See the report before choosing a profile. Synchronous query requests occupy HTTP workers while awaiting their result. Size the HTTP pool for that waiting time as well as request CPU. The FPM @@ -148,12 +148,10 @@ manually repair history. After the replacement completes, `after` requires the whole original prefix, unchanged deadline fields and exact typed result. Delete the temporary checkpoint when the control ends. -One actual [SIGKILL and cold replacement control](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222923826) -passes with the same original run, complete history prefix and unchanged -deadlines. Separate [cache and connection controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223066356) -cover expiry, capacity eviction and supervised recovery after API connection -loss. The latter replaces the CLI process three times, so it does not establish -recovery within one process. +Actual [two-worker FPM replacement, cache and connection controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243887984) +preserve the same original runs, complete history prefixes and unchanged +deadlines. API connection loss replaces the two CLI processes three and six +times, respectively, so it does not establish recovery within one process. History-cache defaults remain 100 entries/300 seconds. The existing SDK validates capacity and TTL, with a 3600-second TTL ceiling. `EXAMPLE_HISTORY_CAPACITY` and @@ -184,6 +182,11 @@ php qualification/cache.php checkpoint fresh-outage /private/outage.json php qualification/cache.php resume fresh-outage /private/outage.json ``` +For the two-worker pool's capacity control, checkpoint four fresh originals +before releasing any of them, then resume those same four. This fills both +independent caches. The two-original `eviction` command above exercises one +CLI worker. + Do not run the three profiles together or reuse a checkpoint. The driver does not change the CLI's cache settings, stop services, restart workers or provision credentials. Use the normal supervisor for connection recovery and record actual diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md index 27f29677..9fdf0487 100644 --- a/examples/laravel-http/qualification/report.md +++ b/examples/laravel-http/qualification/report.md @@ -1,374 +1,306 @@ -# Laravel HTTP hosting qualification: interim report - -These observations were collected October 16–18, 2026. Each result retains its -original application profile. The current four-worker candidate is runtime -source `e81b7c80e8854c7ce97fbd2e95e7ce17c8f10772`; earlier two-worker results -use `f027697c83e753f708386d3487746c13a9a8c1e5`. -The [Composer lock](../composer.lock) and -[RoadRunner configuration](../runtime/roadrunner.yaml) identify the application -packages and HTTP pool. The October 16 results use published Server 2.8.6, -immutable image index `b3c53408`, with Workflow 2.5.22. The [input manifest](inputs.json) -now selects the next backend cohort; it does not retuple those observations. -The SDK is installed normally from Composer. -All 166 installed SDK source/resource/schema files match its released commit. - -**Qualification is incomplete.** The four-worker FPM mixed runs failed. A -confirmed backend command deadlock is tracked in -[Workflow #819](https://github.com/durable-workflow/workflow/issues/819). -Its compatible library fix is now included in the newly published Server -selected below. Fresh affected cancellation and signal controls pass; full -hosting acceptance remains pending. -No final hosting-capacity or Octane performance recommendation is established. - -## Current published backend - -The October 17 cohort selects Server 2.8.7 / `964f9e3d`, immutable image index -`27c08e15`, with normally installed Workflow 2.5.32 / `78075b6f`. -The application, SDK lock, HTTP pools, workload, original deadlines and backend -allocations remain unchanged from the last four-CPU API profile. That Server -release has its own source, image and ordinary upgrade acceptance. It does not -qualify these HTTP measurements. All earlier failed originals remain failed. - -The [fresh functional controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243029697) -verify all 166 normally installed SDK files and the exact application source. -FPM passes alternating request identities, namespace refusals, five lifecycle -originals, four cold typed reads, eager/lazy resolution and real master reloads. -The initial 30-second warm run completes 30 workflows and 180 context requests -with zero errors, drops, repairs or retried tasks. - -| Independent control | Original outcomes | Stopped cache: hit / miss / eviction / forced cold | -| --- | --- | --- | -| TTL 1 second, capacity 100 | One completes with exact history prefix, values and deadlines | 0 / 3 / 1 / 1 | -| Capacity 1, TTL 300 seconds | Two complete with exact prefixes, values and deadlines | 0 / 6 / 3 / 4 | -| Ten-second API outage | Same original completes within its original 90-second cutoff | 0 / 1 / 0 / 0, final replacement only | - -These four originals have sixteen completed first-attempt tasks and zero -repairs, retries or pending tasks. Actual process settings and loaded Laravel -configuration are checked before cache admission. The outage causes three -supervised CLI replacements and leaves one stale registration. It does not -establish recovery within one process. A separate deliberate CLI SIGKILL also -passes same-original cold recovery with the eight-event final history and -unchanged deadlines. Normal stacks deregister fully and stop without OOM or -automatic restarts; intentional crash/outage results retain their stated limits. - -The first intended expiry control accidentally used the default TTL 300 because -the experiment helper's exported defaults overrode Compose's requested settings. -Its workflow completed, but its expiry assertion failed. A second setup was -retired before business admission. After correcting only that helper, the fresh -cache controls above verify the effective settings before starting new originals. -Application and vendor bytes are unchanged. The earlier attempts remain -unqualified for their intended cache settings. - -## Method and remaining qualification - -Both profiles use nginx, four HTTP workers and one independent Artisan durable -worker in the current recipe. The completed short comparisons below used two -HTTP workers. Fresh paired comparisons and both complete mixed soaks are still -required for the current recipe and corrected backend. - -The prospective [worker-pool override](../compose.worker-pool.yaml) runs two -independent Artisan workers at 0.5 CPU/160 MiB each. HTTP and nginx allocations -are unchanged, keeping the application total at 2 CPUs/1 GiB. Its actual Laravel -lifecycle, cold typed reads, request isolation, eager/lazy resolution and real -reload controls pass on published Server 2.8.7. A 30-second warmup completes -30 workflows and 180 context requests with zero errors, drops, repairs or -retries. Both CLI processes stop cleanly and deregister. Cache, crash, outage -and complete load acceptance remain pending for this configuration. - -The first two-process mixed soak at `79b0a6f` remains **failed**. It admits all -300 workflows and completes every activity once, but observes only 239 completed -and 59 cancelled workflows. Two query assertions abort their observers, leaving -two runs waiting at the post-run read. Three context requests are dropped. -All 4,046 HTTP responses have their expected status, with zero repairs, retried -tasks, OOMs or swapping. Both scheduled reloads replace all four FPM workers. -The eleven stack roles subsequently exit zero, without automatic restarts or -remaining registrations. - -The driver incorrectly treats run status `waiting` as proof of a committed -activity. That status can also mean waiting for the activity itself. The revised -driver waits for this example's explicit query phase within the original -35-second cutoff, then sends the signal or cancellation. Its context pool also -increases from eight to sixteen virtual users after the generator reports -exhausting its reserve during reload. The offered rates, application limits, -deadlines, drain and zero-error/drop requirements stay unchanged. These are new -qualification inputs. They do not turn the previous run into a pass. - -Constructed HTTP controls reproduce both old-driver failures. The revised -driver observes all five originals in the mixed phase transition, with no -signal or cancellation before readiness. A separate 1.8-second context-delay -control completes all thirty context requests without dropped offers. The -clean drain control passes, and the injected-error control still fails with -exit 99 after observing every original. These are driver controls, not a -successful application soak. - -The sixteen-context-user driver subsequently passes the fresh FPM mixed cohort -with 300 terminal originals, 1,800 contexts and zero errors, drops or repairs. -Its separate Octane cohort completes all 300 originals and every response check, -but drops twelve context offers at reload, so that run stays failed. The generator -again reports exhausting its reserve; actual context latency reaches 4.002 seconds, -within the existing ten-second HTTP limit. The next driver reserves 64 context -users, covering six requests per second through that unchanged limit. Its -constructed latency control now waits nine seconds. Both profiles need fresh -load qualification with this common input. No request or workflow deadline, -offered rate, application allocation or success threshold is increased. - -Each cell starts with a fresh Server database, namespace and role-credential -records, application storage and processes. OPcache, cached configuration, -the dependency lock, RoadRunner binary and generator stay fixed. RoadRunner -enables only its HTTP pool and loopback control RPC. It has no jobs pool. - -The measurement host ran Linux/amd64 with eight logical CPUs, 16 GiB RAM and no -swap. Its reported CPU was AMD EPYC Rome. Component limits are: +# Laravel HTTP hosting qualification + +This report covers ordinary PHP-FPM and optional Octane/RoadRunner hosting of +the Laravel service example. HTTP processes submit and inspect operations. +Independent Artisan CLI workers execute durable instructions against Server. +HTTP process reuse does not retain workflow Fibers or eliminate replay. + +Both profiles pass the bounded functional, recovery and load qualification +below on the published tuple. Start with FPM for this recipe: the comparison +does not establish a consistent application CPU or workflow-latency gain from +Octane, which uses more memory. Production capacity remains workload-specific. + +## Reproducible inputs + +The October 18 cohort uses normally installed SDK 2.4.2 / `25d796ad`, published +Server 2.8.7 / `964f9e3d` / immutable image index `27c08e15`, and Workflow +2.5.32 / `78075b6f`. All 166 installed SDK source/resource/schema files match +the released commit. The application source is checked before admission. +The common load-driver revision is `af741d1`; application runtime/configuration +remains `e81b7c80`. Later report-only edits do not change those measured bytes. + +The application runs PHP 8.3.35. The [Composer lock](../composer.lock) resolves +Laravel 12.69.3 and Octane 2.21.0. RoadRunner 2025.1.15 has SHA-256 +`c67dd4dc6d14c0120a7f8a2edcb035eddc693db880d349f3be33a38728b89efb`. +The lock SHA-256 is +`a41fe273640b8562608a0c1de468826c1e983e817bf2a056c1b20e00142eec34`. +The [input manifest](inputs.json) pins backend and measurement tools. +RoadRunner enables its HTTP pool and loopback control RPC, with no jobs pool. + +Both profiles have four HTTP workers. This cohort uses +[`compose.worker-pool.yaml`](../compose.worker-pool.yaml) for two independent +CLI workers, each with its own registration and history cache. The default +Compose recipe has one CLI worker. Results from this pool must not be claimed +for the one-worker configuration. + +The isolated Linux/amd64 host has eight logical CPUs, 16 GiB RAM, AMD EPYC Rome +and no swap. Application, backend and tools run in separate containers on that +host. Backend calls use the internal HTTP network. Each cell starts fresh +application storage, processes, Server database, namespace and role-credential +records. The application lock, +OPcache, cached configuration, backend limits and generator remain fixed. | Component | CPU | Memory, no swap | | --- | ---: | ---: | | HTTP application | 0.9 | 640 MiB | -| Durable CLI worker | 1 | 320 MiB | +| Two durable CLI workers | 0.5 each | 160 MiB each | | Application nginx | 0.1 | 64 MiB | -| Separate Server API | 2 in the earlier pairs, 4 in the October 17 cohort | 1 GiB | +| Separate Server API | 4 | 1 GiB | | Separate MySQL | 1 | 2 GiB | | Separate Server queue worker | 0.2 | 384 MiB | | Separate Server maintenance | 0.2 | 128 MiB | | Separate Redis | 0.1 | 128 MiB | | Separate k6 generator | 0.3 | 512 MiB | -The application allocation totals 2 CPUs and 1 GiB. The backend and tools are -additional resources, not hidden inside that limit. - -Each cell has a 30-second warmup. Measured windows offer six context reads and -one new one-activity workflow per second for 120 seconds, with 64 context and -16 workflow virtual users. Each original keeps its 35-second observer, -600-second run deadline and 900-second execution deadline. The drain limit is -36 seconds. Independent installed PHP readers verify the exact int64, null and -Unicode values and histories. SQL checks original identities, first-attempt -tasks, repairs, pending work and microsecond deadline offsets. - -The comparison plan orders pairs FPM/Octane, Octane/FPM, FPM/Octane, followed by -a fresh FPM drift check. Each profile also requires a fresh 300-second mixed -soak. Its five-original -cycle contains two short workflows, one timer, one queried/signalled workflow -and one queried/cancelled workflow. Two real HTTP reloads are requested at -120 and 240 seconds. Their actual times and replacement worker PIDs are read -back. Automatic HTTP recycling remains enabled at 500 requests per worker. - -## How to read the measurements - -Context, submission and status timings are separate HTTP operations. Workflow -latency is observed by polling at 250 ms intervals. It includes backend work -and that polling delay. Application CPU per terminal workflow includes the -fixed context workload, variable status polls, nginx and the CLI worker. It is -not callback CPU or a measure of durable replay savings. - -The sampler remains active through drain and verification. The tables' CPU -deltas cover offered load and its actual drain; the independent post-run -verification is separate. CPU and throttling use cgroup counters. Process RSS may count shared -pages more than once. Charged cgroup memory is reported separately. Sampled -peaks can miss short spikes; cgroup lifetime peaks also include warmup. -Individual process reads can disappear during recycling and are marked -unavailable rather than counted as measured zeroes. - -Backend access counters measure produced response-body bytes. Workflow poll -envelopes contain inline history and protocol metadata, so those counts are -not pure history bytes. Produced bytes can exceed delivered bytes after an -aborted response. Cache diagnostics are read from the actual stopped worker -and include that cell's prerequisites, warmup and measured window. - -## Published-package controls - -These controls ran on `f027697c` with the earlier two-worker HTTP profile. -Their linked receipts retain that scope. The changed HTTP pool and backend -still require their affected controls before final acceptance. - -The [lifecycle and cold-replacement receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222923826) -covers alternating users, request headers and logs, auth/namespace refusals, -five lifecycle originals, exact typed reads, eager/lazy SDK resolution and -HTTP reloads. An actual CLI SIGKILL followed by a fresh worker completes the -same original with its six-event prefix preserved in the final eight events, -one activity, five first-attempt tasks, zero repairs and unchanged deadlines. - -The [cache and connection receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223066356) -records these independent controls: - -| Control | Result | Actual stopped cache counters: hit / miss / eviction / forced cold | -| --- | --- | --- | -| TTL 1 second, capacity 100 | One original completes with its full prefix | 0 / 3 / 2 / 0 | -| Capacity 1, TTL 300 seconds | Two originals complete with their full prefixes | 0 / 6 / 3 / 4 | -| Actual 10-second API outage | Same original completes within its original 90-second cutoff | 0 / 1 / 0 / 0, final replacement only | - -All four cache/outage originals preserve typed results and exact deadlines, -with 16 completed first-attempt tasks and zero repairs. The outage produces an -actual cURL error 56 and three automatic CLI supervisor restarts. It does not -prove recovery within one process. The intentional kill and outage leave one -killed registration each. Their original cleanup wrappers fail the overly -strict zero-registration predicate; that wrapper result remains unsuccessful. -The task resources are removed. Ordinary measured cells retain the stricter -zero-registration and zero-restart checks. - -Earlier load, controller and older-package observations remain separate -[failed or scoped reference outcomes](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6222599030). -They are not substituted for the final published tuple's measurements. - -## Results - -Fresh repetitions and reload soaks are required after the corrected package is -published and qualified in its normal Server consumer. Earlier results keep -their original application/backend identities and outcomes. - -### Published 2.8.7 four-worker FPM run - -The October 17 mixed run uses the published backend above, unchanged application -runtime `e81b7c80`, four HTTP workers and the four-CPU API allocation. It **fails**: - -| Observation | Result | -| --- | ---: | -| Admitted originals | 265 of 300 required | -| Observed completed / cancelled | 201 / 51 | -| Completed context requests | 1,743 of 1,800 required | -| Dropped offers | 92 | -| Canonical repairs | 82 | -| Canonical completed / cancelled / waiting | 210 / 51 / 4 | - -Both real master reloads replace all four HTTP workers. All 51 observed -cancellation requests and 51 signal requests return their expected 204, and -102 query replies pass their committed-activity checks. The earlier cancellation -deadlock is not observed in this run. That scoped result does not qualify the -load target or establish a cause for the remaining backlog and repairs. - -The original driver also has a separate failure: two iterations start outside -the offered window, fail its boundary assertions and trigger immediate threshold -abort at about 302 seconds. Thirteen observers are interrupted before the declared -36-second drain finishes. The independent audit then correctly refuses an -unfinished signal original. Those transport interruptions are not attributed to -a runtime crash. Dropped offers and repairs independently keep this run failed. - -A later read finds 82 distinct completed workflow tasks with one repair and one -attempt each. Sampled repaired tasks waited four to six seconds before their -first lease. This does not establish why they waited. All 265 activities complete -once in the recorded canonical audit; original run/execution deadline offsets -remain exact. The API records 521.84 CPU seconds, without CPU throttling; the -application records 84.08 CPU seconds. There is no OOM, swap or sampled host CPU -steal. Every normal role exits 0 without automatic restarts, all registrations -are removed, and the stack is retired. - -The prospective driver correction refuses late admissions without inventing -replacement offers and evaluates failures after the original observers drain. -The 35-second per-original observer, 36-second drain, rates, VU limits and all -success thresholds remain unchanged. This original is not rerun or relabeled. -Fresh load acceptance remains required after the correction and diagnosis. - -The synthetic [drain regression](check-load-drain.sh) exercises the actual k6 -driver against an HTTP fixture, not a workflow runtime. Five originals each -remain pending for four seconds during a five-second offered window. A separate -case injects one HTTP failure at the last context request. The old driver aborts -with only two terminal observations; the corrected driver observes all five and -still returns k6's failure exit 99. Its clean case observes all five and exits 0. -CI runs both cases without credentials or a real backend. Run locally with Docker -and jq using `LOAD_DRAIN_RUN=unique-drain-test bash qualification/check-load-drain.sh` -from the example directory. This check does not qualify load, durable execution -or a higher capacity. - -### Published-SDK claim diagnostic - -A separate local diagnostic uses normal SDK 2.4.2 / `25d796ad` with all 166 -source/resource/schema files verified, the published 2.8.7 backend and the same -pure SDK workflow/activity definitions as this application. It exercises eight -fresh short, timer, signal/query and cancellation originals per configuration. -It does not run Laravel HTTP, offered load or a performance comparison. The -shared four-CPU host has existing swap and is not reserved for measurements. - -| Functional observation | One CLI process | Two CLI processes | +The application total is 2 CPUs / 1 GiB. Backend and tool costs are additional. + +## Method + +The fixed schedule offers six authenticated context reads and one original +one-activity workflow per second. Each cell warms up for 30 seconds. Measured +windows last 120 seconds. Mixed soaks last 300 seconds and cycle two short +workflows, one timer, one queried/signalled and one queried/cancelled original. +Real HTTP reloads occur at 120 and 240 seconds during a soak. The actual +replacement PIDs are checked. Both profiles configure a 500-request recycle limit. + +The driver reserves 64 context VUs and 16 workflow VUs. The context reserve +covers six offers per second through the existing ten-second transport cap. +Query requests may take at most 30 seconds, capped by the original observer's +remaining time. Each original keeps a 35-second observer, 600-second run +deadline and 900-second execution deadline. The outer drain is 36 seconds. +No failed or late original is replaced by a new admission. + +Independent installed PHP readers verify the exact bigint, null and Unicode +results and original histories. SQL verifies identities, activity counts, +first attempts, repairs, pending tasks and microsecond deadline offsets. +Dropped offers, request/check failures, unfinished originals, OOM, swapping or +unexpected restarts fail acceptance. Normal cells must deregister fully. + +The repeated comparison orders FPM/Octane, Octane/FPM and FPM/Octane, followed +by a fresh FPM drift cell. All use the same inputs. Successful short windows +cannot substitute for a mixed soak or recovery test. + +### Interpreting resource measurements + +Context, submission and status timings are separate HTTP operations. +Workflow latency includes the 250 ms status-poll interval and backend work. +Application CPU includes the fixed context workload, variable status polls, +nginx and CLI workers. CPU per completed workflow amortizes that whole workload; +it does not isolate callback CPU or measure replay savings. + +Sampling covers every application/backend/tool role through drain and +verification. CPU deltas cover offered load and its actual drain; independent +post-run verification is separate. Cgroup counters measure CPU/throttling and +charged memory. Process RSS can double-count shared pages. Sampled peaks may +miss short spikes; cgroup lifetime peaks also include warmup. Disappearing +process reads are unavailable, not measured zeroes. + +Backend counters measure produced response-body bytes, including protocol +metadata and inline histories. They are not pure history bytes and may exceed +delivered bytes after an aborted response. Stopped worker cache diagnostics +include that cell's prerequisites, warmup and measured window. Zero cache hits +must be reported as zero. + +## Functional controls + +Request identity, namespace/auth refusal, optional headers and logs, lazy/eager +SDK resolution, real reloads, five lifecycle originals and four cold typed +reads pass in the reported source cohorts. The HTTP credential and worker +credential are separate. Each process keeps one fixed authority. + +The [FPM cache/recovery controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243887984) +and [independent Octane controls](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6244222635) +use the published tuple above. Cache columns +are actual stopped counters: hit / miss / eviction / forced cold. + +| Control | Originals / first-attempt tasks per profile | FPM counters | Octane counters | +| --- | --- | --- | --- | +| Capacity 100, TTL 1 second | 1 / 4 | 0 / 3 / 1 / 1 | 0 / 3 / 1 / 1 | +| Capacity 1, TTL 300 seconds | 4 / 16 | 0 / 12 / 7 / 8 | 0 / 12 / 9 / 8 | + +All preserve the entire original prefix, typed result and exact deadlines with +zero repairs, retries or pending work. Cache settings are read from both +running processes before admission. The four-original eviction test populates +two independent caches before releasing any original. The existing two-original +`cache.php eviction` command remains the single-worker control. + +Killing both CLI workers in each profile preserves the same original through +cold replacement: eight final events, one activity and five first-attempt +tasks. Intentional SIGKILL exits are 137 without OOM. The original two killed +registrations remain explicit; replacement workers deregister normally. + +Stopping each isolated API for ten seconds produces actual transport errors. +The original completes with four first-attempt tasks. FPM's workers restart +three and six times; Octane's restart three times each. This proves supervised +process replacement, not same-process recovery. Only the exact original +failed-deregistration IDs are permitted to remain. All final stack roles exit +zero without OOM, with actual restart counts checked individually. + +## Current load results + +### Three alternating short pairs + +All six paired cells and the final FPM drift cell pass: 120 offered/admitted/completed workflows, 720 +context reads and 360 completed first-attempt tasks each. Errors, dropped +offers, repairs, retries and pending work are zero. Independent typed/history +reads and exact original deadlines pass. Every stack shuts down normally with +zero registrations. Each row below is a separate fresh cell in execution order. +Latencies are p50 / p95 / p99 in milliseconds. + +| Cell | Context HTTP | Start HTTP | Status HTTP | Workflow observed | +| --- | ---: | ---: | ---: | ---: | +| FPM 1 | 6.85 / 8.50 / 9.75 | 257.71 / 374.55 / 480.31 | 64.13 / 77.07 / 82.55 | 1,281 / 1,614 / 1,682 | +| Octane 1 | 5.24 / 6.53 / 8.20 | 261.84 / 326.38 / 470.17 | 63.54 / 78.71 / 91.57 | 1,289 / 1,658 / 1,756 | +| Octane 2 | 5.24 / 6.52 / 8.37 | 262.77 / 373.53 / 545.42 | 62.48 / 74.75 / 81.29 | 1,279 / 1,642 / 1,721 | +| FPM 2 | 7.11 / 8.84 / 10.70 | 260.24 / 350.59 / 526.64 | 63.49 / 78.45 / 88.24 | 1,273 / 1,624 / 1,674 | +| FPM 3 | 6.59 / 8.19 / 9.37 | 243.50 / 300.69 / 445.57 | 60.34 / 71.04 / 78.83 | 1,245 / 1,556 / 1,589 | +| Octane 3 | 4.75 / 5.85 / 6.67 | 239.75 / 304.61 / 417.70 | 56.82 / 68.16 / 76.56 | 1,229 / 1,529 / 1,593 | +| FPM drift | 6.45 / 7.96 / 8.63 | 241.05 / 319.95 / 446.68 | 58.83 / 68.81 / 73.03 | 1,236 / 1,539 / 1,568 | + +Memory columns are mean / sampled peak MiB. CPU excludes the separately +measured generator. The application and backend include all nine service roles. + +| Cell | App CPU seconds / per completed workflow | App + backend CPU seconds | App RSS MiB | App charged MiB | App + backend RSS MiB | App + backend charged MiB | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| FPM 1 | 16.84 / 0.140 | 293.84 | 318.17 / 323.60 | 138.80 / 144.38 | 1,370.09 / 1,450.57 | 874.32 / 923.47 | +| Octane 1 | 17.10 / 0.143 | 299.83 | 498.30 / 502.68 | 324.09 / 329.16 | 1,545.67 / 1,585.48 | 1,061.92 / 1,106.83 | +| Octane 2 | 16.91 / 0.141 | 297.81 | 497.76 / 501.94 | 323.64 / 328.27 | 1,542.86 / 1,598.89 | 1,057.78 / 1,109.04 | +| FPM 2 | 17.18 / 0.143 | 294.15 | 318.18 / 323.05 | 138.42 / 143.66 | 1,360.24 / 1,403.87 | 869.44 / 911.87 | +| FPM 3 | 16.23 / 0.135 | 281.01 | 319.68 / 324.63 | 138.58 / 143.96 | 1,365.63 / 1,463.71 | 873.66 / 927.04 | +| Octane 3 | 15.72 / 0.131 | 280.14 | 494.94 / 498.59 | 322.41 / 326.48 | 1,543.15 / 1,628.75 | 1,055.40 / 1,105.88 | +| FPM drift | 15.94 / 0.133 | 278.12 | 318.38 / 324.00 | 138.68 / 144.69 | 1,306.73 / 1,399.14 | 871.40 / 933.98 | + +Three-cell mean application CPU is 16.75 seconds for FPM and 16.58 seconds for +Octane, a 1.05% difference. Individual pair directions differ. Charged application +memory averages 138.60 versus 323.38 MiB, an additional 184.78 MiB for Octane. +The final FPM baseline uses 5.34% less application CPU than the first FPM cell, +and its observed workflow p95 is 4.61% lower. That drift is larger than the +cross-profile mean CPU difference. No consistent CPU or execution gain is +established. Octane's context p95 is 1.97–2.34 ms lower in the paired cells. + +Measured short windows run from October 18 at 01:24:22 through 01:51:04 UTC, +including actual drains. Each cell's runtime and raw samples are deleted after +audit. The temporary host and its task access key are deleted at 01:51:48 UTC. + +### Component and transport costs + +Each entry below is mean CPU seconds / mean throttled seconds / maximum sampled +charged MiB across that profile's three short cells. Per-component peaks occur +at different times and must not be summed as a concurrent peak. + +| Component | FPM | Octane | | --- | ---: | ---: | -| Combined SDK limit | 0.5 CPU / 320 MiB | 0.5 CPU / 320 MiB | -| Completed / cancelled originals | 7 / 1 | 7 / 1 | -| Completed / cancelled tasks | 30 / 1 | 30 / 1 | -| Completed activities | 8 | 8 | -| Repairs / retried tasks / final registrations | 0 / 0 / 0 | 0 / 0 / 0 | -| Maximum observed ready-to-first-lease interval | 5.402 seconds | 2.009 seconds | -| Longest of three actual queries | 5.451 seconds | 2.046 seconds | - -Both batches retain their original 35-second observers, exact typed results and -600/900-second deadlines. Each process stops normally without OOM or automatic -restart. Guzzle's ordinary request/statistics hooks record only allowlisted -poll identities, paths, timing and response metadata. Vendor bytes are unchanged. - -In the one-process original, the worker spends 13.478 seconds in actual HTTP -requests during the approximately 13.6-second active interval. Its workflow -poll replies during queued work take at most 0.470 seconds. The initial held -query poll returns `task_queue_changed`; subsequent sweeps lease workflow tasks. -The 5.451-second query observes `workflow_task_pending` until its own workflow's -pending resume completes, then receives the expected answer. These observations -support trying independent worker concurrency. They do not identify the cause -of the earlier mixed-run repairs, prove a missing wakeup, or establish a gain -or capacity. The two-process batch is fresh evidence, not a rescue of the first. - -The next hosting qualification uses the prospective pool override, the corrected -driver and fresh application processes. Original rates, budgets and success -thresholds stay fixed. Full hosting acceptance remains open. - -### Four-worker FPM mixed runs - -Both runs use runtime source `e81b7c80`, the published backend in the manifest -and the unchanged 300-second mixed workload. The second run increases only the -Server API allocation from two to four CPUs, with its memory still 1 GiB. - -| Observation | Two-CPU API | Four-CPU API | +| HTTP application | 11.57 / 0.00 / 43.96 | 11.48 / 0.00 / 228.05 | +| nginx | 0.98 / 0.01 / 2.66 | 0.94 / 0.00 / 2.84 | +| CLI worker 1 | 2.10 / 0.00 / 48.84 | 2.06 / 0.00 / 49.62 | +| CLI worker 2 | 2.11 / 0.00 / 49.70 | 2.09 / 0.00 / 49.65 | +| Backend MySQL | 69.30 / 0.30 / 610.10 | 70.39 / 0.40 / 611.89 | +| Backend Redis | 7.42 / 14.41 / 6.91 | 7.50 / 14.70 / 7.04 | +| Backend API | 184.27 / 0.00 / 100.40 | 186.16 / 0.00 / 100.33 | +| Backend queue worker | 0.88 / 0.52 / 49.72 | 0.92 / 0.62 / 49.91 | +| Backend maintenance | 11.04 / 45.17 / 46.71 | 11.05 / 45.49 / 45.46 | +| Separate k6 generator | 4.08 / 1.20 / 45.16 | 4.10 / 1.21 / 45.65 | + +Complete service snapshots are at most 3.97 seconds apart with zero unavailable +service samples. Maximum sampled host steal is 2% for FPM and 4% for Octane, +with no swap. Backend throttling is recorded above rather than attributed to +HTTP hosting. + +The following are three-cell means of actual backend requests / produced +response-body MiB. Poll counts include empty polls. These envelopes contain +metadata and inline histories; the byte totals do not isolate history payloads. + +| Backend operation | FPM requests / MiB | Octane requests / MiB | | --- | ---: | ---: | -| Admitted original workflows | 233 | 105 | -| Observed completions | 174 | 76 | -| Observed cancellations | 44 | 18 | -| Context responses | 1,728 | 727 | -| Dropped offers | 140 | 61 | -| Observer errors | 1 | 1 | -| Repairs on canonical readback | 118 | 35 | -| Result | Failed | Failed | - -The [two-CPU API run](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223818842) -passes both real master reloads and all HTTP requests, but fails the offered -workload and zero-repair criteria. Later canonical readback has 185 completed, -44 cancelled and four waiting originals. The API records 514.73 CPU seconds -over 302.99 elapsed seconds, including 38.32 seconds throttled. This is one -observed resource limit, not a proven explanation for every missed criterion. - -The separate four-CPU API run stops at a real cancellation HTTP 500. MySQL's -actual error 1213 lock graph shows signal and cancellation transactions for -different runs acquiring task/failure index locks in a cycle. MySQL rolls back -cancellation completely. Its original remains waiting with the same six-event -prefix and no failure record. Later aggregate readback has 82 completed, -18 cancelled and five waiting originals. Workflow #819 owns the reproduction, -compatible package correction and affected published consumer. - -Increasing API CPU is not an established fix, and neither run qualifies the -four-worker recipe. Their resources and failed originals were retired after -the immediate diagnosis. The narrow failure description remains in the owning -issue. No Octane mixed run or fresh four-worker paired comparison is claimed. - -### Earlier two-worker profile - -The earlier source `f027697c` completed three short-window pairs and an FPM -drift check. The [paired receipt](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223479757) -records these measured windows, each with 120 completed workflows and 720 -context reads per profile: - -| Pair order | FPM app CPU seconds | Octane app CPU seconds | FPM / Octane observed workflow p95 ms | -| --- | ---: | ---: | ---: | -| FPM, Octane | 18.089 | 15.774 | 3,312 / 2,515 | -| Octane, FPM | 18.528 | 14.809 | 3,146 / 1,554 | -| FPM, Octane | 14.843 | 14.791 | 1,583 / 1,555 | - -The third pair's application CPU differs by only 0.35%. The receipt records -mean charged application memory across these pairs of 88.82 MiB for FPM and -191.57 MiB for Octane. These limited observations do not establish a consistent -speedup or durable execution gain. FPM remains the ordinary starting profile. - -Its first FPM mixed soak [failed](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223653640): -100 admitted originals, 70 observed completions, 18 observed cancellations, -457 context responses, 282 dropped iterations and two HTTP 502s. Independent -nginx logs show connection resets at the first FPM master reload. Subsequent -canonical readback has 78 completed, 18 cancelled and four waiting originals, -with 16 repairs. No OOM or automatic process restart occurred. - -That failed outcome is unchanged. Its short-window results do not qualify the -four-worker recipe or establish mixed-traffic capacity. The prospective recipe -keeps the same CPU/memory allocation and workload, increases both HTTP pools -and gives FPM a bounded in-flight request grace period. New measurements are -required for the changed profile. +| Workflow-task poll | 666.67 / 17.08 | 664.00 / 17.02 | +| Activity-task poll | 668.67 / 15.60 | 666.00 / 15.54 | +| Query-task poll | 668.67 / 15.54 | 666.00 / 15.48 | +| Other successful GETs | 616.33 / 71.78 | 627.67 / 71.82 | +| Other successful POSTs, 200 | 646.00 / 15.13 | 646.00 / 15.13 | +| Original admission POSTs, 201 | 120.00 / 0.15 | 120.00 / 0.15 | +| Health | 12.00 / 0.06 | 12.00 / 0.06 | + +Stopped CLI cache totals over prerequisites, warmup and the three short cells +are FPM **0 hits / 936 misses / 59 evictions / 471 forced cold replays**, and +Octane **0 / 936 / 64 / 471**. No history-cache hit or replay saving is +demonstrated by these runs. The 100-entry/300-second defaults remain unchanged. + +### Mixed reload soaks + +Both fresh 64-context-VU mixed cohorts pass on the same inputs. Each admits all +300 offered originals, observes 240 completions and 60 cancellations, and +completes all 1,800 contexts. All response/history checks pass. Independent +typed reads and SQL find 300 activities completed once, 1,080 completed +first-attempt tasks, 60 cancelled tasks and zero repairs, retries or pending +work. The sixty unclaimed tasks are cancelled terminal tasks, not backlog. +Original 600/900-second deadline offsets remain exact. + +| Profile | Application CPU seconds | Charged memory mean / peak MiB | Context HTTP p95 ms | Observed workflow p95 ms | Timer due-to-claim range ms | +| --- | ---: | ---: | ---: | ---: | ---: | +| FPM | 53.51 | 150.10 / 162.93 | 8.92 | 3,257 | 170–1,491 | +| Octane | 56.18 | 333.59 / 375.79 | 10.70 | 5,821 | 197–2,811 | + +Both scheduled reloads replace all four HTTP workers in each profile. All +eleven stack roles subsequently exit zero without OOM or automatic restarts; +registrations return to zero. There is no swapping. Maximum sampled host CPU +steal is 2% for FPM and 1% for Octane. Runtime, databases and raw samples are +deleted after audit. These are bounded soaks and do not establish long-term +capacity or a general speedup. + +The [16-context-VU FPM cohort](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243824493) +passes its exact scope: 300 admitted/terminal originals, 240 completed and +60 cancelled, 1,800 contexts, zero errors/drops/repairs/retries/pending work. +Both reloads replace all four workers. Original histories, typed values and +deadlines pass. All eleven roles exit zero without OOM/restarts and +registrations return to zero. + +Its [separate Octane original](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243983972) +is **failed**: all 300 workflows and response/history checks pass, but twelve +context offers are dropped at reload. k6 reports exhausting sixteen context +VUs; actual context latency reaches 4.002 seconds within the ten-second limit. +The current 64-VU reserve is a new common measurement input. The unchanged +16-VU driver fails a constructed nine-second context-delay control; all four +corrected driver controls pass, with injected HTTP failure retaining exit 99. +No application defect or gain is inferred from generator exhaustion. + +## Earlier failures and scoped observations + +Earlier originals remain failed or scoped as recorded in their owning links. +They are not rerun in place or substituted for current results. + +- The first Octane pool expiry control completes its original with an actual + eviction, exact history/deadlines and zero repairs, but its helper wrongly + requires a forced cold replay after expiry. Actual counters are 0 hits, + 3 misses, 1 eviction and 0 forced cold replays. The documented expiry rule + permits that zero. The corrected helper and a separate fresh original pass; + the failed control stays failed. Capacity still requires forced cold replay. +- [First two-CLI-worker mixed run](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243742751): + all 300 admissions, but only 239 completions/59 cancellations observed and + three context drops. The driver confused generic waiting with a committed + activity. The corrected driver waits for the example's explicit query phase + within the original cutoff. Old and corrected synthetic controls establish + that driver correction; the failed application run remains failed. +- [Earlier published 2.8.7 one-worker mixed run](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243209312): + 265 admissions, 252 observed terminal outcomes, 92 dropped offers and 82 + repairs. Driver threshold abort also interrupted thirteen observers before + drain. Later diagnosis does not establish a cause for every delay or repair. +- [Earlier four-HTTP-worker runs](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223818842): + insufficient offers/repairs in one cell and an actual MySQL cancellation + deadlock in another. [Workflow #819](https://github.com/durable-workflow/workflow/issues/819#issuecomment-6243219869) + delivers that specific correction through normal source and published HTTP + consumers. This does not qualify the earlier load results or prove a common + cause for other observations. +- [Earlier two-HTTP-worker short pairs](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223479757) + finish 120 workflows and 720 contexts per cell, but one pair differs by only + 0.35% application CPU. The associated [mixed soak fails](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6223653640) + with reload connection resets and unfinished work. Those old profiles do not + establish a consistent gain or qualify the current four-worker recipe. +- [Published-SDK one/two-process diagnostics](https://github.com/durable-workflow/sdk-php/pull/169#issuecomment-6243209312) + support testing independent concurrency with exact typed histories and no + repairs in their eight-original batches. They are functional observations + on a shared host, not a performance comparison. + +Completed cell databases, histories, raw samples and transport are deleted +after immediate readback. This readable report and owning GitHub results retain +the useful conclusions. Temporary dependency/build state and the isolated host +are retired when the qualification phase ends.