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/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 a69a618b..14bbe8c1 100644 --- a/docs/portal/frameworks/laravel.md +++ b/docs/portal/frameworks/laravel.md @@ -459,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/.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..d06d6131 --- /dev/null +++ b/examples/laravel-http/README.md @@ -0,0 +1,266 @@ +# Laravel service HTTP example + +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. + +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 +workers only submit and inspect runtime operations. Server or Cloud owns durable +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 +`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= +``` + +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 -f compose.worker-pool.yaml 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 optional Octane profile: + +```bash +docker compose --env-file /private/example.env -f compose.yaml -f compose.worker-pool.yaml up --build -d +``` + +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, 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 `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 +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. +`qualification/inputs.json` records the pinned runtime and tool images for the +comparison; application dependencies remain in the normal Composer lock. + +## 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. + +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. + +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. + +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 +`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. + +### 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 +``` + +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 +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, +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. + +`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 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 +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. + +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 +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. +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 +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 +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. 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 +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,recovery-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..b969eb14 --- /dev/null +++ b/examples/laravel-http/app/Models/User.php @@ -0,0 +1,48 @@ + */ + 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..a79c2def --- /dev/null +++ b/examples/laravel-http/app/Providers/AppServiceProvider.php @@ -0,0 +1,20 @@ +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/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 === 'recovery-timer') { + $context->sleep(10); + } 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..4b51fdc4 --- /dev/null +++ b/examples/laravel-http/bootstrap/app.php @@ -0,0 +1,32 @@ +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 { + $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'); + +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} + 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=4 --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} + EXAMPLE_PROCESS_ROLE: worker + init: true + stop_signal: SIGTERM + 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..ac6de8e4 --- /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.2", + "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..c8ade2d7 --- /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": "b7fc96c5a47dec4499520543a32a26bf", + "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.2", + "source": { + "type": "git", + "url": "https://github.com/durable-workflow/sdk-php.git", + "reference": "25d796ad2a55d0d114b58017c3ba23390b09e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/durable-workflow/sdk-php/zipball/25d796ad2a55d0d114b58017c3ba23390b09e120", + "reference": "25d796ad2a55d0d114b58017c3ba23390b09e120", + "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.2", + "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-15T03:25:43+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..c959ecae --- /dev/null +++ b/examples/laravel-http/config/logging.php @@ -0,0 +1,134 @@ + 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' => $logFile, + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => $logFile, + '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' => $logFile, + ], + + ], + +]; 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/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/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; 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..2ae88514 --- /dev/null +++ b/examples/laravel-http/qualification/check-load-drain.sh @@ -0,0 +1,64 @@ +#!/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 +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 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=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" \ + -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 + 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=$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 + 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=="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 --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 + docker rm "$run-http" >/dev/null +done diff --git a/examples/laravel-http/qualification/context.mjs b/examples/laravel-http/qualification/context.mjs new file mode 100644 index 00000000..932523c3 --- /dev/null +++ b/examples/laravel-http/qualification/context.mjs @@ -0,0 +1,58 @@ +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': `${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, 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'] ?? ''); + 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(); +} + +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/qualification/inputs.json b/examples/laravel-http/qualification/inputs.json new file mode 100644 index 00000000..2001daf1 --- /dev/null +++ b/examples/laravel-http/qualification/inputs.json @@ -0,0 +1,23 @@ +{ + "platform": "linux/amd64", + "server": { + "version": "2.8.7", + "commit": "964f9e3d5b713376757294fd38c2cb3b4cba1400", + "image": "ghcr.io/durable-workflow/server@sha256:27c08e15c86e897703c2bab87dcf61847946b379876b15d0a3a491bc91cf30cd", + "workflow_version": "2.5.32" + }, + "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/lifecycle.mjs b/examples/laravel-http/qualification/lifecycle.mjs new file mode 100644 index 00000000..8b2885d5 --- /dev/null +++ b/examples/laravel-http/qualification/lifecycle.mjs @@ -0,0 +1,77 @@ +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, 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', + '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, 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.'); + 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.`); +} + +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, deadline}); +} + +for (const original of originals.filter(value => ['short', 'timer'].includes(value.kind))) { + 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); + 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', 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', 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 new file mode 100644 index 00000000..c3ca8c77 --- /dev/null +++ b/examples/laravel-http/qualification/load-drain-fixture.mjs @@ -0,0 +1,90 @@ +// 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 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; + +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, earlyActions, activityQueries})); + 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 (contextDelay > 0) await new Promise(resolve => setTimeout(resolve, contextDelay)); + 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 (!['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, {started: now, kind, queries: 0, terminal: false}); + response.writeHead(201).end(JSON.stringify({workflow_id: `${user}-${id}`, run_id: id})); + return; + } + 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 (['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; + } + 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 new file mode 100644 index 00000000..c667a1b1 --- /dev/null +++ b/examples/laravel-http/qualification/load.js @@ -0,0 +1,189 @@ +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'; +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.'); +} + +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'); +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)])); +let requestNumber = 0; +const seenPids = new Set(); + +export const options = { + scenarios: { + context: {executor: 'constant-arrival-rate', rate: 6, timeUnit: '1s', duration: `${seconds}s`, + 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'}, + }, + thresholds: { + checks: ['rate==1'], + run_errors: ['count==0'], + http_req_failed: ['rate==0'], + 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 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', + 'Content-Type': 'application/json', 'X-Request-Id': requestId, 'X-Namespace': namespace}, + timeout: `${Math.max(1, Math.floor(Math.min(transportLimitMs, remainingMs)))}ms`, + 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'); + 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; +} + +export function context() { + errors.add(0); + boundaryIterations.add(0); + const index = execution.scenario.iterationInTest; + if (index >= seconds * 6 || Date.now() >= execution.scenario.startTime + seconds * 1000) { + boundaryIterations.add(1); + return; + } + try { + 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'); + 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'); + 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.'); +} + +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); + completed.add(0); + cancelled.add(0); + boundaryIterations.add(0); + const index = execution.scenario.iterationInTest; + const started = Date.now(); + if (index >= seconds || started >= execution.scenario.startTime + seconds * 1000) { + 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 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); + waitForActivity(user, id, cutoff); + 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) { + 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, + 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/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; diff --git a/examples/laravel-http/qualification/report.md b/examples/laravel-http/qualification/report.md new file mode 100644 index 00000000..9fdf0487 --- /dev/null +++ b/examples/laravel-http/qualification/report.md @@ -0,0 +1,306 @@ +# 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 | +| Two durable CLI workers | 0.5 each | 160 MiB each | +| Application nginx | 0.1 | 64 MiB | +| 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 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 | +| --- | ---: | ---: | +| 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 | +| --- | ---: | ---: | +| 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. 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"; diff --git a/examples/laravel-http/routes/api.php b/examples/laravel-http/routes/api.php new file mode 100644 index 00000000..3e908c7b --- /dev/null +++ b/examples/laravel-http/routes/api.php @@ -0,0 +1,21 @@ +group(function (): void { + Route::get('context', function (Request $request, Client $client) { + 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']); + 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 @@ +